-
-
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> |
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
Sir, Can you please provide the complete source code please!?