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