• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car;
18 
19 import android.app.ActivityManager;
20 import android.car.ICarUserService;
21 import android.car.ILocationManagerProxy;
22 import android.car.drivingstate.CarDrivingStateEvent;
23 import android.car.drivingstate.ICarDrivingStateChangeListener;
24 import android.car.hardware.power.CarPowerManager;
25 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
26 import android.car.hardware.power.CarPowerManager.CarPowerStateListenerWithCompletion;
27 import android.car.userlib.CarUserManagerHelper;
28 import android.content.BroadcastReceiver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.location.Location;
33 import android.location.LocationManager;
34 import android.os.Handler;
35 import android.os.HandlerThread;
36 import android.os.RemoteException;
37 import android.os.SystemClock;
38 import android.os.UserHandle;
39 import android.util.AtomicFile;
40 import android.util.JsonReader;
41 import android.util.JsonWriter;
42 import android.util.Log;
43 
44 import com.android.car.systeminterface.SystemInterface;
45 import com.android.internal.annotations.VisibleForTesting;
46 
47 import java.io.File;
48 import java.io.FileInputStream;
49 import java.io.FileNotFoundException;
50 import java.io.FileOutputStream;
51 import java.io.IOException;
52 import java.io.InputStreamReader;
53 import java.io.OutputStreamWriter;
54 import java.io.PrintWriter;
55 import java.util.concurrent.CompletableFuture;
56 
57 /**
58  * This service stores the last known location from {@link LocationManager} when a car is parked
59  * and restores the location when the car is powered on.
60  */
61 public class CarLocationService extends BroadcastReceiver implements CarServiceBase,
62         CarPowerStateListenerWithCompletion {
63     private static final String TAG = "CarLocationService";
64     private static final String FILENAME = "location_cache.json";
65     private static final boolean DBG = true;
66     // The accuracy for the stored timestamp
67     private static final long GRANULARITY_ONE_DAY_MS = 24 * 60 * 60 * 1000L;
68     // The time-to-live for the cached location
69     private static final long TTL_THIRTY_DAYS_MS = 30 * GRANULARITY_ONE_DAY_MS;
70     // The maximum number of times to try injecting a location
71     private static final int MAX_LOCATION_INJECTION_ATTEMPTS = 10;
72 
73     // Used internally for mHandlerThread synchronization
74     private final Object mLock = new Object();
75 
76     // Used internally for mILocationManagerProxy synchronization
77     private final Object mLocationManagerProxyLock = new Object();
78 
79     private final Context mContext;
80     private final CarUserManagerHelper mCarUserManagerHelper;
81     private int mTaskCount = 0;
82     private HandlerThread mHandlerThread;
83     private Handler mHandler;
84     private CarPowerManager mCarPowerManager;
85     private CarDrivingStateService mCarDrivingStateService;
86     private PerUserCarServiceHelper mPerUserCarServiceHelper;
87 
88     // Allows us to interact with the {@link LocationManager} as the foreground user.
89     private ILocationManagerProxy mILocationManagerProxy;
90 
91     // Maintains mILocationManagerProxy for the current foreground user.
92     private final PerUserCarServiceHelper.ServiceCallback mUserServiceCallback =
93             new PerUserCarServiceHelper.ServiceCallback() {
94                 @Override
95                 public void onServiceConnected(ICarUserService carUserService) {
96                     logd("Connected to PerUserCarService");
97                     if (carUserService == null) {
98                         logd("ICarUserService is null. Cannot get location manager proxy");
99                         return;
100                     }
101                     synchronized (mLocationManagerProxyLock) {
102                         try {
103                             mILocationManagerProxy = carUserService.getLocationManagerProxy();
104                         } catch (RemoteException e) {
105                             Log.e(TAG, "RemoteException from ICarUserService", e);
106                             return;
107                         }
108                     }
109                     int currentUser = ActivityManager.getCurrentUser();
110                     logd("Current user: " + currentUser);
111                     if (mCarUserManagerHelper.isHeadlessSystemUser()
112                             && currentUser > UserHandle.USER_SYSTEM) {
113                         asyncOperation(() -> loadLocation());
114                     }
115                 }
116 
117                 @Override
118                 public void onPreUnbind() {
119                     logd("Before Unbinding from PerCarUserService");
120                     synchronized (mLocationManagerProxyLock) {
121                         mILocationManagerProxy = null;
122                     }
123                 }
124 
125                 @Override
126                 public void onServiceDisconnected() {
127                     logd("Disconnected from PerUserCarService");
128                     synchronized (mLocationManagerProxyLock) {
129                         mILocationManagerProxy = null;
130                     }
131                 }
132             };
133 
134     private final ICarDrivingStateChangeListener mICarDrivingStateChangeEventListener =
135             new ICarDrivingStateChangeListener.Stub() {
136                 @Override
137                 public void onDrivingStateChanged(CarDrivingStateEvent event) {
138                     logd("onDrivingStateChanged " + event);
139                     if (event != null
140                             && event.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
141                         deleteCacheFile();
142                         if (mCarDrivingStateService != null) {
143                             mCarDrivingStateService.unregisterDrivingStateChangeListener(
144                                     mICarDrivingStateChangeEventListener);
145                         }
146                     }
147                 }
148             };
149 
CarLocationService(Context context, CarUserManagerHelper carUserManagerHelper)150     public CarLocationService(Context context, CarUserManagerHelper carUserManagerHelper) {
151         logd("constructed");
152         mContext = context;
153         mCarUserManagerHelper = carUserManagerHelper;
154     }
155 
156     @Override
init()157     public void init() {
158         logd("init");
159         IntentFilter filter = new IntentFilter();
160         filter.addAction(LocationManager.MODE_CHANGED_ACTION);
161         mContext.registerReceiver(this, filter);
162         mCarDrivingStateService = CarLocalServices.getService(CarDrivingStateService.class);
163         if (mCarDrivingStateService != null) {
164             CarDrivingStateEvent event = mCarDrivingStateService.getCurrentDrivingState();
165             if (event != null && event.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
166                 deleteCacheFile();
167             } else {
168                 mCarDrivingStateService.registerDrivingStateChangeListener(
169                         mICarDrivingStateChangeEventListener);
170             }
171         }
172         mCarPowerManager = CarLocalServices.createCarPowerManager(mContext);
173         if (mCarPowerManager != null) { // null case happens for testing.
174             mCarPowerManager.setListenerWithCompletion(CarLocationService.this);
175         }
176         mPerUserCarServiceHelper = CarLocalServices.getService(PerUserCarServiceHelper.class);
177         if (mPerUserCarServiceHelper != null) {
178             mPerUserCarServiceHelper.registerServiceCallback(mUserServiceCallback);
179         }
180     }
181 
182     @Override
release()183     public void release() {
184         logd("release");
185         if (mCarPowerManager != null) {
186             mCarPowerManager.clearListener();
187         }
188         if (mCarDrivingStateService != null) {
189             mCarDrivingStateService.unregisterDrivingStateChangeListener(
190                     mICarDrivingStateChangeEventListener);
191         }
192         if (mPerUserCarServiceHelper != null) {
193             mPerUserCarServiceHelper.unregisterServiceCallback(mUserServiceCallback);
194         }
195         mContext.unregisterReceiver(this);
196     }
197 
198     @Override
dump(PrintWriter writer)199     public void dump(PrintWriter writer) {
200         writer.println(TAG);
201         writer.println("Context: " + mContext);
202         writer.println("MAX_LOCATION_INJECTION_ATTEMPTS: " + MAX_LOCATION_INJECTION_ATTEMPTS);
203     }
204 
205     @Override
onStateChanged(int state, CompletableFuture<Void> future)206     public void onStateChanged(int state, CompletableFuture<Void> future) {
207         logd("onStateChanged: " + state);
208         switch (state) {
209             case CarPowerStateListener.SHUTDOWN_PREPARE:
210                 asyncOperation(() -> {
211                     storeLocation();
212                     // Notify the CarPowerManager that it may proceed to shutdown or suspend.
213                     if (future != null) {
214                         future.complete(null);
215                     }
216                 });
217                 break;
218             case CarPowerStateListener.SUSPEND_EXIT:
219                 deleteCacheFile();
220                 if (future != null) {
221                     future.complete(null);
222                 }
223             default:
224                 // This service does not need to do any work for these events but should still
225                 // notify the CarPowerManager that it may proceed.
226                 if (future != null) {
227                     future.complete(null);
228                 }
229                 break;
230         }
231     }
232 
233     @Override
onReceive(Context context, Intent intent)234     public void onReceive(Context context, Intent intent) {
235         logd("onReceive " + intent);
236         // If the system user is headless but the current user is still the system user, then we
237         // should not delete the location cache file due to missing location permissions.
238         if (isCurrentUserHeadlessSystemUser()) {
239             logd("Current user is headless system user.");
240             return;
241         }
242         synchronized (mLocationManagerProxyLock) {
243             if (mILocationManagerProxy == null) {
244                 logd("Null location manager.");
245                 return;
246             }
247             String action = intent.getAction();
248             try {
249                 if (action == LocationManager.MODE_CHANGED_ACTION) {
250                     boolean locationEnabled = mILocationManagerProxy.isLocationEnabled();
251                     logd("isLocationEnabled(): " + locationEnabled);
252                     if (!locationEnabled) {
253                         deleteCacheFile();
254                     }
255                 } else {
256                     logd("Unexpected intent.");
257                 }
258             } catch (RemoteException e) {
259                 Log.e(TAG, "RemoteException from ILocationManagerProxy", e);
260             }
261         }
262     }
263 
264     /** Tells whether the current foreground user is the headless system user. */
isCurrentUserHeadlessSystemUser()265     private boolean isCurrentUserHeadlessSystemUser() {
266         int currentUserId = ActivityManager.getCurrentUser();
267         return mCarUserManagerHelper.isHeadlessSystemUser() && currentUserId == 0;
268     }
269 
270     /**
271      * Gets the last known location from the location manager proxy and store it in a file.
272      */
storeLocation()273     private void storeLocation() {
274         Location location = null;
275         synchronized (mLocationManagerProxyLock) {
276             if (mILocationManagerProxy == null) {
277                 logd("Null location manager proxy.");
278                 return;
279             }
280             try {
281                 location = mILocationManagerProxy.getLastKnownLocation(
282                         LocationManager.GPS_PROVIDER);
283             } catch (RemoteException e) {
284                 Log.e(TAG, "RemoteException from ILocationManagerProxy", e);
285             }
286         }
287         if (location == null) {
288             logd("Not storing null location");
289         } else {
290             logd("Storing location: " + location);
291             AtomicFile atomicFile = new AtomicFile(getLocationCacheFile());
292             FileOutputStream fos = null;
293             try {
294                 fos = atomicFile.startWrite();
295                 try (JsonWriter jsonWriter = new JsonWriter(new OutputStreamWriter(fos, "UTF-8"))) {
296                     jsonWriter.beginObject();
297                     jsonWriter.name("provider").value(location.getProvider());
298                     jsonWriter.name("latitude").value(location.getLatitude());
299                     jsonWriter.name("longitude").value(location.getLongitude());
300                     if (location.hasAltitude()) {
301                         jsonWriter.name("altitude").value(location.getAltitude());
302                     }
303                     if (location.hasSpeed()) {
304                         jsonWriter.name("speed").value(location.getSpeed());
305                     }
306                     if (location.hasBearing()) {
307                         jsonWriter.name("bearing").value(location.getBearing());
308                     }
309                     if (location.hasAccuracy()) {
310                         jsonWriter.name("accuracy").value(location.getAccuracy());
311                     }
312                     if (location.hasVerticalAccuracy()) {
313                         jsonWriter.name("verticalAccuracy").value(
314                                 location.getVerticalAccuracyMeters());
315                     }
316                     if (location.hasSpeedAccuracy()) {
317                         jsonWriter.name("speedAccuracy").value(
318                                 location.getSpeedAccuracyMetersPerSecond());
319                     }
320                     if (location.hasBearingAccuracy()) {
321                         jsonWriter.name("bearingAccuracy").value(
322                                 location.getBearingAccuracyDegrees());
323                     }
324                     if (location.isFromMockProvider()) {
325                         jsonWriter.name("isFromMockProvider").value(true);
326                     }
327                     long currentTime = location.getTime();
328                     // Round the time down to only be accurate within one day.
329                     jsonWriter.name("captureTime").value(
330                             currentTime - currentTime % GRANULARITY_ONE_DAY_MS);
331                     jsonWriter.endObject();
332                 }
333                 atomicFile.finishWrite(fos);
334             } catch (IOException e) {
335                 Log.e(TAG, "Unable to write to disk", e);
336                 atomicFile.failWrite(fos);
337             }
338         }
339     }
340 
341     /**
342      * Reads a previously stored location and attempts to inject it into the location manager proxy.
343      */
loadLocation()344     private void loadLocation() {
345         Location location = readLocationFromCacheFile();
346         logd("Read location from timestamp " + location.getTime());
347         long currentTime = System.currentTimeMillis();
348         if (location.getTime() + TTL_THIRTY_DAYS_MS < currentTime) {
349             logd("Location expired.");
350             deleteCacheFile();
351         } else {
352             location.setTime(currentTime);
353             long elapsedTime = SystemClock.elapsedRealtimeNanos();
354             location.setElapsedRealtimeNanos(elapsedTime);
355             if (location.isComplete()) {
356                 injectLocation(location, 1);
357             }
358         }
359     }
360 
readLocationFromCacheFile()361     private Location readLocationFromCacheFile() {
362         Location location = new Location((String) null);
363         AtomicFile atomicFile = new AtomicFile(getLocationCacheFile());
364         try (FileInputStream fis = atomicFile.openRead()) {
365             JsonReader reader = new JsonReader(new InputStreamReader(fis, "UTF-8"));
366             reader.beginObject();
367             while (reader.hasNext()) {
368                 String name = reader.nextName();
369                 if (name.equals("provider")) {
370                     location.setProvider(reader.nextString());
371                 } else if (name.equals("latitude")) {
372                     location.setLatitude(reader.nextDouble());
373                 } else if (name.equals("longitude")) {
374                     location.setLongitude(reader.nextDouble());
375                 } else if (name.equals("altitude")) {
376                     location.setAltitude(reader.nextDouble());
377                 } else if (name.equals("speed")) {
378                     location.setSpeed((float) reader.nextDouble());
379                 } else if (name.equals("bearing")) {
380                     location.setBearing((float) reader.nextDouble());
381                 } else if (name.equals("accuracy")) {
382                     location.setAccuracy((float) reader.nextDouble());
383                 } else if (name.equals("verticalAccuracy")) {
384                     location.setVerticalAccuracyMeters((float) reader.nextDouble());
385                 } else if (name.equals("speedAccuracy")) {
386                     location.setSpeedAccuracyMetersPerSecond((float) reader.nextDouble());
387                 } else if (name.equals("bearingAccuracy")) {
388                     location.setBearingAccuracyDegrees((float) reader.nextDouble());
389                 } else if (name.equals("isFromMockProvider")) {
390                     location.setIsFromMockProvider(reader.nextBoolean());
391                 } else if (name.equals("captureTime")) {
392                     location.setTime(reader.nextLong());
393                 } else {
394                     reader.skipValue();
395                 }
396             }
397             reader.endObject();
398         } catch (FileNotFoundException e) {
399             Log.d(TAG, "Location cache file not found.");
400         } catch (IOException e) {
401             Log.e(TAG, "Unable to read from disk", e);
402         } catch (NumberFormatException | IllegalStateException e) {
403             Log.e(TAG, "Unexpected format", e);
404         }
405         return location;
406     }
407 
deleteCacheFile()408     private void deleteCacheFile() {
409         boolean deleted = getLocationCacheFile().delete();
410         logd("Deleted cache file: " + deleted);
411     }
412 
413     /**
414      * Attempts to inject the location multiple times in case the LocationManager was not fully
415      * initialized or has not updated its handle to the current user yet.
416      */
injectLocation(Location location, int attemptCount)417     private void injectLocation(Location location, int attemptCount) {
418         boolean success = false;
419         synchronized (mLocationManagerProxyLock) {
420             if (mILocationManagerProxy == null) {
421                 logd("Null location manager proxy.");
422             } else {
423                 try {
424                     success = mILocationManagerProxy.injectLocation(location);
425                 } catch (RemoteException e) {
426                     Log.e(TAG, "RemoteException from ILocationManagerProxy", e);
427                 }
428             }
429         }
430         logd("Injected location " + location + " with result " + success + " on attempt "
431                 + attemptCount);
432         if (success) {
433             return;
434         } else if (attemptCount <= MAX_LOCATION_INJECTION_ATTEMPTS) {
435             asyncOperation(() -> {
436                 injectLocation(location, attemptCount + 1);
437             }, 200 * attemptCount);
438         } else {
439             logd("No location injected.");
440         }
441     }
442 
getLocationCacheFile()443     private File getLocationCacheFile() {
444         SystemInterface systemInterface = CarLocalServices.getService(SystemInterface.class);
445         File file = new File(systemInterface.getSystemCarDir(), FILENAME);
446         logd("File: " + file);
447         return file;
448     }
449 
450     @VisibleForTesting
asyncOperation(Runnable operation)451     void asyncOperation(Runnable operation) {
452         asyncOperation(operation, 0);
453     }
454 
asyncOperation(Runnable operation, long delayMillis)455     private void asyncOperation(Runnable operation, long delayMillis) {
456         synchronized (mLock) {
457             // Create a new HandlerThread if this is the first task to queue.
458             if (++mTaskCount == 1) {
459                 mHandlerThread = new HandlerThread("CarLocationServiceThread");
460                 mHandlerThread.start();
461                 mHandler = new Handler(mHandlerThread.getLooper());
462             }
463         }
464         mHandler.postDelayed(() -> {
465             try {
466                 operation.run();
467             } finally {
468                 synchronized (mLock) {
469                     // Quit the thread when the task queue is empty.
470                     if (--mTaskCount == 0) {
471                         mHandler.getLooper().quit();
472                         mHandler = null;
473                         mHandlerThread = null;
474                     }
475                 }
476             }
477         }, delayMillis);
478     }
479 
logd(String msg)480     private static void logd(String msg) {
481         if (DBG) {
482             Log.d(TAG, msg);
483         }
484     }
485 }
486