Core Android

Upload Manager – Display Notification Progress Bar while Uploading

In this post, we will create a sample app Upload Manager such as Android Download Manager. In this android app tutorial we build a complete solution for file uploading with head up a notification, No matter your app is background or foreground because that time we are using Service Enqueue for file uploading, In case of failure, the user can retry or cancel file upload without open the application. I will try to make a complete solution just like a phone download manager.

Prerequisite

In this article, We are using Retrofit, JobIntentService and Broadcast Receiver, Notification Service. You have a deep knowledge of each one. I have written articles for all major concepts. Read for capture file from Camera & Gallery using FileProvider. For Working with JobIntentService follow this article.

Data Flow of this Upload Manager

Step for implementation Upload Manager

  • Create a new project with min SDK 21.
  • Add lib dependency in app/build.gradle
  • Create a Retrofit instance for calling file upload service
  • Forgetting file upload progress let’s creates a CountingRequestBody
  • Now create a subclass of JobIntentService
  • Furthermore, Create a BroadcastReceiver for listening file upload progress
  • Create another BroadcastReceiver with Retry and Cancel action button. While any error occurred during file upload user can retry for file upload
  • In MainActivity, We write code for getting the file from camera & gallery using FileProvider for upload file to the server
  • Finally, Enqueue the job to JobIntentService.

After following above step we will prepare Upload Manager (Demo App)

1. Create Project

Let move to android studio and create a new project with named FileUploadService. Choose min SDK version 21 and select EmptyActivity template.

Let’s go to res =>value => open string.xml file add some string constant that we are using in this project.

<resources>
    <string name="app_name">Upload Manager</string>
    <string name="noti_channel_default">Default Channel</string>
    <string name="btn_retry_not">Retry</string>
    <string name="btn_cancel_not">Cancel</string>
    <string name="file_upload_successful">File has been uploaded successfully</string>
    <string name="uploading">Uploading</string>
    <string name="in_progress">in progress</string>
    <string name="message_failed">File has been not uploaded</string>
    <string name="message_upload_success">Uploading Success</string>
    <string name="error_upload_failed">Uploading failed</string>
    <string name="message_upload_failed">File is not uploaded. Please TRY AGAIN</string>
</resources>
2. Add Dependency

In this android app tutorial, we are using for libraries. These are listed below.

  • Retrofit – I think no need to much introduction about that. You guys were already aware that one. Retrofit mostly used for calling Remote API
  • RxAndroid and RxJava – RxJava and RxJava are most common libraries for these days. They provide react feature in android app development
  • Dexter – Manage run time permission in android
  • Glide – is image loading libraries that use to show image on the ImageView in android.

Let’s open the app build.gradle file and add some dependencies for using necessary libraries

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    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'
}
3. Let’s Prepare Retrofit instance

Create a new Retrofit Interface and define a method named on file upload. We will use this one for file upload.

3.1 – Interface RestApiService
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
 * Author     : AndroidWave
 */
public interface RestApiService {


    @Multipart
    @POST("fileUpload.php")
    Single<ResponseBody> onFileUpload(@Part("email") RequestBody mEmail, @Part MultipartBody.Part file);

}
3.2 – Create Retrofit Service class using RestApiService interface

On above I have created RestApiService interface. Let’s create a service class that return Retrofit instance. We have to add converter factory as well such as RxJava2CallAdapterFactory and GsonConverterFactory

package com.wave.fileuploadservice.service;

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
 */
public class RetrofitInstance {

    private static Retrofit retrofit = null;

    public static RestApiService getApiService() {
        if (retrofit == null) {
            retrofit = new Retrofit
                    .Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .build();

        }
        return retrofit.create(RestApiService.class);

    }
}
4. Creates a countable RequestBody for listening file progress
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;

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);
    }
}
5. Now creates a FileUploadService

Create a new subclass of JobIntentService in src folder named is FileUploadService. In this service we are majorly doing three things. let’s check above diagram. We are using two BroadcastReceiver one for listing file upload progress. Second for retry and cancel action button.

package com.wave.fileuploadservice;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.wave.fileuploadservice.receiver.FileProgressReceiver;
import com.wave.fileuploadservice.receiver.RetryJobReceiver;
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.FlowableOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;

import static com.wave.fileuploadservice.receiver.RetryJobReceiver.ACTION_CLEAR;
import static com.wave.fileuploadservice.receiver.RetryJobReceiver.ACTION_RETRY;

public class FileUploadService extends JobIntentService {
  private static final String TAG = "FileUploadService";
  RestApiService apiService;
  Disposable mDisposable;
  public static final int NOTIFICATION_ID = 1;
  public static final int NOTIFICATION_RETRY_ID = 2;
  /**
   * Unique job ID for this service.
   */
  private static final int JOB_ID = 102;
  String mFilePath;

