1

I have read a lot of other similar questions, and have checked out the Android documentation on Background Execution Limits (https://developer.android.com/about/versions/oreo/background#services), and still could not find a solution. I have tried most of the phone's settings as well.

The issue: I have an app, that is used to collect location data in the background. I am using a Service to achieve this. At the moment, it collects location data every 5 seconds. It works perfectly fine on my Nexus 5 (API 23); however, it only works on my Nexus 5X (API 27), when the application is in the foreground. As soon as it goes in the background it stops. So this has nothing to do with long running tasks in the background, as soon as I navigate away from the app, the service stops immediately.

Here is my service class code:

import android.app.Service;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.util.Log;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;

import app.khash.climbcollector.DataBase.DataContract.DataEntry;

public class GPSService extends Service implements
        GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener,
        com.google.android.gms.location.LocationListener {

private LocationRequest mLocationRequest;
private GoogleApiClient mGoogleApiClient;
private static final String TAG = GPSService.class.getSimpleName();

@Override
public void onCreate() {
    super.onCreate();
    buildGoogleApiClient();
}//onCreate


@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if (!mGoogleApiClient.isConnected())
        mGoogleApiClient.connect();
    return START_STICKY;
}//onStartCommand


@Override
public void onConnected(Bundle bundle) {
    startLocationUpdate();
}//onConnected

@Override
public void onConnectionSuspended(int i) {
    Log.i(TAG, "onConnectionSuspended " + i);

}//onConnectionSuspended

@Override
public void onLocationChanged(Location location) {

    //insert location to the database passing in the location object
    insertDataToDb(location);
}//onLocationChanged

//method for adding the location data to db
private void insertDataToDb(Location location) {

    final DateFormat dateFormat = new SimpleDateFormat("MM.dd.yyyy 'at' HH:mm:ss z");

    //Current date and time using the format declared at the beginning
    final String currentDateTime = dateFormat.format(Calendar.getInstance().getTime());

    double lat = location.getLatitude();
    double lng = location.getLongitude();
    double alt = location.getAltitude();

    SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
    String key = getString(R.string.route_name_intent_extra);
    String routeName = sharedPref.getString(key, "default");

    // Create a new map of values,
    ContentValues values = new ContentValues();
    values.put(DataEntry.COLUMN_DATA_LATITUDE, lat);
    values.put(DataEntry.COLUMN_DATA_LONGITUDE, lng);
    values.put(DataEntry.COLUMN_DATA_ALTITUDE, alt);
    values.put(DataEntry.COLUMN_DATA_DATE, currentDateTime);
    values.put(DataEntry.COLUMN_DATA_ROUTE_NAME, routeName);


    // Insert a new location into the provider, returning the content URI for the new location.
    Uri newUri = getContentResolver().insert(DataEntry.CONTENT_URI, values);

    // Show a toast message depending on whether or not the insertion was successful
    if (newUri == null) {
        // If the new content URI is null, then there was an error with insertion.
        Log.v(TAG, "error saving data");
    } else {
        //since the insert method return the Uri of the row created, we can extract the ID of
        //the new row using the parseID method with our newUri as an input. This method gets the
        //last segment of the Uri, which is our new ID in this case and we store it in an object
        // And add it to the confirmation method.
        String id = String.valueOf(ContentUris.parseId(newUri));
        // Otherwise, the insertion was successful and we can log
        Log.v(TAG, "Successfully added: " + id);
    }

}//insertDataToDb

@Nullable
@Override
public IBinder onBind(Intent intent) {
    return null;
}

@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
    Log.i(TAG, "onConnectionFailed ");

}//onConnectionFailed

private void initLocationRequest() {
    mLocationRequest = new LocationRequest();
    mLocationRequest.setInterval(5000);
    mLocationRequest.setFastestInterval(2000);
    mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

}//initLocationRequest

private void startLocationUpdate() {
    initLocationRequest();

    //check for location permission
    if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION)
            != PackageManager.PERMISSION_GRANTED &&
            ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION)
                    != PackageManager.PERMISSION_GRANTED) {

        return;
    }//check permission

    LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, this);
}//startLocationUpdate

