Tag

job-intent-service

Browsing

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 is in . In Oreo Android introduced JobIntentService for performing background. It is the modern way of using the background service to perform background task(that run in both cases while is foreground or background). I have written also a separate article on Working with JobIntentService also. Now we will use JobIntentService for file uploading.

Step for implementation

  1. Create a new class and extends JobIntentService.
  2. Now override the onHandleWork() methods.
  3. Setup Retrofit instance and Expose API file upload API
  4. Place file upload logic in onHandleWork()
  5. Create a local BroadCastReceiver() that update the result of service in activity
  6. Now expose enqueueWork() to start this service
  7. Open MainActivity and starts the JobIntentService
  8. We have to pass file path with Intent so write a below code for getting from camera and intent.
  9. 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 add the dependency

    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 ()
  • 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 🙂

Download Sample Project- Upload Files to Server using Service

Introduction

If you are looking at what exactly limitations are applied in background service in Android in Oreo. You are in the right place. In the previous article, I have explained, What is Service and Types of Service. In this article, I’m gonna explain, what exactly limitations are applied in background service in Android in Oreo.

Understand Background and Foreground service

Before moving forward lets clear some terminology, What is background service what is foreground service? The app said to be in the foreground when the app is visible, that is its activity is visible and the user is interacting with the activity. Such as we open the Facebook application so the Facebook application comes in the foreground now some other criteria is when the application has a service running in the foreground.

So if either of the above-mentioned criteria fails then the application is said to be in the background. Let me explain it with an example, suppose in your device, no one app is recently open mean recent app stack is clear now. Now your friend sends you WhatsApp message. One notification will come in WhatsApp mean, WhatsApp application is acting as a background application that is it has not opened it and it has no visibility now. Now launch this WhatsApp application then you get landing right so when the application becomes visible then the WhatsApp application is said to be in the foreground state. In simple words, we can say that when the application is not visible it is in background and also when the application is visible it is said to be in the foreground

Limitation on Background Service

Starting from Android Oreo( API 26) and onwards the background applications(when an application is not foreground ) cannot use the started service. When you call startService() method from the background applications simply through the IllegalStateException. In other words, you can say if you call startService() when your application is not in the foreground.

Why so limitation?  Just because running a service in the background consumes a lot of memory. this simply impacts the device performance thus resulting in that make the quick battery drain and degrade the User experience.

The following service you can run in Oreo (API 26)

  • Bounded Service
  • Foreground Service
  • When the application is currently in the foreground

In case you call startService() method while app is foreground after that application moves to the background, after small amount of time service also shutdown

For better clarity, We will take an example of IntentService and apply below use cases.

Use Case

Now I will create an IntentService, will run it in the foreground as well as background and run also Pre Oreo and Oreo device also.  

Create a new application

Let’s create a new application with Empty Activity template.

Create a subclass of IntentService and add block of code
package com.wave.backgroundserviceexample;

import android.app.IntentService;
import android.content.Intent;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.util.Log;
import android.widget.Toast;

public class CounterService extends IntentService {
    final Handler mHandler = new Handler();
    private static final String TAG = "CounterService";

    /**
     * provide name of worker thread
     */
    public CounterService() {
        super(TAG);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        showToast("Job Execution Started");
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        int maxCount = intent.getIntExtra("maxCountValue", -1);
        /**
         * Suppose we are performing task 1 to 1000, Each task will takes time 1 sec , So You saw we sleep thread or one second.
         */
        for (int i = 0; i < maxCount; i++) {
            Log.d(TAG, "onHandleWork: The number is: " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // Helper for showing tests
    void showToast(final CharSequence text) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(CounterService.this, text, Toast.LENGTH_SHORT).show();
            }
        });
    }
}
After that Open actvitiy_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">

    <Button
        android:id="@+id/button"
        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:background="@color/colorAccent"
        android:onClick="onStartIntentService"
        android:padding="16dp"
        android:text="Start IntentService"
        android:textAllCaps="false"
        android:textColor="#fff"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
Let’s move to MainActivity and add below code
package com.wave.backgroundserviceexample;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void onStartIntentService(View view) {
        Intent mIntent = new Intent(MainActivity.this, CounterService.class);
        mIntent.putExtra("maxCountValue", 1000);
        startService(mIntent);
    }
}

