5

I'm using jobScheduler to get background location updates. But each time the job is scheduled, FusedLocationProviderClient is null. Why is that? I've checked if(FusedLocationProviderClient == null) condition and each time the job is scheduled, the code under it runs (that means fusedLocationProviderClient is null after it is initialized.) Please have a look at the code below. Moreover locationAvailability is often false, hence onLocationResult is not called that gives null location value. How can I optimize FusedLocationProviderClient. One more thing, are fusedLocationProviderClient always being null and locationAvailability giving false related?

@Override
public boolean onStartJob(JobParameters jobParameters) {
    Log.e("onStartJob", "onStartJob");//for debug
    jobP = jobParameters;

    if (!checkAndRequestPermissions()) {
        Toast.makeText(this, "Please provide location permission for paramount app.", Toast.LENGTH_LONG).show();
        provider = null;
    } else {
        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) {
        }
        if (mLocationRequest == null) {
            Log.e("onStartJob", "LocationRequest initialized"); //for debug
            mLocationRequest = LocationRequest.create();
            mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
            mLocationRequest.setInterval(100 * 1000);
            mLocationRequest.setFastestInterval(60 * 1000);
        }

        if (client == null) {
            Log.e("onStartJob", "client initialized"); //for debug
            client = LocationServices.getFusedLocationProviderClient(this);
            client.requestLocationUpdates(mLocationRequest, new LocationCallback() {
                @Override
                public void onLocationResult(LocationResult locationResult) {
                    Log.e("onLocationResult ", "onLocationResult");
                    onLocationChanged(locationResult.getLastLocation());
                }
                @Override
                public void onLocationAvailability(LocationAvailability locationAvailability) {
                    Log.e("onLocationAvailability", locationAvailability + "");;
                }
            },
            Looper.myLooper());
        }

        try {
            provider = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE) + "";
            gpsProvider = provider;
        } catch (Settings.SettingNotFoundException e) {
            Log.e("provider", "gps provider error");
        }
    }
    return true;
}

@Override
public boolean onStopJob(JobParameters jobParameters) {
    Log.e("onStopJob", "onStopJob");//for debug
    if (ul != null) {
        ul.cancel(true);
    }
    return true;
}

public void onLocationChanged(Location location) {
    latitude = location.getLatitude() + "";
    longitude = location.getLongitude() + "";
    Log.e("latitude" , latitude);
}

The log values in the above code is shown below:

03-15 17:09:25.889 10687-10687/com.myProject.com.jobschedulers E/onStartJob: onStartJob
03-15 17:09:25.900 10687-10687/com.myProject.com.jobschedulers E/onstartJob: client initialized
03-15 17:09:25.957 10687-10687/com.myProject.com.jobschedulers E/onLocationResult: onLocationResult
03-15 17:09:25.960 10687-10687/com.myProject.com.jobschedulers E/onLocationAvailability: LocationAvailability[isLocationAvailable: true]
03-15 17:23:26.975 10687-10687/com.myProject.com.jobschedulers E/onStartJob: onStartJob
03-15 17:23:26.993 10687-10687/com.myProject.com.jobschedulers E/onstartJob: client initialized
03-15 17:23:27.017 10687-10687/com.myProject.com.jobschedulers E/onLocationAvailability: LocationAvailability[isLocationAvailable: false]
03-15 17:41:32.672 10687-10687/com.myProject.com.jobschedulers E/onStartJob: onStartJob
03-15 17:41:32.690 10687-10687/com.myProject.com.jobschedulers E/onstartJob: client initialized
03-15 17:41:32.741 10687-10687/com.myProject.com.jobschedulers E/onLocationAvailability: LocationAvailability[isLocationAvailable: false]
03-15 17:53:17.335 10687-10687/com.myProject.com.jobschedulers E/onStartJob: onStartJob
03-15 17:53:17.351 10687-10687/com.myProject.com.jobschedulers E/onstartJob: client initialized
03-15 17:53:17.383 10687-10687/com.myProject.com.jobschedulers E/onLocationAvailability: LocationAvailability[isLocationAvailable: false]
beck
  • 2,967
  • 3
  • 16
  • 27

2 Answers2

0

JobScheduler is only from 21 API, consider using JobIntentService launched from BroadcastReceiver from PendingIntent and start it all from FusedLocationProviderClient.requestLocationUpdates method. It will look like this:

class MainActivity : Activity() {
    ...
    lateinit var  mFusedLocationClient:FusedLocationProviderClient
    lateinit var mLocationRequestBackground: LocationRequest
    fun init {
        mFusedLocationClient = LocationServices.getFusedLocationProviderClient(activity = this)
        mLocationRequestBackground = LocationRequest()
        mLocationRequestBackground.interval = UPDATE_INTERVAL_IN_MILLISECONDS_BACKGROUND // i.e. 15 minutes - you hope to get updates at this interval, in background it will be not more than few times per hour on Android Oreo
        mLocationRequestBackground.fastestInterval = FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS_BACKGROUND //i.e. 1 minute - updates will not come faster than this
        mLocationRequestBackground.priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY //do not overwhelm user's device with requests, but use requests from other apps
        mLocationRequestBackground.smallestDisplacement = 25f //distance in meters to trigger update
    }
    ...
    fun onStop() {
        //start updating in background
        mFusedLocationClient.requestLocationUpdates(mLocationRequestBackground,
                    LocationBroadcastReceiver.getPendingIntent(activity = this))
    }
    ...
}

