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 static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
20 
21 import android.app.ActivityManager;
22 import android.car.ICarPerUserService;
23 import android.car.ILocationManagerProxy;
24 import android.car.builtin.util.Slogf;
25 import android.car.drivingstate.CarDrivingStateEvent;
26 import android.car.drivingstate.ICarDrivingStateChangeListener;
27 import android.car.hardware.power.CarPowerManager;
28 import android.car.hardware.power.CarPowerManager.CarPowerStateListenerWithCompletion;
29 import android.car.hardware.power.CarPowerManager.CompletablePowerStateChangeFuture;
30 import android.car.hardware.power.CarPowerPolicy;
31 import android.car.hardware.power.CarPowerPolicyFilter;
32 import android.car.hardware.power.ICarPowerPolicyListener;
33 import android.car.hardware.power.PowerComponent;
34 import android.content.BroadcastReceiver;
35 import android.content.Context;
36 import android.content.Intent;
37 import android.content.IntentFilter;
38 import android.location.Location;
39 import android.location.LocationManager;
40 import android.os.Handler;
41 import android.os.HandlerThread;
42 import android.os.RemoteException;
43 import android.os.SystemClock;
44 import android.os.UserHandle;
45 import android.os.UserManager;
46 import android.util.AtomicFile;
47 import android.util.JsonReader;
48 import android.util.JsonWriter;
49 import android.util.proto.ProtoOutputStream;
50 
51 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
52 import com.android.car.internal.util.IndentingPrintWriter;
53 import com.android.car.power.CarPowerManagementService;
54 import com.android.car.systeminterface.SystemInterface;
55 import com.android.internal.annotations.GuardedBy;
56 import com.android.internal.annotations.VisibleForTesting;
57 
58 import java.io.File;
59 import java.io.FileInputStream;
60 import java.io.FileNotFoundException;
61 import java.io.FileOutputStream;
62 import java.io.IOException;
63 import java.io.InputStreamReader;
64 import java.io.OutputStreamWriter;
65 
66 /**
67  * This service stores the last known location from {@link LocationManager} when a car is parked
68  * and restores the location when the car is powered on.
69  */
70 public class CarLocationService extends BroadcastReceiver implements CarServiceBase,
71         CarPowerStateListenerWithCompletion {
72     private static final String TAG = CarLog.tagFor(CarLocationService.class);
73     private static final String FILENAME = "location_cache.json";
74     // The accuracy for the stored timestamp
75     private static final long GRANULARITY_ONE_DAY_MS = 24 * 60 * 60 * 1000L;
76     // The time-to-live for the cached location
77     private static final long TTL_THIRTY_DAYS_MS = 30 * GRANULARITY_ONE_DAY_MS;
78     // The maximum number of times to try injecting a location
79     private static final int MAX_LOCATION_INJECTION_ATTEMPTS = 10;
80 
81     // Constants for location serialization.
82     private static final String PROVIDER = "provider";
83     private static final String LATITUDE = "latitude";
84     private static final String LONGITUDE = "longitude";
85     private static final String ALTITUDE = "altitude";
86     private static final String SPEED = "speed";
87     private static final String BEARING = "bearing";
88     private static final String ACCURACY = "accuracy";
89     private static final String VERTICAL_ACCURACY = "verticalAccuracy";
90     private static final String SPEED_ACCURACY = "speedAccuracy";
91     private static final String BEARING_ACCURACY = "bearingAccuracy";
92     private static final String IS_FROM_MOCK_PROVIDER = "isFromMockProvider";
93     private static final String CAPTURE_TIME = "captureTime";
94 
95     private final Object mLock = new Object();
96 
97     // Used internally for mILocationManagerProxy synchronization
98     private final Object mLocationManagerProxyLock = new Object();
99 
100     private final Context mContext;
101     private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
102             getClass().getSimpleName());
103     private final Handler mHandler = new Handler(mHandlerThread.getLooper());
104 
105     private final CarPowerManager mCarPowerManager;
106     private final CarDrivingStateService mCarDrivingStateService;
107     private final CarPerUserServiceHelper mCarPerUserServiceHelper;
108     private final CarPowerManagementService mCarPowerManagementService;
109 
110     @GuardedBy("mLock")
111     private LocationManager mLocationManager;
112 
113     // Allows us to interact with the {@link LocationManager} as the foreground user.
114     @GuardedBy("mLocationManagerProxyLock")
115     private ILocationManagerProxy mILocationManagerProxy;
116 
117     // Used for supporting features such as suspend to RAM and suspend to disk. Power policy
118     // listener listens to changes in the PowerComponent.LOCATION state. If PowerComponent.LOCATION
119     // is disabled, suspend gnss functionality. If PowerComponent.LOCATION is enabled, resume gnss
120     // functionality.
121     private final ICarPowerPolicyListener mPowerPolicyListener =
122             new ICarPowerPolicyListener.Stub() {
123                 @Override
124                 public void onPolicyChanged(CarPowerPolicy appliedPolicy,
125                         CarPowerPolicy accumulatedPolicy) {
126                     LocationManager locationManager;
127 
128                     synchronized (mLock) {
129                         // LocationManager can be temporarily unavailable during boot up. Reacquire
130                         // here if mLocationManager is null.
131                         if (mLocationManager == null) {
132                             logd("Null location manager. Retry.");
133                             mLocationManager = mContext.getSystemService(LocationManager.class);
134                         }
135 
136                         // If LocationManager is still null after the retry, return.
137                         if (mLocationManager == null) {
138                             logd("Null location manager. Skip gnss controls.");
139                             return;
140                         }
141 
142                         locationManager = mLocationManager;
143                     }
144 
145                     boolean isOn =
146                             accumulatedPolicy.isComponentEnabled(PowerComponent.LOCATION);
147                     try {
148                         if (isOn) {
149                             logd("Resume GNSS requests.");
150                             locationManager.setAutomotiveGnssSuspended(false);
151                             if (locationManager.isAutomotiveGnssSuspended()) {
152                                 Slogf.w(
153                                         TAG,
154                                         "Failed - isAutomotiveGnssSuspended is true. "
155                                                 + "GNSS should NOT be suspended.");
156                             }
157                         } else {
158                             logd("Suspend GNSS requests.");
159                             locationManager.setAutomotiveGnssSuspended(true);
160                             if (!locationManager.isAutomotiveGnssSuspended()) {
161                                 Slogf.w(
162                                         TAG,
163                                         "Failed - isAutomotiveGnssSuspended is false. "
164                                                 + "GNSS should be suspended.");
165                             }
166                         }
167                     } catch (NullPointerException e) {
168                         Slogf.w(
169                                 TAG,
170                                 "The device might not support GNSS thus GNSSManagerService may be"
171                                         + " null",
172                                 e);
173                     }
174                 }
175     };
176 
177     // Maintains mILocationManagerProxy for the current foreground user.
178     private final CarPerUserServiceHelper.ServiceCallback mUserServiceCallback =
179             new CarPerUserServiceHelper.ServiceCallback() {
180                 @Override
181                 public void onServiceConnected(ICarPerUserService carPerUserService) {
182                     logd("Connected to CarPerUserService");
183                     if (carPerUserService == null) {
184                         logd("ICarPerUserService is null. Cannot get location manager proxy");
185                         return;
186                     }
187                     synchronized (mLocationManagerProxyLock) {
188                         try {
189                             mILocationManagerProxy = carPerUserService.getLocationManagerProxy();
190                         } catch (RemoteException e) {
191                             Slogf.e(TAG, "RemoteException from ICarPerUserService", e);
192                             return;
193                         }
194                     }
195                     int currentUser = ActivityManager.getCurrentUser();
196                     logd("Current user: %s", currentUser);
197                     if (UserManager.isHeadlessSystemUserMode()
198                             && currentUser > UserHandle.SYSTEM.getIdentifier()) {
199                         asyncOperation(() -> loadLocation());
200                     }
201                 }
202 
203                 @Override
204                 public void onPreUnbind() {
205                     logd("Before Unbinding from CarPerUserService");
206                     synchronized (mLocationManagerProxyLock) {
207                         mILocationManagerProxy = null;
208                     }
209                 }
210 
211                 @Override
212                 public void onServiceDisconnected() {
213                     logd("Disconnected from CarPerUserService");
214                     synchronized (mLocationManagerProxyLock) {
215                         mILocationManagerProxy = null;
216                     }
217                 }
218             };
219 
220     private final ICarDrivingStateChangeListener mICarDrivingStateChangeEventListener =
221             new ICarDrivingStateChangeListener.Stub() {
222                 @Override
223                 public void onDrivingStateChanged(CarDrivingStateEvent event) {
224                     logd("onDrivingStateChanged: %s", event);
225                     if (event != null
226                             && event.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
227                         deleteCacheFile();
228                         if (mCarDrivingStateService != null) {
229                             mCarDrivingStateService.unregisterDrivingStateChangeListener(
230                                     mICarDrivingStateChangeEventListener);
231                         }
232                     }
233                 }
234             };
235 
CarLocationService(Context context)236     public CarLocationService(Context context) {
237         logd("constructed");
238         mContext = context;
239 
240         mCarPowerManagementService = CarLocalServices.getService(
241                 CarPowerManagementService.class);
242         if (mCarPowerManagementService == null) {
243             Slogf.w(TAG, "Cannot find CarPowerManagementService.");
244         }
245 
246         mCarPowerManager = CarLocalServices.createCarPowerManager(mContext);
247         if (mCarPowerManager == null) {
248             Slogf.w(TAG, "Cannot find CarPowerManager.");
249         }
250 
251         mCarPerUserServiceHelper = CarLocalServices.getService(CarPerUserServiceHelper.class);
252         if (mCarPerUserServiceHelper == null) {
253             Slogf.w(TAG, "Cannot find CarPerUserServiceHelper.");
254         }
255 
256         mCarDrivingStateService = CarLocalServices.getService(CarDrivingStateService.class);
257         if (mCarDrivingStateService == null) {
258             Slogf.w(TAG, "Cannot find CarDrivingStateService.");
259         }
260     }
261 
262     @Override
init()263     public void init() {
264         logd("init");
265         IntentFilter filter = new IntentFilter();
266         filter.addAction(LocationManager.MODE_CHANGED_ACTION);
267         mContext.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
268 
269         if (mCarDrivingStateService != null) {
270             CarDrivingStateEvent event = mCarDrivingStateService.getCurrentDrivingState();
271             if (event != null && event.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
272                 deleteCacheFile();
273             } else {
274                 mCarDrivingStateService.registerDrivingStateChangeListener(
275                         mICarDrivingStateChangeEventListener);
276             }
277         }
278 
279         if (mCarPowerManager != null) { // null case happens for testing.
280             mCarPowerManager.setListenerWithCompletion((command) -> mHandler.post(command),
281                     CarLocationService.this);
282         }
283 
284         if (mCarPerUserServiceHelper != null) {
285             mCarPerUserServiceHelper.registerServiceCallback(mUserServiceCallback);
286         }
287 
288         synchronized (mLock) {
289             mLocationManager = mContext.getSystemService(LocationManager.class);
290             if (mLocationManager == null) {
291                 Slogf.w(TAG, "Null location manager.");
292             }
293         }
294 
295         // Power policy listener will check for the LocationManager dependency and reacquire it if
296         // is not available yet at time of boot.
297         addPowerPolicyListener();
298     }
299 
300     @Override
release()301     public void release() {
302         logd("release");
303         if (mCarPowerManager != null) {
304             mCarPowerManager.clearListener();
305         }
306         if (mCarDrivingStateService != null) {
307             mCarDrivingStateService.unregisterDrivingStateChangeListener(
308                     mICarDrivingStateChangeEventListener);
309         }
310         if (mCarPerUserServiceHelper != null) {
311             mCarPerUserServiceHelper.unregisterServiceCallback(mUserServiceCallback);
312         }
313         if (mCarPowerManagementService != null) {
314             mCarPowerManagementService.removePowerPolicyListener(mPowerPolicyListener);
315         }
316         mContext.unregisterReceiver(this);
317     }
318 
319     @Override
320     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)321     public void dump(IndentingPrintWriter writer) {
322         writer.println(TAG);
323         mCarPerUserServiceHelper.dump(writer);
324         writer.printf("Context: %s\n", mContext);
325         writer.printf("MAX_LOCATION_INJECTION_ATTEMPTS: %d\n", MAX_LOCATION_INJECTION_ATTEMPTS);
326     }
327 
328     @Override
329     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)330     public void dumpProto(ProtoOutputStream proto) {}
331 
332     @Override
onStateChanged(int state, CompletablePowerStateChangeFuture future)333     public void onStateChanged(int state, CompletablePowerStateChangeFuture future) {
334         logd("onStateChanged: %d", state);
335         switch (state) {
336             case CarPowerManager.STATE_PRE_SHUTDOWN_PREPARE:
337                 if (future != null) {
338                     future.complete();
339                 }
340                 break;
341             case CarPowerManager.STATE_SHUTDOWN_PREPARE:
342                 storeLocation();
343                 // Notify the CarPowerManager that it may proceed to shutdown or suspend.
344                 if (future != null) {
345                     future.complete();
346                 }
347                 break;
348             case CarPowerManager.STATE_SUSPEND_EXIT:
349                 if (mCarDrivingStateService != null) {
350                     CarDrivingStateEvent event = mCarDrivingStateService.getCurrentDrivingState();
351                     if (event != null
352                             && event.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
353                         deleteCacheFile();
354                     } else {
355                         logd("Registering to receive driving state.");
356                         mCarDrivingStateService.registerDrivingStateChangeListener(
357                                 mICarDrivingStateChangeEventListener);
358                     }
359                 }
360                 if (future != null) {
361                     future.complete();
362                 }
363             default:
364                 // This service does not need to do any work for these events but should still
365                 // notify the CarPowerManager that it may proceed.
366                 if (future != null) {
367                     future.complete();
368                 }
369                 break;
370         }
371     }
372 
373     @Override
onReceive(Context context, Intent intent)374     public void onReceive(Context context, Intent intent) {
375         logd("onReceive %s", intent);
376         // If the system user is headless but the current user is still the system user, then we
377         // should not delete the location cache file due to missing location permissions.
378         if (isCurrentUserHeadlessSystemUser()) {
379             logd("Current user is headless system user.");
380             return;
381         }
382         synchronized (mLocationManagerProxyLock) {
383             if (mILocationManagerProxy == null) {
384                 logd("Null location manager.");
385                 return;
386             }
387             String action = intent.getAction();
388             try {
389                 if (action == LocationManager.MODE_CHANGED_ACTION) {
390                     boolean locationEnabled = mILocationManagerProxy.isLocationEnabled();
391                     logd("isLocationEnabled(): %s", locationEnabled);
392                     if (!locationEnabled) {
393                         deleteCacheFile();
394                     }
395                 } else {
396                     logd("Unexpected intent.");
397                 }
398             } catch (RemoteException e) {
399                 Slogf.e(TAG, "RemoteException from ILocationManagerProxy", e);
400             }
401         }
402     }
403 
404     /**
405      * If config_enableCarLocationServiceGnssControlsForPowerManagement is true, add a power policy
406      * listener.
407      */
addPowerPolicyListener()408     private void addPowerPolicyListener() {
409         if (mCarPowerManagementService == null) {
410             return;
411         }
412 
413         if (!mContext.getResources().getBoolean(
414                 R.bool.config_enableCarLocationServiceGnssControlsForPowerManagement)) {
415             logd("GNSS controls not enabled.");
416             return;
417         }
418 
419         CarPowerPolicyFilter carPowerPolicyFilter = new CarPowerPolicyFilter.Builder()
420                 .setComponents(PowerComponent.LOCATION).build();
421         mCarPowerManagementService.addPowerPolicyListener(
422                 carPowerPolicyFilter, mPowerPolicyListener);
423     }
424 
425     /** Tells whether the current foreground user is the headless system user. */
isCurrentUserHeadlessSystemUser()426     private boolean isCurrentUserHeadlessSystemUser() {
427         int currentUserId = ActivityManager.getCurrentUser();
428         return UserManager.isHeadlessSystemUserMode()
429                 && currentUserId == UserHandle.SYSTEM.getIdentifier();
430     }
431 
432     /**
433      * Gets the last known location from the location manager proxy and store it in a file.
434      */
storeLocation()435     private void storeLocation() {
436         Location location = null;
437         synchronized (mLocationManagerProxyLock) {
438             if (mILocationManagerProxy == null) {
439                 logd("Null location manager proxy.");
440                 return;
441             }
442             try {
443                 location = mILocationManagerProxy.getLastKnownLocation(
444                         LocationManager.GPS_PROVIDER);
445             } catch (RemoteException e) {
446                 Slogf.e(TAG, "RemoteException from ILocationManagerProxy", e);
447             }
448         }
449         if (location == null) {
450             logd("Not storing null location");
451         } else {
452             logd("Storing location");
453             AtomicFile atomicFile = new AtomicFile(getLocationCacheFile());
454             FileOutputStream fos = null;
455             try {
456                 fos = atomicFile.startWrite();
457                 try (JsonWriter jsonWriter = new JsonWriter(new OutputStreamWriter(fos, "UTF-8"))) {
458                     jsonWriter.beginObject();
459                     jsonWriter.name(PROVIDER).value(location.getProvider());
460                     jsonWriter.name(LATITUDE).value(location.getLatitude());
461                     jsonWriter.name(LONGITUDE).value(location.getLongitude());
462                     if (location.hasAltitude()) {
463                         jsonWriter.name(ALTITUDE).value(location.getAltitude());
464                     }
465                     if (location.hasSpeed()) {
466                         jsonWriter.name(SPEED).value(location.getSpeed());
467                     }
468                     if (location.hasBearing()) {
469                         jsonWriter.name(BEARING).value(location.getBearing());
470                     }
471                     if (location.hasAccuracy()) {
472                         jsonWriter.name(ACCURACY).value(location.getAccuracy());
473                     }
474                     if (location.hasVerticalAccuracy()) {
475                         jsonWriter.name(VERTICAL_ACCURACY).value(
476                                 location.getVerticalAccuracyMeters());
477                     }
478                     if (location.hasSpeedAccuracy()) {
479                         jsonWriter.name(SPEED_ACCURACY).value(
480                                 location.getSpeedAccuracyMetersPerSecond());
481                     }
482                     if (location.hasBearingAccuracy()) {
483                         jsonWriter.name(BEARING_ACCURACY).value(
484                                 location.getBearingAccuracyDegrees());
485                     }
486                     if (location.isFromMockProvider()) {
487                         jsonWriter.name(IS_FROM_MOCK_PROVIDER).value(true);
488                     }
489                     long currentTime = location.getTime();
490                     // Round the time down to only be accurate within one day.
491                     jsonWriter.name(CAPTURE_TIME).value(
492                             currentTime - currentTime % GRANULARITY_ONE_DAY_MS);
493                     jsonWriter.endObject();
494                 }
495                 atomicFile.finishWrite(fos);
496             } catch (IOException e) {
497                 Slogf.e(TAG, "Unable to write to disk", e);
498                 atomicFile.failWrite(fos);
499             }
500         }
501     }
502 
503     /**
504      * Reads a previously stored location and attempts to inject it into the location manager proxy.
505      */
loadLocation()506     private void loadLocation() {
507         Location location = readLocationFromCacheFile();
508         logd("Read location from timestamp %s", location.getTime());
509         long currentTime = System.currentTimeMillis();
510         if (location.getTime() + TTL_THIRTY_DAYS_MS < currentTime) {
511             logd("Location expired.");
512             deleteCacheFile();
513         } else {
514             location.setTime(currentTime);
515             long elapsedTime = SystemClock.elapsedRealtimeNanos();
516             location.setElapsedRealtimeNanos(elapsedTime);
517             if (location.isComplete()) {
518                 injectLocation(location, 1);
519             }
520         }
521     }
522 
readLocationFromCacheFile()523     private Location readLocationFromCacheFile() {
524         Location location = new Location((String) null);
525         File file = getLocationCacheFile();
526         AtomicFile atomicFile = new AtomicFile(file);
527         try (FileInputStream fis = atomicFile.openRead()) {
528             JsonReader reader = new JsonReader(new InputStreamReader(fis, "UTF-8"));
529             reader.beginObject();
530             while (reader.hasNext()) {
531                 String name = reader.nextName();
532                 switch (name) {
533                     case PROVIDER:
534                         location.setProvider(reader.nextString());
535                         break;
536                     case LATITUDE:
537                         location.setLatitude(reader.nextDouble());
538                         break;
539                     case LONGITUDE:
540                         location.setLongitude(reader.nextDouble());
541                         break;
542                     case ALTITUDE:
543                         location.setAltitude(reader.nextDouble());
544                         break;
545                     case SPEED:
546                         location.setSpeed((float) reader.nextDouble());
547                         break;
548                     case BEARING:
549                         location.setBearing((float) reader.nextDouble());
550                         break;
551                     case ACCURACY:
552                         location.setAccuracy((float) reader.nextDouble());
553                         break;
554                     case VERTICAL_ACCURACY:
555                         location.setVerticalAccuracyMeters((float) reader.nextDouble());
556                         break;
557                     case SPEED_ACCURACY:
558                         location.setSpeedAccuracyMetersPerSecond((float) reader.nextDouble());
559                         break;
560                     case BEARING_ACCURACY:
561                         location.setBearingAccuracyDegrees((float) reader.nextDouble());
562                         break;
563                     case IS_FROM_MOCK_PROVIDER:
564                         location.setIsFromMockProvider(reader.nextBoolean());
565                         break;
566                     case CAPTURE_TIME:
567                         location.setTime(reader.nextLong());
568                         break;
569                     default:
570                         Slogf.w(TAG, "Unrecognized key: " + name);
571                         reader.skipValue();
572                 }
573             }
574             reader.endObject();
575         } catch (FileNotFoundException e) {
576             logd("Location cache file not found: %s", file);
577         } catch (IOException e) {
578             Slogf.e(TAG, "Unable to read from disk", e);
579         } catch (NumberFormatException | IllegalStateException e) {
580             Slogf.e(TAG, "Unexpected format", e);
581         }
582         return location;
583     }
584 
deleteCacheFile()585     private void deleteCacheFile() {
586         File file = getLocationCacheFile();
587         boolean deleted = file.delete();
588         if (deleted) {
589             logd("Successfully deleted cache file at %s", file);
590         } else {
591             logd("Failed to delete cache file at %s", file);
592         }
593     }
594 
595     /**
596      * Attempts to inject the location multiple times in case the LocationManager was not fully
597      * initialized or has not updated its handle to the current user yet.
598      */
injectLocation(Location location, int attemptCount)599     private void injectLocation(Location location, int attemptCount) {
600         boolean success = false;
601         synchronized (mLocationManagerProxyLock) {
602             if (mILocationManagerProxy == null) {
603                 logd("Null location manager proxy.");
604             } else {
605                 try {
606                     success = mILocationManagerProxy.injectLocation(location);
607                 } catch (RemoteException e) {
608                     Slogf.e(TAG, "RemoteException from ILocationManagerProxy", e);
609                 }
610             }
611         }
612         if (success) {
613             logd("Successfully injected stored location on attempt %s.", attemptCount);
614             return;
615         } else if (attemptCount <= MAX_LOCATION_INJECTION_ATTEMPTS) {
616             logd("Failed to inject stored location on attempt %s.", attemptCount);
617             asyncOperation(() -> {
618                 injectLocation(location, attemptCount + 1);
619             }, 200L * attemptCount);
620         } else {
621             logd("No location injected.");
622         }
623     }
624 
getLocationCacheFile()625     private File getLocationCacheFile() {
626         SystemInterface systemInterface = CarLocalServices.getService(SystemInterface.class);
627         return new File(systemInterface.getSystemCarDir(), FILENAME);
628     }
629 
630     @VisibleForTesting
asyncOperation(Runnable operation)631     void asyncOperation(Runnable operation) {
632         asyncOperation(operation, 0);
633     }
634 
asyncOperation(Runnable operation, long delayMillis)635     private void asyncOperation(Runnable operation, long delayMillis) {
636         mHandler.postDelayed(() -> operation.run(), delayMillis);
637     }
638 
logd(String msg, Object... vals)639     private static void logd(String msg, Object... vals) {
640         // Disable logs here if they become too spammy.
641         Slogf.d(TAG, msg, vals);
642     }
643 }
644