Skip to content

Instantly share code, notes, and snippets.

@blackcj
Last active February 8, 2022 12:33
Show Gist options
  • Save blackcj/20efe2ac885c7297a676 to your computer and use it in GitHub Desktop.
Save blackcj/20efe2ac885c7297a676 to your computer and use it in GitHub Desktop.
Background location services.
/**
*
* BackgroundLocationService used for tracking user location in the background.
* @author cblack
*/
public class BackgroundLocationService extends Service implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
LocationListener {
IBinder mBinder = new LocalBinder();
private GoogleApiClient mGoogleApiClient;
private PowerManager.WakeLock mWakeLock;
private LocationRequest mLocationRequest;
// Flag that indicates if a request is underway.
private boolean mInProgress;
private Boolean servicesAvailable = false;
public class LocalBinder extends Binder {
public BackgroundLocationService getServerInstance() {
return BackgroundLocationService.this;
}
}
@Override
public void onCreate() {
super.onCreate();
mInProgress = false;
// Create the LocationRequest object
mLocationRequest = LocationRequest.create();
// Use high accuracy
mLocationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
// Set the update interval to 5 seconds
mLocationRequest.setInterval(Constants.UPDATE_INTERVAL);
// Set the fastest update interval to 1 second
mLocationRequest.setFastestInterval(Constants.FASTEST_INTERVAL);
servicesAvailable = servicesConnected();
/*
* Create a new location client, using the enclosing class to
* handle callbacks.
*/
setUpLocationClientIfNeeded();
}
/*
* Create a new location client, using the enclosing class to
* handle callbacks.
*/
protected synchronized void buildGoogleApiClient() {
this.mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(LocationServices.API)
.build();
}
private boolean servicesConnected() {
// Check that Google Play services is available
int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
// If Google Play services is available
if (ConnectionResult.SUCCESS == resultCode) {
return true;
} else {
return false;
}
}
public int onStartCommand (Intent intent, int flags, int startId)
{
super.onStartCommand(intent, flags, startId);
PowerManager mgr = (PowerManager)getSystemService(Context.POWER_SERVICE);
/*
WakeLock is reference counted so we don't want to create multiple WakeLocks. So do a check before initializing and acquiring.
This will fix the "java.lang.Exception: WakeLock finalized while still held: MyWakeLock" error that you may find.
*/
if (this.mWakeLock == null) { //**Added this
this.mWakeLock = mgr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyWakeLock");
}
if (!this.mWakeLock.isHeld()) { //**Added this
this.mWakeLock.acquire();
}
if(!servicesAvailable || mGoogleApiClient.isConnected() || mInProgress)
return START_STICKY;
setUpLocationClientIfNeeded();
if(!mGoogleApiClient.isConnected() || !mGoogleApiClient.isConnecting() && !mInProgress)
{
appendLog(DateFormat.getDateTimeInstance().format(new Date()) + ": Started", Constants.LOG_FILE);
mInProgress = true;
mGoogleApiClient.connect();
}
return START_STICKY;
}
private void setUpLocationClientIfNeeded()
{
if(mGoogleApiClient == null)
buildGoogleApiClient();
}
// Define the callback method that receives location updates
@Override
public void onLocationChanged(Location location) {
// Report to the UI that the location was updated
String msg = Double.toString(location.getLatitude()) + "," +
Double.toString(location.getLongitude());
Log.d("debug", msg);
// Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
appendLog(DateFormat.getDateTimeInstance().format(new Date()) + ":" + msg, Constants.LOCATION_FILE);
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public String getTime() {
SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return mDateFormat.format(new Date());
}
public void appendLog(String text, String filename)
{
File logFile = new File(filename);
if (!logFile.exists())
{
try
{
logFile.createNewFile();
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try
{
//BufferedWriter for performance, true to set append to file flag
BufferedWriter buf = new BufferedWriter(new FileWriter(logFile, true));
buf.append(text);
buf.newLine();
buf.close();
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void onDestroy() {
// Turn off the request flag
this.mInProgress = false;
if (this.servicesAvailable && this.mGoogleApiClient != null) {
this.mGoogleApiClient.unregisterConnectionCallbacks(this);
this.mGoogleApiClient.unregisterConnectionFailedListener(this);
this.mGoogleApiClient.disconnect();
// Destroy the current location client
this.mGoogleApiClient = null;
}
// Display the connection status
// Toast.makeText(this, DateFormat.getDateTimeInstance().format(new Date()) + ":
// Disconnected. Please re-connect.", Toast.LENGTH_SHORT).show();
if (this.mWakeLock != null) {
this.mWakeLock.release();
this.mWakeLock = null;
}
super.onDestroy();
}
/*
* Called by Location Services when the request to connect the
* client finishes successfully. At this point, you can
* request the current location or start periodic updates
*/
@Override
public void onConnected(Bundle bundle) {
// Request location updates using static settings
Intent intent = new Intent(this, LocationReceiver.class);
LocationServices.FusedLocationApi.requestLocationUpdates(this.mGoogleApiClient,
mLocationRequest, this); // This is the changed line.
appendLog(DateFormat.getDateTimeInstance().format(new Date()) + ": Connected", Constants.LOG_FILE);
}
/*
* Called by Location Services if the connection to the
* location client drops because of an error.
*/
@Override
public void onConnectionSuspended(int i) {
// Turn off the request flag
mInProgress = false;
// Destroy the current location client
mGoogleApiClient = null;
// Display the connection status
// Toast.makeText(this, DateFormat.getDateTimeInstance().format(new Date()) + ": Disconnected. Please re-connect.", Toast.LENGTH_SHORT).show();
appendLog(DateFormat.getDateTimeInstance().format(new Date()) + ": Disconnected", Constants.LOG_FILE);
}
/*
* Called by Location Services if the attempt to
* Location Services fails.
*/
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
mInProgress = false;
/*
* Google Play services can resolve some errors it detects.
* If the error has a resolution, try sending an Intent to
* start a Google Play services activity that can resolve
* error.
*/
if (connectionResult.hasResolution()) {
// If no resolution is available, display an error dialog
} else {
}
}
}
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "com.blackcj.locationtracker"
minSdkVersion 18
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:22.2.1'
compile 'com.google.android.gms:play-services-location:8.4.0'
}
public final class Constants {
// Milliseconds per second
private static final int MILLISECONDS_PER_SECOND = 1000;
// Update frequency in seconds
private static final int UPDATE_INTERVAL_IN_SECONDS = 60;
// Update frequency in milliseconds
public static final long UPDATE_INTERVAL = MILLISECONDS_PER_SECOND * UPDATE_INTERVAL_IN_SECONDS;
// The fastest update frequency, in seconds
private static final int FASTEST_INTERVAL_IN_SECONDS = 60;
// A fast frequency ceiling in milliseconds
public static final long FASTEST_INTERVAL = MILLISECONDS_PER_SECOND * FASTEST_INTERVAL_IN_SECONDS;
// Stores the lat / long pairs in a text file
public static final String LOCATION_FILE = "sdcard/location.txt";
// Stores the connect / disconnect data in a text file
public static final String LOG_FILE = "sdcard/log.txt";
public static final String RUNNING = "runningInBackground"; // Recording data in background
public static final String APP_PACKAGE_NAME = "com.blackcj.locationtracker";
/**
* Suppress default constructor for noninstantiability
*/
private Constants() {
throw new AssertionError();
}
}
public class LocationLoggerServiceManager extends BroadcastReceiver {
private SharedPreferences mPrefs;
public static final String TAG = "LLoggerServiceManager";
@Override
public void onReceive(Context context, Intent intent) {
// Make sure we are getting the right intent
if( "android.intent.action.BOOT_COMPLETED".equals(intent.getAction())) {
boolean mUpdatesRequested = false;
// Open the shared preferences
mPrefs = context.getSharedPreferences(
Constants.APP_PACKAGE_NAME, Context.MODE_PRIVATE);
/*
* Get any previous setting for location updates
* Gets "false" if an error occurs
*/
if (mPrefs.contains(Constants.RUNNING)) {
mUpdatesRequested = mPrefs.getBoolean(Constants.RUNNING, false);
}
if(mUpdatesRequested){
ComponentName comp = new ComponentName(context.getPackageName(), BackgroundLocationService.class.getName());
ComponentName service = context.startService(new Intent().setComponent(comp));
if (null == service){
// something really wrong here
Log.e(TAG, "Could not start service " + comp.toString());
}
}
} else {
Log.e(TAG, "Received unexpected intent " + intent.toString());
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.blackcj.locationtracker">
<!-- Used to start logging after re-boot -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".LocationLoggerServiceManager"
android:enabled="true"
android:exported="false"
android:label="LocationLoggerServiceManager">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service android:name=".BackgroundLocationService"
android:enabled="true"
android:exported="true" >
<intent-filter>
<action
android:name="com.google.android.gms.wearable.BIND_LISTENER" />
</intent-filter>
</service>
</application>
</manifest>
@blackcj
Copy link
Author

blackcj commented Aug 8, 2016

@Paul776 It really depends on your use case. This code is intended for "always on" location tracking which will impact battery. Another solution would be to set up an AlarmManager to wake at specific intervals. The WakeLock is used to ensure background code execution isn't interrupted when the device goes to sleep. In my testing, the WakeLock is keeping the service alive after reboot and when force closing the app.

@anamikagit
Copy link

anamikagit commented Aug 30, 2016

@blackcj where is the Main Activity Sir

@goyalsk
Copy link

goyalsk commented Aug 31, 2016

Hi,
I am a newbee... I am trying to replace my current logic to get location by pendingIntent method. During my search, I came across your forum and really appreciated the gist but somehow, onLocationChanged is not getting fired. I am struggling to figure the missing link. Can you please help.....
I have included the definitions for both in manifest also.
My code is saved @
https://gist.github.com/goyalsk/324827993f96cd08a63a520991b34acc

@Paul776
Copy link

Paul776 commented Sep 3, 2016

@blackcj
This doesn't seems to work properly. Yes the service stay alive when killing the app. But when the device goes to sleep. I don't receive the location anymore

Moreover setting mGoogleApiClient = null; in the onConnectionSuspened cause a NPE here
LocationServices.FusedLocationApi.requestLocationUpdates(this.mGoogleApiClient,
mLocationRequest, this); // This is the changed line.

When on onConnectionSuspened is called, Android will start to reconnect by himself. when it succeed, onConnected is called again and here you have your NPE

One more thing. When the service is killed by Android because he decided it (:)) onDestroy won't be called thus the wakeLock won't e release (though Im not sure this is a problem because the service is dead anyway)

@goyalsk
Copy link

goyalsk commented Sep 7, 2016

Hi, I was able to resolve my issue.... I did the following:

  1. use WakefulBroadcastReceiver and hence did not require wakelocks in background service.
  2. Created a new intentService to receive location updates from LocationReceiver and do all relevant processing.
  3. Updated location receiver to send the location details to intentService.

Thanks for your help.

@stagga85
Copy link

stagga85 commented Nov 9, 2016

Can't this all just be done using a PendingIntent?

@recaldev
Copy link

recaldev commented Jan 24, 2017

Hello, in the onConnectionFailed there is a comments that says:

        /*
         * Google Play services can resolve some errors it detects.
         * If the error has a resolution, try sending an Intent to
         * start a Google Play services activity that can resolve
         * error.
         */

The problem is that in the Service i cannot solve this, i have this code in an activity right now:

@Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        if (mResolvingError) { // Already attempting to resolve an error.
            return;
        } else if (connectionResult.hasResolution()) {
            try {
                mResolvingError = true;
                connectionResult.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
            } catch (IntentSender.SendIntentException e) { // There was an error with the resolution intent. Try again.
                mGoogleApiClient.connect();
            }
        } else { // Show dialog using GoogleApiAvailability.getErrorDialog()
            showErrorDialog(connectionResult.getErrorCode());
            mResolvingError = true;
        }
}

As you can see i need the onActivityResult method and other stuff from activity, if this callback is called in the service how can i launch an intent to solve this? If the GoogleApiClient object is on the service how can i can communicate that the problem was solved?

I'm having a bad time trying to implement this because this Service is exactly what i was needing and it works fine but what happen if the onConnectionFailed is called? I need to know the position of the client every few minutes.

@andrewclam1991
Copy link

Hi newbie here. How does the service communicate back to the calling activity with the completed work?

@pgomez-racowireless
Copy link

pgomez-racowireless commented Aug 1, 2017

Hi All,

I found this Gist very useful so I just wanted to thank you all for sharing.

also @andrewclam1991 I communicate back to the activity doing the following changes:

  1. on the BackgroundLocationService make it implement LocationListener which will make you override "onLocationChanged(Location location)" function
  2. from that function "onLocationChanged" you can send a broadcast like this: "sendBroadcast(Intent(ACTION_SEND_UI_UPDATE))"
  3. On your activity you can register for that event like this (code is in Kotlin but you get the idea):
    override fun onStart() { super.onStart() registerReceiver(mUpdateUIListener, BackgroundLocationService.intentFilter) } override fun onStop() { super.onStop() unregisterReceiver(mUpdateUIListener) } private val mUpdateUIListener = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { ACTION_SEND_UI_UPDATE -> { //Do some work here } } } }

Hope this is useful for someone

@FaizanMubasher
Copy link

Use this
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) instead of this GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);

@princearagones
Copy link

Hello there, how come when i implemented this, background service when i use stopService(i) // i = intent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment