-
-
Save blackcj/20efe2ac885c7297a676 to your computer and use it in GitHub Desktop.
/** | |
* | |
* 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> |
You should close the connection to Google Play Services on onDestroy()
mLocationClient.disconnect();
Perfect exactly what I was looking for :)
Is Internet connection required to use google playservices location api ?
Wow great stuff, thanks a lot!
I know this is a pretty old Gist so here are some updates to the code:
-
Firstly, you're going to want to use the
GoogleApiClient
rather than the 'LocationClient
'. So change that where needed.- You build the
GoogleApiClient
like this, using it is pretty much the same as the other class:
protected synchronized void buildGoogleApiClient() { Log.i(TAG, "Building GoogleApiClient"); this.mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(LocationServices.API) .build(); }
- You build the
-
Added some checks to
onStartCommand
to stop multiple WakeLocks from being retained as they were causing errors.
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 || mLocationClient.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;
}
- Change this line in
onConnect
:
Intent intent = new Intent(this, LocationReceiver.class);
PendingIntent pendingIntent = PendingIntent
.getBroadcast(this, 54321, intent, PendingIntent.FLAG_CANCEL_CURRENT);
mLocationClient.requestLocationUpdates(mLocationRequest, locationIntent); //** This line changes
- To:
Intent intent = new Intent(this, LocationReceiver.class);
PendingIntent pendingIntent = PendingIntent
.getBroadcast(this, 54321, intent, PendingIntent.FLAG_CANCEL_CURRENT);
LocationServices.FusedLocationApi.requestLocationUpdates(this.mGoogleApiClient,
mLocationRequest, pendingIntent); // This is the changed line.
- Here's my
onDestroy
:
@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();
}
- Here's the updated LocationReceiver BroadcastReceiver:
public class LocationReceiver extends BroadcastReceiver {
private String TAG = this.getClass().getSimpleName();
private LocationResult mLocationResult;
@Override
public void onReceive(Context context, Intent intent) {
// Need to check and grab the Intent's extras like so
if(LocationResult.hasResult(intent)) {
this.mLocationResult = LocationResult.extractResult(intent);
Log.i(TAG, "Location Received: " + this.mLocationResult.toString());
}
}
}
Also, @tspshikari: No the Internet permission is not required to use the GooglePlayServices Location API, that was probably just some other permission that they had for their app. :)
Great stuff! 👍
@JimVanG, one question: why do you need a wake lock when the pending intent method handles location updates in background for you? If you set the LocationReceiver to inherit from a WakefulBroadcastReceiver, I think you can spare yourself the wake lock. (I am a beginner to Android so just thinking out loud, would appreciate any thoughts on this one!) :-)
Thank you for the updates @JimVanG. I've already put your WakeLock code to good use in my current project. I will update this gist to include those changes along with an update for Android 6.0 permissions in the near future. It's great to see that this old gist is still getting used.
This is very helpful 👍
It took me a moment to realize that LocationReceiver
has to be registered in AndroidManifest.xml.
<receiver android:name=".LocationReceiver" />
Waiting for the new version, it's a great piece of code.
Just for note, I had to change the LocationReceiver to an inner static class, and also add it as well as the service to the Manifest:
<receiver android:name=".service.BackgroundLocationService$LocationReceiver" />
<service android:name=".service.BackgroundLocationService" android:exported="false" />
Awesome stuff... Just what I was looking for 👍 💯
hey @blackcj news on an updated version of the gist? :)
The code has been updated with changes provided by @JimVanG and the latest support library. If targeting Android 23+ you will need to update to support the permission model changes.
Sir, Can you please provide the complete source code please!?
I need to pass params to the receiver.
Intent intent = new Intent(this, LocationReceiver.class);
intent.putExtra(..)
In the LocationReceiver I do receive my Extra on the other hand LocationResult.hasResult always result false.
is there a work around for this?
@Paul776 I changed from PendingIntent.FLAG_CANCEL_CURRENT
to PendingIntent.FLAG_UPDATE_CURRENT
Edit: weird, this only works on a device with Android 5.1 not on a device with Android 4.3
Can someone help?
why is Wake Lock required since background updates are sent by OS to the BroadcastReceiver by the PendingIntent?
Also I'm trying this code on Android 5 as well and when I kill the application in the task manager the server is killed and never comes back
If I remove the wake lock gibberish, in THAT case the service is restart once killed. Can someone explain what is this wakeup used for ?
@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.
@blackcj where is the Main Activity Sir
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
@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)
Hi, I was able to resolve my issue.... I did the following:
- use WakefulBroadcastReceiver and hence did not require wakelocks in background service.
- Created a new intentService to receive location updates from LocationReceiver and do all relevant processing.
- Updated location receiver to send the location details to intentService.
Thanks for your help.
Can't this all just be done using a PendingIntent?
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.
Hi newbie here. How does the service communicate back to the calling activity with the completed work?
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:
- on the BackgroundLocationService make it implement LocationListener which will make you override "onLocationChanged(Location location)" function
- from that function "onLocationChanged" you can send a broadcast like this: "sendBroadcast(Intent(ACTION_SEND_UI_UPDATE))"
- 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
Use this
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
instead of this GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
Hello there, how come when i implemented this, background service when i use stopService(i) // i = intent
@ritesh2014
You need to add receivers to your manifest for GPS on/off (similar to the one added for BOOT_COMPLETED above). Look at S.O. question/answer here. Have not tried it but makes sense. Could also add similar for airplane mode and whatever else.
Incidentally, I wanted service to run continuously, even when phone (screen) off. Made following mods to
BackgroundLocationService.java
with wakelock:and this:
Not sure how well this performs in terms of battery life yet - seems to be going okay on my GNex - but seems to keep locations commingt a bit smoother when screen off. Need to add
android.permission.WAKE_LOCK
permission to manifest.