Now above code, we have created a JobIntentService In this service we are printing 1 to 1000 no in one-second interval. In MainActivity we expose only onClick methods. In this method, We are starting IntentService with the help of startService() method. Now run this project.

Use Case -1 App Foreground and API level smaller than equal to 25 (Pre Oreo)

Build the project and run in pre oreo devices, and click the Start IntentServce button. Button event will be triggered and service is started. You can verify it with Logcat. Output like below

D/CounterService: Job Execution Started
D/CounterService: onHandleWork: The number is: 0
D/CounterService: onHandleWork: The number is: 1
D/CounterService: onHandleWork: The number is: 2
D/CounterService: onHandleWork: The number is: 3
D/CounterService: onHandleWork: The number is: 4
...
UseCase -2 App Foreground and API level 26 or onward (Oreo or P)

Run the project in Oreo devices and click on Start IntentServce button. The result as same as UseCase-1.

UseCase -3 App Background and API level smaller than equal to 25 (Pre Oreo)

Now testing this use case we have to start the service when the app is background. For running an app in the background, We have to create a receiver with a BootComplete receiver. create a new class like below

package com.wave.backgroundserviceexample;

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

public class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context mContext, Intent intent) {
        if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
            Intent mIntent = new Intent(mContext, CounterService.class);
            mIntent.putExtra("maxCountValue", 1000);
            mContext.startService(mIntent);
        }
    }
}

Now register this recevier in manifest

<receiver android:name=".BootCompleteReceiver">
      <intent-filter>
           <action android:name="android.intent.action.BOOT_COMPLETED" />
      </intent-filter>
</receiver>

Run the project and restart the device. While the device will be booted, onReceive() will call and startService() method will execute. The output result will be expected. Mean IntentService will work perfectly in the pre Oreo device.

UseCase – 4 App Background and API level 26 or onward (Oreo or P).

In this use case run the same project in Oreo devices and above and restart the device. The application has crashed in the Oreo device. that means we cannot start the IntentService from the background application in the Oreo device.

The possible solution for dealing with Service in Oreo and onward

What is Service

When none of applications is active state than few Android components are being active in the background and that active component is actually the service. In other words, Service is an application component that executes task in the background without affecting user’s current activity. By default, Service always runs in the main UI thread, It does not runs in separate worker thread and also it is not run in a separate process.

Let’s take some example for better understand. Suppose a user is playing his game, This game is basically activity that shows the current interaction of the user and sometimes while playing the game in the background there are some operations that are going on.

For example while user is busy playing the game, there are few applications are getting updated from Play Store and we all have faced this situation where while interacting with any application in the background all of a sudden the application starts to get updated from the Google Play Store. While the other applications are being updated in the background that time operation is not affecting the current user interaction of playing the game. So all of these background operations actually carried out by the Android component known as a service

For better clarity let me show you one more example. Suppose you are using the Facebook application and while using the Facebook application there are few background operations that are going on such as uploading or downloading of a file from some other application or while using the Facebook application you want to listen to the music. So here again these two operations are actually being carried out with the help of the Android component known as Service.
Now whenever background operations going on then all the background operations consume some memory. So other words you can say, whenever we use any application or perform any task it definitely eats up your device RAM.

Type of Service

In my opinion the service is broadly categorised into two types the first one is the Started Service and the second one is the Bound Service. The started service can be further categorised into two subcategory the first one can be called as the Foreground Service and the second one is the Background Service .

Now if you have a look at the DEVELOPER.ANDROID that is the official documentation of service then you will find that there are three types of service such as the Foreground, Background, and Bound Service but right now I thought it is much better to categorize the service in 2 subcategories of Started Service and the Bound Service

1. Started Service

Started Services is started with the help of an Android component(Activity, BroadcastReceiver, ContentProvider and Service). That why it is known as a Started Service. The Started service can be started by the Activity, BroadcastReceiver, ContentProvider and the Service itself. Here is noticeable point is that a service can start another service as well as.

The Started Service is starts with the help of a method of startService() method. Whenever any Android component a Service the Service runs indefinitely in the background and even if the calling component is destroyed then also the started service will continue to run indefinitely in the background. Basically, I am trying to say is suppose the calling Android component is destroyed then also the started service continues to run and this might leads to memory leakage. The started service performs a single operation at a time and there is always a one way communication.