private void stopLocationUpdate() {
    LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
}

protected synchronized void buildGoogleApiClient() {
    mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addOnConnectionFailedListener(this)
            .addConnectionCallbacks(this)
            .addApi(LocationServices.API)
            .build();
}//buildGoogleApiClient

@Override
public void onDestroy() {
    super.onDestroy();

    mGoogleApiClient.disconnect();
}//onDestroy


}//GPSService

I call the service from my main activity using this:

Intent serviceStartIntent = new Intent(this, GPSService.class);
startService(serviceStartIntent);

And stop it using this code:

Intent serviceStopIntent = new Intent(this, GPSService.class);
stopService(serviceStopIntent);

Here is the code I have tried using the JobScheduler.

public class GPSJobService extends JobService implements
        GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener,
        com.google.android.gms.location.LocationListener {

    private String TAG = GPSJobService.class.getSimpleName();
    private LocationRequest mLocationRequest;
    private GoogleApiClient mGoogleApiClient;

    private JobParameters mParam;


    @Override
    public void onCreate() {
        Log.v(TAG, "onCreate called");
        super.onCreate();
        buildGoogleApiClient();
    }//onCreate

    @Override
    public void onDestroy() {
        Log.v(TAG, "onDestroy called");
        super.onDestroy();
        mGoogleApiClient.disconnect();
    }//onDestroy

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.v(TAG, "onStartCommand called");
        if (!mGoogleApiClient.isConnected())
            mGoogleApiClient.connect();
        return START_STICKY;
    }//onStartCommand

    @Override
    public boolean onStartJob(JobParameters params) {
        Log.v(TAG, "onStartJob called");

        if (!mGoogleApiClient.isConnected())
            mGoogleApiClient.connect();

        mParam = params;

        return true;
    }//onStartJob

    @Override
    public void onLocationChanged(Location location) {
        //insert location to the database passing in the location object
        insertDataToDb(location);
    }//onLocationChanged

    private void initLocationRequest() {
        mLocationRequest = new LocationRequest();
        mLocationRequest.setInterval(5000);
        mLocationRequest.setFastestInterval(2000);
        mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

    }//initLocationRequest

    private void startLocationUpdate() {
        initLocationRequest();

        //check for location permission
        if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED &&
                ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION)
                        != PackageManager.PERMISSION_GRANTED) {
            return;
        }//check permission

        LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, this);
    }//startLocationUpdate

    //method for adding the location data to db
    private void insertDataToDb(Location location) {

        final DateFormat dateFormat = new SimpleDateFormat("MM.dd.yyyy 'at' HH:mm:ss z");

        //Current date and time using the format declared at the beginning
        final String currentDateTime = dateFormat.format(Calendar.getInstance().getTime());

        double lat = location.getLatitude();
        double lng = location.getLongitude();
        double alt = location.getAltitude();

        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
        String key = getString(R.string.route_name_intent_extra);
        String routeName = sharedPref.getString(key, "default");

        // Create a new map of values,
        ContentValues values = new ContentValues();
        values.put(DataEntry.COLUMN_DATA_LATITUDE, lat);
        values.put(DataEntry.COLUMN_DATA_LONGITUDE, lng);
        values.put(DataEntry.COLUMN_DATA_ALTITUDE, alt);
        values.put(DataEntry.COLUMN_DATA_DATE, currentDateTime);
        values.put(DataEntry.COLUMN_DATA_ROUTE_NAME, routeName);


        // Insert a new location into the provider, returning the content URI for the new location.
        Uri newUri = getContentResolver().insert(DataEntry.CONTENT_URI, values);

        // Show a toast message depending on whether or not the insertion was successful
        if (newUri == null) {
            // If the new content URI is null, then there was an error with insertion.
            Log.v(TAG, "error saving data");
        } else {
            //since the insert method return the Uri of the row created, we can extract the ID of
            //the new row using the parseID method with our newUri as an input. This method gets the
            //last segment of the Uri, which is our new ID in this case and we store it in an object
            // And add it to the confirmation method.
            String id = String.valueOf(ContentUris.parseId(newUri));
            // Otherwise, the insertion was successful and we can log
            Log.v(TAG, "Successfully added: " + id);
            //finish the job
            jobFinished(mParam, true);
        }

    }//insertDataToDb

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.v(TAG, "onStopJob called");
        return false;
    }//onStopJob

    @Override
    public void onConnected(@Nullable Bundle bundle) {
        startLocationUpdate();
    }//onConnected

    @Override
    public void onConnectionSuspended(int i) {
        Log.v(TAG, "onConnectionSuspended called");
    }//onConnectionSuspended

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        Log.v(TAG, "onConnectionFailed called");
    }//onConnectionFailed

    protected synchronized void buildGoogleApiClient() {
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addOnConnectionFailedListener(this)
                .addConnectionCallbacks(this)
                .addApi(LocationServices.API)
                .build();
    }//buildGoogleApiClient
}//GPSJobService

