Home | History | Annotate | Download | only in car
      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