In case of started service that is we can pass the data from the Android component to the service but by default there is no mechanism to get back result from the service back to the calling component so we have to find a workaround to get back result and as said a service continues to run indefinitely in the background that why we should always call stopSelf() method or stopService() method to stop the service and avoid any memory leakage.

Let’s summarise Started Service in short
  • It started by Android components, these components are Activity, ContentProvider, BroadcastReceiver and Service itself.
  • It stared with help of startService() methods and stop by stopService() or stopSelf() methods.
  • When service once started its run in the background indefinitely even when the stared component is also destroyed.
  • By default it has one-way communication only.
  • We should stop the service after the task is finished by using stopService() or stopSelf() methods

2. Bound Service

The service is called a bound service when Android component simply to the particular service. We can bind service with three component. The components are Activity, ContentProvider and the Service itself. BroadcastReceiver can never bind the service. and binding to the service is achieved with the help of a special method of bindService() methods. A service as long as there is at least one component bound to it.

When this component is destroyed then the bound service is also destroyed simultaneously. So we can say that when all the calling components are destroyed then the service is also destroyed. In other words you can say, The Bound Service is dependent on the bounded component for its (own) existence. The bound service has a two way communication Unlike the started service where we had only one way communication so here in case of bound service we have the two way communication you can pass data from the component to the service and also by default get back result.

Summery of Bound Service
  • We can bind to the service with Activity, Content Provider and Service components with help of the bindService() method.
  • When all calling components are destroyed, the service also destroyed.
  • Bound Service continuously interacts with calling components.
  • By default is have two way communication.
1.1 What is Foreground Service

Suppose you are currently using the Gmail application and listening to the music that is being played by some other application. So music player is basically using the foreground service to play the music. The foreground service always uses the notification to notify the user that some long running operation is going on and using the notification you can actually interact with the service or the ongoing operation such as pause the music or play the next music.

So whenever in your application you see a notification that is performing some long running tasks that that service is basically the foreground service and the foreground service is always noticeable to the user that is the user is aware of this ongoing process.

Foreground Service is actually created by using the startForeground() method. So when we use this method it actually the service to run in the foreground. And by using the () method the service again returns back to run in the background and now its becoming the background service now.

I point is noticeable here that using stopForeground() method foreground service is not destroyed. Basically, it actually makes a foreground service to background service. So how to destroy the foreground service so , that again you need to call the stopService() or stopSelf() the method to destroy the foreground service as well.

When a service runs in the foreground it should always be noticeable by the user to always use notification for the ongoing operation like we saw in case of music player application or downloading or uploading of the file or while updating the applications from the PlayStore we also have the notification to notify the user that some operation is going on.

1.2 What is background service

I will take example of the WhatsApp application and also the Gmail application by taking these two examples you can actually to correlated the things with the Facebook application as well.

Let take a example

Suppose you are connected to internet, Now you will simply turn off your Wi-Fi right. In case you have no internet on your phone. Means you are offline. Now let try to send some message to your friends WhatsApp account. So here you will just try to send a message like (Test message). After sending the message you are not connected to the internet that why this message is currently undelivered and it is showing a pending status close the application and let also destroy the WhatsApp application from the minimized application list. So right now I don’t have any running application and no recent items in the recent app.

Now just turn on Wi-Fi. Now device is connected with internet and message is automatically delivered without WhatsApp app open. In other words WhatsApp application is still closed you have not opened my WhatsApp application but still, my message was delivered to your friend’s device. how is that possible? This is simply of background service. The WhatsApp application was actually running the background service and due to that service, my message was delivered in the background itself so we don’t need to open the WhatsApp application for our message to get delivered so this is the beauty of using the background service.

Conclusion

A service is started it is actually background in nature so when we use in the startService() method it actually creates a background service by default. Now starting from Android Oreo that is API 26 onwards there are certain restrictions imposed on the background service.

I will discuss it in my upcoming articles so please don’t worry about it and next to the background service is not noticeable by the user as we saw in case of WhatsApp and Gmail how they use the background service to send data to some other user in the background.

Exit mobile version