  NotificationHelper mNotificationHelper;

  public static void enqueueWork(Context context, Intent intent) {
    enqueueWork(context, FileUploadService.class, JOB_ID, intent);
  }

  @Override
  public void onCreate() {
    super.onCreate();
    mNotificationHelper = new NotificationHelper(this);
  }

  @Override
  protected void onHandleWork(@NonNull Intent intent) {
    Log.d(TAG, "onHandleWork: ");
    /**
     * Download/Upload of file
     * The system or framework is already holding a wake lock for us at this point
     */

    // get file file here
    mFilePath = intent.getStringExtra("mFilePath");
    if (mFilePath == null) {
      Log.e(TAG, "onHandleWork: Invalid file URI");
      return;
    }
    apiService = RetrofitInstance.getApiService();
    Flowable<Double> fileObservable = Flowable.create(new FlowableOnSubscribe<Double>() {
      @Override
      public void subscribe(FlowableEmitter<Double> emitter) throws Exception {
        apiService.onFileUpload(
            FileUploadService.this.createRequestBodyFromText("info@androidwave.com"),
            FileUploadService.this.createMultipartBody(mFilePath, emitter)).blockingGet();
        emitter.onComplete();
      }
    }, BackpressureStrategy.LATEST);

    mDisposable = fileObservable.subscribeOn(Schedulers.computation())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Consumer<Double>() {
          @Override
          public void accept(Double progress) throws Exception {
            // call onProgress()
            FileUploadService.this.onProgress(progress);
          }
        }, new Consumer<Throwable>() {
          @Override
          public void accept(Throwable throwable) throws Exception {
            // call onErrors() if error occurred during file upload
            FileUploadService.this.onErrors(throwable);
          }
        }, new Action() {
          @Override
          public void run() throws Exception {
            // call onSuccess() while file upload successful
            FileUploadService.this.onSuccess();
          }
        });
  }

  private void onErrors(Throwable throwable) {
    /**
     * Error occurred in file uploading
     */
    Intent successIntent = new Intent("com.wave.ACTION_CLEAR_NOTIFICATION");
    successIntent.putExtra("notificationId", NOTIFICATION_ID);
    sendBroadcast(successIntent);

    PendingIntent resultPendingIntent = PendingIntent.getActivity(this,
        0 /* Request code */, new Intent(this, MainActivity.class),
        PendingIntent.FLAG_UPDATE_CURRENT);

    /**
     * Add retry action button in notification
     */
    Intent retryIntent = new Intent(this, RetryJobReceiver.class);
    retryIntent.putExtra("notificationId", NOTIFICATION_RETRY_ID);
    retryIntent.putExtra("mFilePath", mFilePath);
    retryIntent.setAction(ACTION_RETRY);

    /**
     * Add clear action button in notification
     */
    Intent clearIntent = new Intent(this, RetryJobReceiver.class);
    clearIntent.putExtra("notificationId", NOTIFICATION_RETRY_ID);
    clearIntent.putExtra("mFilePath", mFilePath);
    clearIntent.setAction(ACTION_CLEAR);

    PendingIntent retryPendingIntent = PendingIntent.getBroadcast(this, 0, retryIntent, 0);
    PendingIntent clearPendingIntent = PendingIntent.getBroadcast(this, 0, clearIntent, 0);
    NotificationCompat.Builder mBuilder =
        mNotificationHelper.getNotification(getString(R.string.error_upload_failed),
            getString(R.string.message_upload_failed), resultPendingIntent);
    // attached Retry action in notification
    mBuilder.addAction(android.R.drawable.ic_menu_revert, getString(R.string.btn_retry_not),
        retryPendingIntent);
    // attached Cancel action in notification
    mBuilder.addAction(android.R.drawable.ic_menu_revert, getString(R.string.btn_cancel_not),
        clearPendingIntent);
    // Notify notification
    mNotificationHelper.notify(NOTIFICATION_RETRY_ID, mBuilder);
  }

  /**
   * Send Broadcast to FileProgressReceiver with progress
   *
   * @param progress file uploading progress
   */
  private void onProgress(Double progress) {
    Intent progressIntent = new Intent(this, FileProgressReceiver.class);
    progressIntent.setAction("com.wave.ACTION_PROGRESS_NOTIFICATION");
    progressIntent.putExtra("notificationId", NOTIFICATION_ID);
    progressIntent.putExtra("progress", (int) (100 * progress));
    sendBroadcast(progressIntent);
  }

  /**
   * Send Broadcast to FileProgressReceiver while file upload successful
   */
  private void onSuccess() {
    Intent successIntent = new Intent(this, FileProgressReceiver.class);
    successIntent.setAction("com.wave.ACTION_UPLOADED");
    successIntent.putExtra("notificationId", NOTIFICATION_ID);
    successIntent.putExtra("progress", 100);
    sendBroadcast(successIntent);
  }

  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,
      final FlowableEmitter<Double> emitter) {
    RequestBody requestBody = createRequestBodyFromFile(file, mimeType);
    return new CountingRequestBody(requestBody, new CountingRequestBody.Listener() {
      @Override
      public void onRequestProgress(long bytesWritten, long contentLength) {
        double progress = (1.0 * bytesWritten) / contentLength;
        emitter.onNext(progress);
      }
    });
  }
}
6. Now Create a BroadcastReceiver for listening file upload progress

