• 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.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 
CarLocationService(Context context, CarPowerManagementService carPowerManagementService, CarPropertyService carPropertyService)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
init()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
release()98     public void release() {
99         logd("release");
100         mCarPropertyService.unregisterListener(VehicleProperty.IGNITION_STATE,
101                 mCarPropertyEventListener);
102         mContext.unregisterReceiver(this);
103     }
104 
105     @Override
dump(PrintWriter writer)106     public void dump(PrintWriter writer) {
107         writer.println(TAG);
108         writer.println("Context: " + mContext);
109         writer.println("CarPropertyService: " + mCarPropertyService);
110     }
111 
112     @Override
onPrepareShutdown(boolean shuttingDown)113     public long onPrepareShutdown(boolean shuttingDown) {
114         logd("onPrepareShutdown " + shuttingDown);
115         asyncOperation(() -> storeLocation());
116         return 0;
117     }
118 
119     @Override
onPowerOn(boolean displayOn)120     public void onPowerOn(boolean displayOn) { }
121 
122     @Override
getWakeupTime()123     public int getWakeupTime() {
124         return 0;
125     }
126 
127     @Override
onReceive(Context context, Intent intent)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 
storeLocation()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 
loadLocation()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 
readLocationFromCacheFile()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 
deleteCacheFile()281     private void deleteCacheFile() {
282         logd("Deleting cache file");
283         mContext.deleteFile(FILENAME);
284     }
285 
286     @VisibleForTesting
asyncOperation(Runnable operation)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 
logd(String msg)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
onEvent(List<CarPropertyEvent> events)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