In the previous blog, I have learned, File uploading to the server from an activity. Now In this article, I’m going to explain how to upload file to the server using Android Service.
In earlier, Normally We use IntentService to perform background task, it is an advanced form of background service in android. That creates a separate thread to perform background operation. But Android Oreo imposed some limitation of background execution. I have written a separate article on it you can read here. So IntentService does not work in Oreo devices and onward while
Step for implementation
- Create a new class and extends JobIntentService.
- Now override the onHandleWork() methods.
- Setup Retrofit instance and Expose API file upload API
- Place file upload logic in onHandleWork()
- Create a local BroadCastReceiver() that update the result of service in activity
- Now expose enqueueWork() to start this service
- Open MainActivity and starts the JobIntentService
- We have to pass file path with Intent so write a below code for getting
file from camera and intent. - Open AndroidManifest and add followings code
Upload Files to Server using Service (Demo App)
Let’s implements above one by one.
1. Create a new class and extends JobIntentService
Create a new project with Basic Template. open app/build
implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.retrofit2:converter-gson:2.4.0' // reactive implementation 'io.reactivex.rxjava2:rxjava:2.1.10' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' /** * dependency to request the runtime permissions. */ implementation 'com.karumi:dexter:5.0.0' implementation 'com.github.bumptech.glide:glide:4.8.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
After that create a new class in named is FileUploadService and extends JobIntentService. Then override the onHandleWork() methods.
package com.wave.fileuploadservice; import android.content.Intent; import android.support.annotation.NonNull; import android.support.v4.app.JobIntentService; /** * Created on : Mar 13, 2019 * Author : AndroidWave */ public class FileUploadService extends JobIntentService { @Override protected void onHandleWork(@NonNull Intent intent) { } }
2. Setup Retrofit instance and Expose API file upload API
Create a interface named RestApiService and expose file upload API
package com.wave.fileuploadservice.service; import io.reactivex.Single; import okhttp3.MultipartBody; import okhttp3.RequestBody; import okhttp3.ResponseBody; import retrofit2.http.Multipart; import retrofit2.http.POST; import retrofit2.http.Part; /** * Created on : Feb 25, 2019 */ public interface RestApiService { @Multipart @POST("fileUpload.php") Single<ResponseBody> onFileUpload(@Part("email") RequestBody mEmail, @Part MultipartBody.Part file); }
Now create instances of Retrofit like this
package com.wave.fileuploadservice.service; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.Request; import retrofit2.Retrofit; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.converter.gson.GsonConverterFactory; import static com.wave.fileuploadservice.BuildConfig.BASE_URL; /** * Created on : Feb 25, 2019 * Author : AndroidWave */ public class RetrofitInstance { private static Retrofit retrofit = null; public static RestApiService getApiService() { OkHttpClient client = new OkHttpClient.Builder() .readTimeout(2, TimeUnit.MINUTES) .writeTimeout(2, TimeUnit.MINUTES).addInterceptor(chain -> { Request original = chain.request(); Request.Builder requestBuilder = requestBuilder = original.newBuilder() .method(original.method(), original.body()); Request request = requestBuilder.build(); return chain.proceed(request); }).build(); if (retrofit == null) { retrofit = new Retrofit .Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); } return retrofit.create(RestApiService.class); } }
3. Create a utility class named is CountingRequestBody
package com.wave.fileuploadservice.service; import android.support.annotation.NonNull; import java.io.IOException; import okhttp3.MediaType; import okhttp3.RequestBody; import okio.Buffer; import okio.BufferedSink; import okio.ForwardingSink; import okio.Okio; import okio.Sink; /** * Created on : Dec 30, 2018 */ public class CountingRequestBody extends RequestBody { private final RequestBody delegate; private final Listener listener; public CountingRequestBody(RequestBody delegate, Listener listener) { this.delegate = delegate; this.listener = listener; } @Override public MediaType contentType() { return delegate.contentType(); } @Override public long contentLength() { try { return delegate.contentLength(); } catch (IOException e) { e.printStackTrace(); } return -1; } @Override public void writeTo(@NonNull BufferedSink sink) throws IOException { CountingSink countingSink = new CountingSink(sink); BufferedSink bufferedSink = Okio.buffer(countingSink); delegate.writeTo(bufferedSink); bufferedSink.flush(); } final class CountingSink extends ForwardingSink { private long bytesWritten = 0; CountingSink(Sink delegate) { super(delegate); } @Override public void write(@NonNull Buffer source, long byteCount) throws IOException { super.write(source, byteCount); bytesWritten += byteCount; listener.onRequestProgress(bytesWritten, contentLength()); } } public interface Listener { void onRequestProgress(long bytesWritten, long contentLength); } }
4.. Open the FileUploadService and do following operation
Furthermore, open the FileUploadService again and call the API likes below
- Place file upload logic in onHandleWork()
- Create a local BroadCastReceiver() that updates the result in activity
- Now expose enqueueWork() to start this service
The complete FileUploadService is looks like this
package com.wave.fileuploadservice; import android.content.Context; import android.content.Intent; import android.support.annotation.NonNull; import android.support.v4.app.JobIntentService; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.wave.fileuploadservice.service.CountingRequestBody; import com.wave.fileuploadservice.service.RestApiService; import com.wave.fileuploadservice.service.RetrofitInstance; import com.wave.fileuploadservice.utils.MIMEType; import io.reactivex.BackpressureStrategy; import io.reactivex.Flowable; import io.reactivex.FlowableEmitter; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import java.io.File; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; public class FileUploadService extends JobIntentService { private static final String TAG = "FileUploadService"; Disposable mDisposable; /** * Unique job ID for this service. */ private static final int JOB_ID = 102; public static void enqueueWork(Context context, Intent intent) { enqueueWork(context, FileUploadService.class, JOB_ID, intent); } @Override public void onCreate() { super.onCreate(); } @Override protected void onHandleWork(@NonNull Intent intent) { /** * Download/Upload of file * The system or framework is already holding a wake lock for us at this point */ // get file file here String mFilePath = intent.getStringExtra("mFilePath"); if (mFilePath == null) { Log.e(TAG, "onHandleWork: Invalid file URI"); return; } RestApiService apiService = RetrofitInstance.getApiService(); Flowable<Double> fileObservable = Flowable.create(emitter -> { apiService.onFileUpload(createRequestBodyFromText("info@androidwave.com"), createMultipartBody(mFilePath, emitter)).blockingGet(); emitter.onComplete(); }, BackpressureStrategy.LATEST); mDisposable = fileObservable.subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(progress -> onProgress(progress), throwable -> onErrors(throwable), () -> onSuccess()); } private void onErrors(Throwable throwable) { sendBroadcastMeaasge("Error in file upload " + throwable.getMessage()); Log.e(TAG, "onErrors: ", throwable); } private void onProgress(Double progress) { sendBroadcastMeaasge("Uploading in progress... " + (int) (100 * progress)); Log.i(TAG, "onProgress: " + progress); } private void onSuccess() { sendBroadcastMeaasge("File uploading successful "); Log.i(TAG, "onSuccess: File Uploaded"); } public void sendBroadcastMeaasge(String message) { Intent localIntent = new Intent("my.own.broadcast"); localIntent.putExtra("result", message); LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent); } private RequestBody createRequestBodyFromFile(File file, String mimeType) { return RequestBody.create(MediaType.parse(mimeType), file); } private RequestBody createRequestBodyFromText(String mText) { return RequestBody.create(MediaType.parse("text/plain"), mText); } /** * return multi part body in format of FlowableEmitter */ private MultipartBody.Part createMultipartBody(String filePath, FlowableEmitter<Double> emitter) { File file = new File(filePath); return MultipartBody.Part.createFormData("myFile", file.getName(), createCountingRequestBody(file, MIMEType.IMAGE.value, emitter)); } private RequestBody createCountingRequestBody(File file, String mimeType, FlowableEmitter<Double> emitter) { RequestBody requestBody = createRequestBodyFromFile(file, mimeType); return new CountingRequestBody(requestBody, (bytesWritten, contentLength) -> { double progress = (1.0 * bytesWritten) / contentLength; emitter.onNext(progress); }); } }
4.1 Declare service inside the Manifest
- For Pre-Oreo devices => We have to set uses permission – WAKE_LOCK permission
- For Oreo device => you have to declare android.permission.BIND_JOB_SERVICE
<!--for JobIntentService--> <uses-permission android:name="android.permission.WAKE_LOCK" /> <service android:name=".FileUploadService" android:permission="android.permission.BIND_JOB_SERVICE" />
5. Open activity_main.xml and add below code
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ImageView android:id="@+id/ivDisplayImage" android:layout_width="wrap_content" android:layout_height="200dp" android:layout_marginStart="8dp" android:layout_marginTop="56dp" android:layout_marginEnd="8dp" android:layout_marginBottom="16dp" app:layout_constraintBottom_toTopOf="@+id/tvSelectedFilePath" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" app:srcCompat="@drawable/photo" /> <Button android:id="@+id/buttonUpload" android:layout_width="200dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:background="@color/colorAccent" android:text="UPLOAD" android:textColor="#fff" android:textSize="18sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tvSelectedFilePath" /> <TextView android:id="@+id/tvSelectedFilePath" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="32dp" android:layout_marginEnd="8dp" android:layout_marginBottom="16dp" android:gravity="center" android:hint="Selected file" android:textSize="18sp" android:textColor="#161616" app:layout_constraintBottom_toTopOf="@+id/buttonUpload" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/ivDisplayImage" /> <ImageView android:id="@+id/imageView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="16dp" app:layout_constraintBottom_toTopOf="@+id/tvSelectedFilePath" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:srcCompat="@drawable/pencil" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="32dp" android:layout_marginEnd="8dp" android:text="File Uploading using Service Demo App" android:textColor="@color/colorPrimary" android:textSize="18sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/txvResult" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" android:text="Result" android:textSize="17sp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/buttonUpload" /> </android.support.constraint.ConstraintLayout>
6. Write a File Provider
In this demo, we will capture image from camera and gallery with the help of FileProvider. I have written a separate article on capturing image from camera/gallery using FileProvider. I would like to suggest Read this article more clarity.
6.1 Go to res => xml => create a new xml file named is file_provider_path.xml and paste that code
<?xml version="1.0" encoding="utf-8"?> <paths> <external-path name="my_images" path="Android/data/com.wave.fileuploadservice/files/Pictures" /> <!-- replace com.wave.fileuploadservice with your package name --> </paths>
6.2 Now open the AndroidManifest.xml and declare file provider inside the application tag
<provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_provider_paths" /> </provider>
6.3 Declare storage and camera permission in manifest
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.camera.flash" />
7. Finally, Open the Activity (MainActivity) add do below operation
- We are using Dexter for using permission on run time. So Expose requestStoragePermission() for requesting permission
- Create methods for preparing intent for camera and gallery names is startCamera() and
chooseGallery () - Handle callback result in onActivityResult()
- Register and UnRegister local BroadcastReceiver
Final code of Activity looks like below
package com.wave.fileuploadservice; import android.Manifest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; import android.support.v4.content.FileProvider; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.karumi.dexter.Dexter; import com.karumi.dexter.MultiplePermissionsReport; import com.karumi.dexter.PermissionToken; import com.karumi.dexter.listener.PermissionRequest; import com.karumi.dexter.listener.multi.MultiplePermissionsListener; import java.io.File; import java.io.IOException; import java.util.Calendar; import java.util.List; public class MainActivity extends AppCompatActivity implements View.OnClickListener { static final int REQUEST_TAKE_PHOTO = 101; static final int REQUEST_GALLERY_PHOTO = 102; File mPhotoFile; ImageView ivDisplayImage; Button buttonUpload; TextView tvSelectedFilePath; ImageView ivSelectImage; TextView txvResult; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ivDisplayImage = findViewById(R.id.ivDisplayImage); buttonUpload = findViewById(R.id.buttonUpload); tvSelectedFilePath = findViewById(R.id.tvSelectedFilePath); ivSelectImage = findViewById(R.id.imageView2); txvResult = findViewById(R.id.txvResult); buttonUpload.setOnClickListener(this); ivSelectImage.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.buttonUpload: if (tvSelectedFilePath.getText().toString().isEmpty()) { Toast.makeText(this, "Select file first", Toast.LENGTH_LONG).show(); return; } Intent mIntent = new Intent(this, FileUploadService.class); mIntent.putExtra("mFilePath", tvSelectedFilePath.getText().toString()); FileUploadService.enqueueWork(this, mIntent); break; case R.id.imageView2: selectImage(); break; } } /** * Alert dialog for capture or select from galley */ private void selectImage() { final CharSequence[] items = { "Take Photo", "Choose from Library", "Cancel" }; AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setItems(items, (dialog, item) -> { if (items[item].equals("Take Photo")) { requestStoragePermission(true); } else if (items[item].equals("Choose from Library")) { requestStoragePermission(false); } else if (items[item].equals("Cancel")) { dialog.dismiss(); } }); builder.show(); } /** * Requesting multiple permissions (storage and camera) at once * This uses multiple permission model from dexter * On permanent denial opens settings dialog */ private void requestStoragePermission(boolean isCamera) { Dexter.withActivity(this) .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA) .withListener(new MultiplePermissionsListener() { @Override public void onPermissionsChecked(MultiplePermissionsReport report) { // check if all permissions are granted if (report.areAllPermissionsGranted()) { if (isCamera) { startCamera(); } else { chooseGallery(); } } // check for permanent denial of any permission if (report.isAnyPermissionPermanentlyDenied()) { // show alert dialog navigating to Settings chooseGallery(); } } @Override public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions, PermissionToken token) { token.continuePermissionRequest(); } }) .withErrorListener( error -> Toast.makeText(getApplicationContext(), "Error occurred! ", Toast.LENGTH_SHORT) .show()) .onSameThread() .check(); } public void startCamera() { mPhotoFile = newFile(); Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { if (mPhotoFile != null) { Uri photoURI = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider", mPhotoFile); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); } } } public void chooseGallery() { Intent pickPhoto = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); pickPhoto.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivityForResult(pickPhoto, REQUEST_GALLERY_PHOTO); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { if (requestCode == REQUEST_TAKE_PHOTO) { tvSelectedFilePath.setText(mPhotoFile.getAbsolutePath()); Glide.with(MainActivity.this) .load(mPhotoFile) .apply(new RequestOptions().centerCrop().circleCrop()) .into(ivDisplayImage); } else if (requestCode == REQUEST_GALLERY_PHOTO) { Uri selectedImage = data.getData(); tvSelectedFilePath.setText(getRealPathFromUri(selectedImage)); Glide.with(MainActivity.this) .load(getRealPathFromUri(selectedImage)) .apply(new RequestOptions().centerCrop().circleCrop()) .into(ivDisplayImage); } } } public File newFile() { Calendar cal = Calendar.getInstance(); long timeInMillis = cal.getTimeInMillis(); String mFileName = String.valueOf(timeInMillis) + ".jpeg"; File mFilePath = getFilePath(); try { File newFile = new File(mFilePath.getAbsolutePath(), mFileName); newFile.createNewFile(); return newFile; } catch (IOException e) { e.printStackTrace(); } return null; } public File getFilePath() { return getExternalFilesDir(Environment.DIRECTORY_PICTURES); } public String getRealPathFromUri(Uri contentUri) { Cursor cursor = null; try { String[] proj = { MediaStore.Images.Media.DATA }; cursor = getContentResolver().query(contentUri, proj, null, null, null); assert cursor != null; int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); return cursor.getString(columnIndex); } finally { if (cursor != null) { cursor.close(); } } } @Override protected void onResume() { super.onResume(); IntentFilter intentFilter = new IntentFilter("my.own.broadcast"); LocalBroadcastManager.getInstance(this) .registerReceiver(myLocalBroadcastReceiver, intentFilter); } private BroadcastReceiver myLocalBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String result = intent.getStringExtra("result"); txvResult.setText(result); } }; @Override protected void onPause() { super.onPause(); LocalBroadcastManager.getInstance(this).unregisterReceiver(myLocalBroadcastReceiver); } }
I’m new in Android so If you have any queries or suggestions, feel free to ask them in the comment section below. Happy Coding 🙂
13 Comments
fantastic work
where did you define the BASE_URL ? and where’s the destination to go to?
app/build.gradle
Hello its nice Tutorial its working but whenever i clear activity notification not showing and service is stop . how can i upload from background .
This solution is outdated you can use this one https://androidwave.com/upload-manager/
You could give me the server code, so he can get the image, I do not know how I can not find it in his tutorial. grateful
Sure will update soon
Hi,
get error (unexpected end of stream)
can u help me??
i search the internet but cant find great answer for this error
Need some more details to debug it.
how to return json from this server only return progress i wanr to return json my pojo
At a time you can get one thing json or progress.
Hi,
its nice tutorial,
but where is “MIMEType” defined?
package com.wave.fileuploadservice.utils;
public enum MIMEType {
IMAGE(“image/*”), VIDEO(“video/*”);
public String value;
MIMEType(String value) {
this.value = value;
}
}