Create a subclass of BroadcastReceiver named is FileProgressReceiver and override onReceive() methods. As per name suggesting. we receive file upload progress here and update the progress bar notification. Let’s define below action and manages actions accordingly.

package com.wave.fileuploadservice.receiver;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import com.wave.fileuploadservice.MainActivity;
import com.wave.fileuploadservice.NotificationHelper;
import com.wave.fileuploadservice.R;
import java.util.Objects;

public class FileProgressReceiver extends BroadcastReceiver {
  private static final String TAG = "FileProgressReceiver";
  public static final String ACTION_CLEAR_NOTIFICATION = "com.wave.ACTION_CLEAR_NOTIFICATION";
  public static final String ACTION_PROGRESS_NOTIFICATION = "com.wave.ACTION_PROGRESS_NOTIFICATION";
  public static final String ACTION_UPLOADED = "com.wave.ACTION_UPLOADED";

  NotificationHelper mNotificationHelper;
  public static final int NOTIFICATION_ID = 1;
  NotificationCompat.Builder notification;

  @Override
  public void onReceive(Context mContext, Intent intent) {
    mNotificationHelper = new NotificationHelper(mContext);

    // Get notification id
    int notificationId = intent.getIntExtra("notificationId", 1);
    // Receive progress
    int progress = intent.getIntExtra("progress", 0);

    switch (Objects.requireNonNull(intent.getAction())) {
      case ACTION_PROGRESS_NOTIFICATION:
        notification = mNotificationHelper.getNotification(mContext.getString(R.string.uploading),
            mContext.getString(R.string.in_progress), progress);
        mNotificationHelper.notify(NOTIFICATION_ID, notification);
        break;
      case ACTION_CLEAR_NOTIFICATION:
        mNotificationHelper.cancelNotification(notificationId);
        break;
      case ACTION_UPLOADED:
        Intent resultIntent = new Intent(mContext, MainActivity.class);
        PendingIntent resultPendingIntent = PendingIntent.getActivity(mContext,
            0 /* Request code */, resultIntent,
            PendingIntent.FLAG_UPDATE_CURRENT);
        notification =
            mNotificationHelper.getNotification(mContext.getString(R.string.message_upload_success),
                mContext.getString(R.string.file_upload_successful), resultPendingIntent);
        mNotificationHelper.notify(NOTIFICATION_ID, notification);
        break;
      default:
        break;
    }
  }
}
7. Create RetryJobReceiver

Create a new class that extends BroadcastReceiver named is RetryJobReceiver. FileUploadService will send a broadcast to RetryJobReceiver in case of an error in during file uploading. Such as network failure, internal server error, etc.

package com.wave.fileuploadservice.receiver;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

import com.wave.fileuploadservice.FileUploadService;
import com.wave.fileuploadservice.NotificationHelper;

import java.util.Objects;

public class RetryJobReceiver extends BroadcastReceiver {

    public static final String ACTION_RETRY = "com.wave.ACTION_RETRY";
    public static final String ACTION_CLEAR = "com.wave.ACTION_CLEAR";
    NotificationHelper mNotificationHelper;

    @Override
    public void onReceive(Context context, Intent intent) {
        /**
         * Handle notification user actions
         */
        mNotificationHelper = new NotificationHelper(context);
        int notificationId = intent.getIntExtra("notificationId", 0);
        String filePath = intent.getStringExtra("mFilePath");
        switch (Objects.requireNonNull(intent.getAction())) {
            case ACTION_RETRY:
                mNotificationHelper.cancelNotification(notificationId);
                Intent mIntent = new Intent(context, FileUploadService.class);
                mIntent.putExtra("mFilePath", filePath);
                FileUploadService.enque