I am calling it in my main activity like this:

            case R.id.bttn_job_start:
                ComponentName serviceComponent = new ComponentName(this, GPSJobService.class);

                JobInfo.Builder builder = new JobInfo.Builder(mJobId, serviceComponent);

                builder.setMinimumLatency(5000);

                builder.setBackoffCriteria(5000, JobInfo.BACKOFF_POLICY_LINEAR);

                //repeat every 5 seconds
//                builder.setPeriodic(5000);

                JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);

                jobScheduler.schedule(builder.build());

                Toast.makeText(this, "Started", Toast.LENGTH_SHORT).show();


                break;

I have tried either setting latency and backoff criteria, or set as periodic (commented out right now). Both methods work while the app is in the foreground. Neither works when the app goes to the background.

As I mentioned earlier, it works perfect on my Nexus 5, but not on Nexus 5X.

Any suggestions on how I can fix this problem?

Thanks,

Khash
  • 151
  • 1
  • 12
  • Use [job scheduling](https://developer.android.com/topic/performance/scheduling) to perform repetetive tasks. However updating location every 5 sec seems a bit too aggressive imo. – Pawel Jun 04 '18 at 23:45
  • @Pawel: Thanks I will try that. I am familiar with the job scheduler, however, was hoping I can somehow fix this, instead of changing the whole system. The 5 second could be modified, but this is for mapping real time climbing/hiking data, so the accuracy and consistency of data is pretty important. This is just a helper app to collect the data for our database. – Khash Jun 05 '18 at 00:20
  • @Pawel: I have tried using JobScheduler, but I am having the same issue. It works fine when the app is in foreground, but then as soon as it goes in the background, the service/job is not working anymore – Khash Jun 05 '18 at 01:37

1 Answers1

0

It is behaving as expected. Starting from Android o, while an app is in the foreground, it can create and run both foreground and background services freely. When an app goes into the background, it has a window of several minutes (based on my observations its around 1 - 2 minutes) in which it is still allowed to create and use services. The system stops the app's background services, just as if the app had called the services' Service.stopSelf() methods.

If the task need to be run through completion immediately and reliably the best alternative to this would be to use ForegroundService. You can follow this SO which describes this approach.

If the Job needs to be executed at later point of time when certain constraints are met, consider using JobScheduler or WorkManager.

Sagar
  • 23,903
  • 4
  • 62
  • 62
  • Thanks for the response. One thing that is a bit strange for me is the fact that the system does not not give me even 1-2 minutes, the service is killed immediately as soon as as the app moves away from foreground. I have also tried running the service in the foreground, and it gave me the same issue. I have also tried using JobScheduler, having the same problem. Working while in the foreground, and then stopping as soon as it moves to background, or never even starting if the app is already in the background. I am adding my job Scheduler code to the question – Khash Jun 05 '18 at 20:06
  • @Khash there is no gurantee that it will run 1 to 2 minutes. It depends on OS. And you caanot start Background service if your app not in Foreground. – Sagar Jun 05 '18 at 23:02
  • @Khash I think you have completely changed the scope of your question. For your original question the answer provided would suffice. – Sagar Jun 06 '18 at 11:01
  • How did I change the scope of my question? I only added the JobScheduler code (in response to the answer). – Khash Jun 06 '18 at 16:24