class LocationBroadcastReceiver : BroadcastReceiver() {
    companion object {
        const val EXTRA_LATITUDE = "EXTRA_LATITUDE"
        const val EXTRA_LONGITUDE = "EXTRA_LONGITUDE"
        const val WAKEUP = "com.appid.intent.action.WAKEUP"
        fun getPendingIntent(context: Context, userId: String? = null): PendingIntent {
            val alarmIntent = Intent(context, LocationBroadcastReceiver::class.java)
            alarmIntent.action = WAKEUP
            return PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT)
        }
    }

    override fun onReceive(context: Context?, intent: Intent?) {
        context ?: return
        if (intent?.action != WAKEUP) return

        val location: LocationResult? = LocationResult.extractResult(intent)
        val loc: Location? = location?.lastLocation
        loc?.apply {
                val i = Intent(context, SendLocationService::class.java)
                i.putExtra(EXTRA_LATITUDE, latitude)
                i.putExtra(EXTRA_LONGITUDE, longitude)
                SendLocationService.enqueueWork(context, i)
        }
    }
}

class SendLocationService : JobIntentService() {
    companion object {
        private const val JOB_ID = 1000
        const val TAG = "SendLocationService"
        fun enqueueWork(context: Context, work: Intent) {
            enqueueWork(context, SendLocationService::class.java, JOB_ID, work)
        }
    }

    private var sending: Boolean = false
    override fun onHandleWork(intent: Intent) = runBlocking {
        val latitude = intent.getDoubleExtra(LocationBroadcastReceiver.EXTRA_LATITUDE, 0.0)
        val longitude = intent.getDoubleExtra(LocationBroadcastReceiver.EXTRA_LONGITUDE, 0.0)
        suspendCoroutine<Unit> { continuation ->
            YourApi.postMyLocation(lat = latitude, lng = longitude)
                    .callback { continuation.resume(Unit) }
        }
    }

    override fun onStopCurrentWork(): Boolean {
        return !sending
    }
}

don't forget to add them to manifest

    <service android:name=".services.SendLocationService"
        android:exported="false"
        android:permission="android.permission.BIND_JOB_SERVICE"/>
    <receiver android:name=".services.LocationBroadcastReceiver"
        android:exported="false"/>
Wackaloon
  • 2,285
  • 1
  • 16
  • 33
0

I went through the same issue.

The first mistake I made was not to guarantee that removeLocationUpdates would be run on the same thread as the requestLocationUpdates. Actually, it doesn't have to be the same thread, but after a requestLocationUpdates, you must call removeLocationUpdates to make the next requestLocationUpdates valid. To ensure this, it is much easier to work on the same thread.

For example:

  private fun FusedLocationProviderClient.requestLocation(
      request: LocationRequest
  ): Single<LocationResult> {
    return Single.create<LocationResult> { emitter ->
      requestLocationUpdates(request, object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
          removeLocationUpdates(object : LocationCallback() {})
              .addOnCompleteListener {
                if (emitter.isDisposed) {
                  info("onLocationResult called after disposing.")
                  return@addOnCompleteListener
                }

                if (result != null && result.locations.isNotEmpty()) {
                  onSuccess(result)
                } else {
                  onError(RuntimeException("Invalid location result"))
                }
              }
        }

        private fun onError(error: Exception) {
          if (!emitter.isDisposed) {
            emitter.onError(error)
          }
        }

        private fun onSuccess(item: LocationResult) {
          if (!emitter.isDisposed) {
            emitter.onSuccess(item)
          }
        }

      }, Looper.getMainLooper())
    }
  }

As the code suggests, I have attracted Single's emitter to the addOnCompleteListener in removeLocationUpdates to ensure the call of removeLocationUpdates behind the requestLocationUpdates. Without RxJava, of course, it would be easier to implement.

The second mistake I made was the wrong interval setting in LocationRequest. According to the doc:

This method sets the rate in milliseconds at which your app prefers to receive location updates. Note that the location updates may be somewhat faster or slower than this rate to optimize for battery usage, or there may be no updates at all (if the device has no connectivity, for example).

interval triggers the location update event for the Android system. If there is no location update when requestLocationUpdates is called, the onLocationResult callback will never be called until a new location update occurs.

The third mistake I made was to set the wrong priority in LocationRequest. In API 10 and below, it was not PRIORITY_BALANCED_POWER_ACCURACY but it was resolved by using PRIORITY_HIGH_ACCURACY. In this case, I have only tested on the emulator, so the actual device may have different results. I guess PRIORITY_BALANCED_POWER_ACCURACY doesn't seem to work properly because the emulator doesn't provide Bluetooth hardware.

So my LocationRequest looks like:

LocationRequest.apply {
  priority = PRIORITY_HIGH_ACCURACY
  interval = 10000L
}

I hope the three mistakes and solutions I made is helpful to you!

poqw
  • 63
  • 2
  • 6
  • poqw, could you please elaborate on the following snippet from your post above? > The explanation is unkind but ultimately, if you call requestLocationUpdates once, you must have a Location update event triggered by interval before the next requestLocationUpdates. Finding this bug was the hardest. – N0000B Sep 10 '19 at 17:20
  • @N0000B Sorry for my poor explanation. As I mentioned on my answer, `interval` triggers the location update event for the Android system. If there is no location update when `requestLocationUpdates` is called, the `onLocationResult` callback will never be called until a new location update occurs. – poqw Sep 11 '19 at 02:32