/** * Copyright 2017 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.gms.location.sample.locationupdatesforegroundservice; import android.app.ActivityManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.location.Location; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; 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.LocationListener; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; /** * A bound and started service that is promoted to a foreground service when location updates have * been requested and all clients unbind. * * For apps running in the background on "O" devices, location is computed only once every 10 * minutes and delivered batched every 30 minutes. This restriction applies even to apps * targeting "N" or lower which are run on "O" devices. * * This sample show how to use a long-running service for location updates. When an activity is * bound to this service, frequent location updates are permitted. When the activity is removed * from the foreground, the service promotes itself to a foreground service, and location updates * continue. When the activity comes back to the foreground, the foreground service stops, and the * notification assocaited with that service is removed. */ public class LocationUpdatesService extends Service implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener { private static final String PACKAGE_NAME = "com.google.android.gms.location.sample.locationupdatesforegroundservice"; private static final String TAG = LocationUpdatesService.class.getSimpleName(); static final String ACTION_BROADCAST = PACKAGE_NAME + ".broadcast"; static final String EXTRA_LOCATION = PACKAGE_NAME + ".location"; private static final String EXTRA_STARTED_FROM_NOTIFICATION = PACKAGE_NAME + ".started_from_notification"; private final IBinder mBinder = new LocalBinder(); /** * The desired interval for location updates. Inexact. Updates may be more or less frequent. */ private static final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000; /** * The fastest rate for active location updates. Updates will never be more frequent * than this value. */ private static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = UPDATE_INTERVAL_IN_MILLISECONDS / 2; /** * The identifier for the notification displayed for the foreground service. */ private static final int NOTIFICATION_ID = 12345678; /** * Used to check whether the bound activity has really gone away and not unbound as part of an * orientation change. We create a foreground service notification only if the former takes * place. */ private boolean mChangingConfiguration = false; private NotificationManager mNotificationManager; /** * The entry point to Google Play Services. */ private GoogleApiClient mGoogleApiClient; /** * Contains parameters used by {@link com.google.android.gms.location.FusedLocationProviderApi}. */ private LocationRequest mLocationRequest; private Handler mServiceHandler; /** * The current location. */ private Location mLocation; public LocationUpdatesService() { } @Override public void onCreate() { mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(LocationServices.API) .build(); mGoogleApiClient.connect(); createLocationRequest(); HandlerThread handlerThread = new HandlerThread(TAG); handlerThread.start(); mServiceHandler = new Handler(handlerThread.getLooper()); mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "Service started"); boolean startedFromNotification = intent.getBooleanExtra(EXTRA_STARTED_FROM_NOTIFICATION, false); // We got here because the user decided to remove location updates from the notification. if (startedFromNotification) { removeLocationUpdates(); stopSelf(); } // Tells the system to not try to recreate the service after it has been killed. return START_NOT_STICKY; } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mChangingConfiguration = true; } @Override public IBinder onBind(Intent intent) { // Called when a client (MainActivity in case of this sample) comes to the foreground // and binds with this service. The service should cease to be a foreground service // when that happens. Log.i(TAG, "in onBind()"); stopForeground(true); mChangingConfiguration = false; return mBinder; } @Override public void onRebind(Intent intent) { // Called when a client (MainActivity in case of this sample) returns to the foreground // and binds once again with this service. The service should cease to be a foreground // service when that happens. Log.i(TAG, "in onRebind()"); stopForeground(true); mChangingConfiguration = false; super.onRebind(intent); } @Override public boolean onUnbind(Intent intent) { Log.i(TAG, "Last client unbound from service"); // Called when the last client (MainActivity in case of this sample) unbinds from this // service. If this method is called due to a configuration change in MainActivity, we // do nothing. Otherwise, we make this service a foreground service. if (!mChangingConfiguration && Utils.requestingLocationUpdates(this)) { Log.i(TAG, "Starting foreground service"); /* // TODO(developer). If targeting O, use the following code. if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) { mNotificationManager.startServiceInForeground(new Intent(this, LocationUpdatesService.class), NOTIFICATION_ID, getNotification()); } else { startForeground(NOTIFICATION_ID, getNotification()); } */ startForeground(NOTIFICATION_ID, getNotification()); } return true; // Ensures onRebind() is called when a client re-binds. } @Override public void onDestroy() { mServiceHandler.removeCallbacksAndMessages(null); mGoogleApiClient.disconnect(); } /** * Makes a request for location updates. Note that in this sample we merely log the * {@link SecurityException}. */ public void requestLocationUpdates() { Log.i(TAG, "Requesting location updates"); Utils.setRequestingLocationUpdates(this, true); startService(new Intent(getApplicationContext(), LocationUpdatesService.class)); try { LocationServices.FusedLocationApi.requestLocationUpdates( mGoogleApiClient, mLocationRequest, LocationUpdatesService.this); } catch (SecurityException unlikely) { Utils.setRequestingLocationUpdates(this, false); Log.e(TAG, "Lost location permission. Could not request updates. " + unlikely); } } /** * Removes location updates. Note that in this sample we merely log the * {@link SecurityException}. */ public void removeLocationUpdates() { Log.i(TAG, "Removing location updates"); try { LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, LocationUpdatesService.this); Utils.setRequestingLocationUpdates(this, false); stopSelf(); } catch (SecurityException unlikely) { Utils.setRequestingLocationUpdates(this, true); Log.e(TAG, "Lost location permission. Could not remove updates. " + unlikely); } } /** * Returns the {@link NotificationCompat} used as part of the foreground service. */ private Notification getNotification() { Intent intent = new Intent(this, LocationUpdatesService.class); CharSequence text = Utils.getLocationText(mLocation); // Extra to help us figure out if we arrived in onStartCommand via the notification or not. intent.putExtra(EXTRA_STARTED_FROM_NOTIFICATION, true); // The PendingIntent that leads to a call to onStartCommand() in this service. PendingIntent servicePendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); // The PendingIntent to launch activity. PendingIntent activityPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0); return new NotificationCompat.Builder(this) .addAction(R.drawable.ic_launch, getString(R.string.launch_activity), activityPendingIntent) .addAction(R.drawable.ic_cancel, getString(R.string.remove_location_updates), servicePendingIntent) .setContentText(text) .setContentTitle(Utils.getLocationTitle(this)) .setOngoing(true) .setPriority(Notification.PRIORITY_HIGH) .setSmallIcon(R.mipmap.ic_launcher) .setTicker(text) .setWhen(System.currentTimeMillis()).build(); } @Override public void onConnected(@Nullable Bundle bundle) { Log.i(TAG, "GoogleApiClient connected"); try { mLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient); } catch (SecurityException unlikely) { Log.e(TAG, "Lost location permission." + unlikely); } } @Override public void onConnectionSuspended(int i) { // In this example, we merely log the suspension. Log.e(TAG, "GoogleApiClient connection suspended."); } @Override public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { // In this example, we merely log the failure. Log.e(TAG, "GoogleApiClient connection failed."); } @Override public void onLocationChanged(Location location) { Log.i(TAG, "New location: " + location); mLocation = location; // Notify anyone listening for broadcasts about the new location. Intent intent = new Intent(ACTION_BROADCAST); intent.putExtra(EXTRA_LOCATION, location); LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); // Update notification content if running as a foreground service. if (serviceIsRunningInForeground(this)) { mNotificationManager.notify(NOTIFICATION_ID, getNotification()); } } /** * Sets the location request parameters. */ private void createLocationRequest() { mLocationRequest = new LocationRequest(); mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS); mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS); mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); } /** * Class used for the client Binder. Since this service runs in the same process as its * clients, we don't need to deal with IPC. */ public class LocalBinder extends Binder { LocationUpdatesService getService() { return LocationUpdatesService.this; } } /** * Returns true if this is a foreground service. * * @param context The {@link Context}. */ public boolean serviceIsRunningInForeground(Context context) { ActivityManager manager = (ActivityManager) context.getSystemService( Context.ACTIVITY_SERVICE); for (ActivityManager.RunningServiceInfo service : manager.getRunningServices( Integer.MAX_VALUE)) { if (getClass().getName().equals(service.service.getClassName())) { if (service.foreground) { return true; } } } return false; } }