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