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.car.hardware.CarPropertyValue; 20 import android.car.hardware.property.CarPropertyEvent; 21 import android.car.hardware.property.ICarPropertyEventListener; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.hardware.automotive.vehicle.V2_0.VehicleIgnitionState; 27 import android.hardware.automotive.vehicle.V2_0.VehicleProperty; 28 import android.location.Location; 29 import android.location.LocationManager; 30 import android.os.Handler; 31 import android.os.HandlerThread; 32 import android.os.RemoteException; 33 import android.os.SystemClock; 34 import android.util.AtomicFile; 35 import android.util.JsonReader; 36 import android.util.JsonWriter; 37 import android.util.Log; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 41 import java.io.FileInputStream; 42 import java.io.FileNotFoundException; 43 import java.io.FileOutputStream; 44 import java.io.IOException; 45 import java.io.InputStreamReader; 46 import java.io.OutputStreamWriter; 47 import java.io.PrintWriter; 48 import java.util.List; 49 50 /** 51 * This service stores the last known location from {@link LocationManager} when a car is parked 52 * and restores the location when the car is powered on. 53 */ 54 public class CarLocationService extends BroadcastReceiver implements CarServiceBase, 55 CarPowerManagementService.PowerEventProcessingHandler { 56 private static final String TAG = "CarLocationService"; 57 private static final String FILENAME = "location_cache.json"; 58 private static final boolean DBG = false; 59 // The accuracy for the stored timestamp 60 private static final long GRANULARITY_ONE_DAY_MS = 24 * 60 * 60 * 1000L; 61 // The time-to-live for the cached location 62 private static final long TTL_THIRTY_DAYS_MS = 30 * GRANULARITY_ONE_DAY_MS; 63 64 // Used internally for mHandlerThread synchronization 65 private final Object mLock = new Object(); 66 67 private final Context mContext; 68 private final CarPowerManagementService mCarPowerManagementService; 69 private final CarPropertyService mCarPropertyService; 70 private final CarPropertyEventListener mCarPropertyEventListener; 71 private int mTaskCount = 0; 72 private HandlerThread mHandlerThread; 73 private Handler mHandler; 74 75 public CarLocationService(Context context, CarPowerManagementService carPowerManagementService, 76 CarPropertyService carPropertyService) { 77 logd("constructed"); 78 mContext = context; 79 mCarPowerManagementService = carPowerManagementService; 80 mCarPropertyService = carPropertyService; 81 mCarPropertyEventListener = new CarPropertyEventListener(); 82 } 83 84 @Override 85 public void init() { 86 logd("init"); 87 IntentFilter filter = new IntentFilter(); 88 filter.addAction(Intent.ACTION_LOCKED_BOOT_COMPLETED); 89 filter.addAction(LocationManager.MODE_CHANGED_ACTION); 90 filter.addAction(LocationManager.GPS_ENABLED_CHANGE_ACTION); 91 mContext.registerReceiver(this, filter); 92 mCarPropertyService.registerListener(VehicleProperty.IGNITION_STATE, 0, 93 mCarPropertyEventListener); 94 mCarPowerManagementService.registerPowerEventProcessingHandler(this); 95 } 96 97 @Override 98 public void release() { 99 logd("release"); 100 mCarPropertyService.unregisterListener(VehicleProperty.IGNITION_STATE, 101 mCarPropertyEventListener); 102 mContext.unregisterReceiver(this); 103 } 104 105 @Override 106 public void dump(PrintWriter writer) { 107 writer.println(TAG); 108 writer.println("Context: " + mContext); 109 writer.println("CarPropertyService: " + mCarPropertyService); 110 } 111 112 @Override 113 public long onPrepareShutdown(boolean shuttingDown) { 114 logd("onPrepareShutdown " + shuttingDown); 115 asyncOperation(() -> storeLocation()); 116 return 0; 117 } 118 119 @Override 120 public void onPowerOn(boolean displayOn) { } 121 122 @Override 123 public int getWakeupTime() { 124 return 0; 125 } 126 127 @Override 128 public void onReceive(Context context, Intent intent) { 129 logd("onReceive " + intent); 130 String action = intent.getAction(); 131 if (action == Intent.ACTION_LOCKED_BOOT_COMPLETED) { 132 asyncOperation(() -> loadLocation()); 133 } else { 134 LocationManager locationManager = 135 (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); 136 if (action == LocationManager.MODE_CHANGED_ACTION) { 137 boolean locationEnabled = locationManager.isLocationEnabled(); 138 logd("isLocationEnabled(): " + locationEnabled); 139 if (!locationEnabled) { 140 asyncOperation(() -> deleteCacheFile()); 141 } 142 } else if (action == LocationManager.GPS_ENABLED_CHANGE_ACTION) { 143 boolean gpsEnabled = 144 locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); 145 logd("isProviderEnabled('gps'): " + gpsEnabled); 146 if (!gpsEnabled) { 147 asyncOperation(() -> deleteCacheFile()); 148 } 149 } 150 } 151 } 152 153 private void storeLocation() { 154 LocationManager locationManager = 155 (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); 156 Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); 157 if (location == null) { 158 logd("Not storing null location"); 159 deleteCacheFile(); 160 } else { 161 logd("Storing location: " + location); 162 AtomicFile atomicFile = new AtomicFile(mContext.getFileStreamPath(FILENAME)); 163 FileOutputStream fos = null; 164 try { 165 fos = atomicFile.startWrite(); 166 JsonWriter jsonWriter = new JsonWriter(new OutputStreamWriter(fos, "UTF-8")); 167 jsonWriter.beginObject(); 168 jsonWriter.name("provider").value(location.getProvider()); 169 jsonWriter.name("latitude").value(location.getLatitude()); 170 jsonWriter.name("longitude").value(location.getLongitude()); 171 if (location.hasAltitude()) { 172 jsonWriter.name("altitude").value(location.getAltitude()); 173 } 174 if (location.hasSpeed()) { 175 jsonWriter.name("speed").value(location.getSpeed()); 176 } 177 if (location.hasBearing()) { 178 jsonWriter.name("bearing").value(location.getBearing()); 179 } 180 if (location.hasAccuracy()) { 181 jsonWriter.name("accuracy").value(location.getAccuracy()); 182 } 183 if (location.hasVerticalAccuracy()) { 184 jsonWriter.name("verticalAccuracy").value( 185 location.getVerticalAccuracyMeters()); 186 } 187 if (location.hasSpeedAccuracy()) { 188 jsonWriter.name("speedAccuracy").value( 189 location.getSpeedAccuracyMetersPerSecond()); 190 } 191 if (location.hasBearingAccuracy()) { 192 jsonWriter.name("bearingAccuracy").value( 193 location.getBearingAccuracyDegrees()); 194 } 195 if (location.isFromMockProvider()) { 196 jsonWriter.name("isFromMockProvider").value(true); 197 } 198 long currentTime = location.getTime(); 199 // Round the time down to only be accurate within one day. 200 jsonWriter.name("captureTime").value( 201 currentTime - currentTime % GRANULARITY_ONE_DAY_MS); 202 jsonWriter.endObject(); 203 jsonWriter.close(); 204 atomicFile.finishWrite(fos); 205 } catch (IOException e) { 206 Log.e(TAG, "Unable to write to disk", e); 207 atomicFile.failWrite(fos); 208 } 209 } 210 } 211 212 private void loadLocation() { 213 Location location = readLocationFromCacheFile(); 214 logd("Read location from " + location.getTime()); 215 long currentTime = System.currentTimeMillis(); 216 if (location.getTime() + TTL_THIRTY_DAYS_MS < currentTime) { 217 logd("Location expired."); 218 } else { 219 location.setTime(currentTime); 220 long elapsedTime = SystemClock.elapsedRealtimeNanos(); 221 location.setElapsedRealtimeNanos(elapsedTime); 222 if (location.isComplete()) { 223 LocationManager locationManager = 224 (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); 225 boolean success = locationManager.injectLocation(location); 226 logd("Injected location " + location + " with result " + success); 227 } 228 } 229 } 230 231 private Location readLocationFromCacheFile() { 232 Location location = new Location((String) null); 233 AtomicFile atomicFile = new AtomicFile(mContext.getFileStreamPath(FILENAME)); 234 try { 235 FileInputStream fis = atomicFile.openRead(); 236 JsonReader reader = new JsonReader(new InputStreamReader(fis, "UTF-8")); 237 reader.beginObject(); 238 while (reader.hasNext()) { 239 String name = reader.nextName(); 240 if (name.equals("provider")) { 241 location.setProvider(reader.nextString()); 242 } else if (name.equals("latitude")) { 243 location.setLatitude(reader.nextDouble()); 244 } else if (name.equals("longitude")) { 245 location.setLongitude(reader.nextDouble()); 246 } else if (name.equals("altitude")) { 247 location.setAltitude(reader.nextDouble()); 248 } else if (name.equals("speed")) { 249 location.setSpeed((float) reader.nextDouble()); 250 } else if (name.equals("bearing")) { 251 location.setBearing((float) reader.nextDouble()); 252 } else if (name.equals("accuracy")) { 253 location.setAccuracy((float) reader.nextDouble()); 254 } else if (name.equals("verticalAccuracy")) { 255 location.setVerticalAccuracyMeters((float) reader.nextDouble()); 256 } else if (name.equals("speedAccuracy")) { 257 location.setSpeedAccuracyMetersPerSecond((float) reader.nextDouble()); 258 } else if (name.equals("bearingAccuracy")) { 259 location.setBearingAccuracyDegrees((float) reader.nextDouble()); 260 } else if (name.equals("isFromMockProvider")) { 261 location.setIsFromMockProvider(reader.nextBoolean()); 262 } else if (name.equals("captureTime")) { 263 location.setTime(reader.nextLong()); 264 } else { 265 reader.skipValue(); 266 } 267 } 268 reader.endObject(); 269 fis.close(); 270 deleteCacheFile(); 271 } catch (FileNotFoundException e) { 272 Log.d(TAG, "Location cache file not found."); 273 } catch (IOException e) { 274 Log.e(TAG, "Unable to read from disk", e); 275 } catch (NumberFormatException | IllegalStateException e) { 276 Log.e(TAG, "Unexpected format", e); 277 } 278 return location; 279 } 280 281 private void deleteCacheFile() { 282 logd("Deleting cache file"); 283 mContext.deleteFile(FILENAME); 284 } 285 286 @VisibleForTesting 287 void asyncOperation(Runnable operation) { 288 synchronized (mLock) { 289 // Create a new HandlerThread if this is the first task to queue. 290 if (++mTaskCount == 1) { 291 mHandlerThread = new HandlerThread("CarLocationServiceThread"); 292 mHandlerThread.start(); 293 mHandler = new Handler(mHandlerThread.getLooper()); 294 } 295 } 296 mHandler.post(() -> { 297 try { 298 operation.run(); 299 } finally { 300 synchronized (mLock) { 301 // Quit the thread when the task queue is empty. 302 if (--mTaskCount == 0) { 303 mHandler.getLooper().quit(); 304 mHandler = null; 305 mHandlerThread = null; 306 } 307 } 308 } 309 }); 310 } 311 312 private static void logd(String msg) { 313 if (DBG) { 314 Log.d(TAG, msg); 315 } 316 } 317 318 private class CarPropertyEventListener extends ICarPropertyEventListener.Stub { 319 @Override 320 public void onEvent(List<CarPropertyEvent> events) throws RemoteException { 321 for (CarPropertyEvent event : events) { 322 if (event.getEventType() == CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE) { 323 CarPropertyValue value = event.getCarPropertyValue(); 324 if (value.getPropertyId() == VehicleProperty.IGNITION_STATE) { 325 int ignitionState = (Integer) value.getValue(); 326 logd("property ignition value: " + ignitionState); 327 if (ignitionState == VehicleIgnitionState.OFF) { 328 logd("ignition off"); 329 asyncOperation(() -> storeLocation()); 330 } 331 } 332 } 333 } 334 } 335 } 336 } 337