1 /* 2 * Copyright (C) 2020 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.timezone.location.provider.core; 18 19 import static com.android.timezone.location.provider.core.LogUtils.formatElapsedRealtimeMillis; 20 import static com.android.timezone.location.provider.core.LogUtils.formatUtcTime; 21 import static com.android.timezone.location.provider.core.LogUtils.logDebug; 22 import static com.android.timezone.location.provider.core.LogUtils.logWarn; 23 import static com.android.timezone.location.provider.core.Mode.MODE_DESTROYED; 24 import static com.android.timezone.location.provider.core.Mode.MODE_FAILED; 25 import static com.android.timezone.location.provider.core.Mode.MODE_STARTED; 26 import static com.android.timezone.location.provider.core.Mode.MODE_STOPPED; 27 import static com.android.timezone.location.provider.core.Mode.prettyPrintListenModeEnum; 28 29 import static java.util.concurrent.TimeUnit.NANOSECONDS; 30 31 import android.location.Location; 32 import android.service.timezone.TimeZoneProviderSuggestion; 33 34 import androidx.annotation.GuardedBy; 35 import androidx.annotation.IntDef; 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 39 import com.android.timezone.location.common.PiiLoggable; 40 import com.android.timezone.location.common.PiiLoggables; 41 import com.android.timezone.location.lookup.GeoTimeZonesFinder; 42 import com.android.timezone.location.lookup.GeoTimeZonesFinder.LocationToken; 43 import com.android.timezone.location.provider.core.Environment.LocationListeningResult; 44 import com.android.timezone.location.provider.core.LocationListeningAccountant.ListeningInstruction; 45 46 import java.io.IOException; 47 import java.io.PrintWriter; 48 import java.time.Duration; 49 import java.util.List; 50 import java.util.Objects; 51 52 /** 53 * A class encapsulating the time zone detection logic for an Offline location-based 54 * {@link android.service.timezone.TimeZoneProviderService}. It has been decoupled from the Android 55 * environment and many API via the {@link Environment} interface to enable easier unit testing. 56 * 57 * <p>The overall goal of this class is to balance power consumption with responsiveness. 58 * 59 * <p>Implementation details: 60 * 61 * <p>The instance interacts with multiple threads, but state changes occur in a single-threaded 62 * manner through the use of a lock object, {@link #mLock}. Because multiple threads are involved, 63 * service lifecycle calls like {@link #onDestroy()} may be invoked by different threads than binder 64 * calls like {@link #onStopUpdates()}, leading to unintuitive ordering. See 65 * {@link android.service.timezone.TimeZoneProviderService} for details. 66 * 67 * <p>There are two listening modes: 68 * <ul> 69 * <li>{@link #LOCATION_LISTEN_MODE_PASSIVE}: used most of the time and consumes a negligible amount 70 * of power. It provides an indication of current location but no indication of when location is 71 * unknown.</li> 72 * <li>{@link #LOCATION_LISTEN_MODE_ACTIVE}: used in short bursts, is expected to be high power (may 73 * use GNSS hardware), though it may also not cost much, depending on user settings. This mode 74 * obtains a single location or an "unknown location" response.</li> 75 * </ul> 76 * 77 * <p>When first started, the provider is given an initialization timeout. It is expected to produce 78 * a time zone suggestion within this period. The timeout is configured and the {@link 79 * #mInitializationTimeoutCancellable} field is set. The timeout is cancelled / cleared if a 80 * location is determined (or if the provider is stopped). If the timeout is left to trigger, an 81 * uncertain suggestion is made. 82 * 83 * <p>The provider starts in {@link #LOCATION_LISTEN_MODE_ACTIVE} and remains in that mode for a 84 * short duration. Independently of whether a location is detected, the provider always moves from 85 * {@link #LOCATION_LISTEN_MODE_ACTIVE} to {@link #LOCATION_LISTEN_MODE_PASSIVE}. 86 * 87 * <p>When entering {@link #LOCATION_LISTEN_MODE_PASSIVE}, a mode timeout is set. If a location is 88 * detected while in {@link #LOCATION_LISTEN_MODE_PASSIVE}, the provider may stay in {@link 89 * #LOCATION_LISTEN_MODE_PASSIVE}. If no location has been detected, then the provider may move into 90 * into {@link #LOCATION_LISTEN_MODE_ACTIVE}. 91 * 92 * <p>Generally, when the location is determined, the time zones for the location are looked 93 * up and an {@link TimeZoneProviderResult result} is submitted via {@link 94 * Environment#reportTimeZoneProviderResult(TimeZoneProviderResult)}. When a location cannot be 95 * determined and a suggestion is required, a {@link TimeZoneProviderResult#RESULT_TYPE_UNCERTAIN} 96 * {@link TimeZoneProviderResult result} is submitted. 97 */ 98 public final class OfflineLocationTimeZoneDelegate { 99 100 @IntDef({ LOCATION_LISTEN_MODE_ACTIVE, LOCATION_LISTEN_MODE_PASSIVE }) 101 public @interface ListenModeEnum {} 102 103 /** Use when location listen mode is not applicable. */ 104 @ListenModeEnum 105 public static final int LOCATION_LISTEN_MODE_NA = 0; 106 107 /** Actively listen for a location, once, for a short period. */ 108 @ListenModeEnum 109 public static final int LOCATION_LISTEN_MODE_ACTIVE = 1; 110 111 /** Passively listen for a location until cancelled, possibly for a long period. */ 112 @ListenModeEnum 113 public static final int LOCATION_LISTEN_MODE_PASSIVE = 2; 114 115 @NonNull 116 private final Environment mEnvironment; 117 118 @NonNull 119 private final LocationListeningAccountant mLocationListeningAccountant; 120 121 private final Object mLock = new Object(); 122 123 /** The current mode of the provider. See {@link Mode} for details. */ 124 @GuardedBy("mLock") 125 private final ReferenceWithHistory<Mode> mCurrentMode = 126 new ReferenceWithHistory<>(10, PiiLoggables.toPiiStringFunction()); 127 128 /** 129 * The last location listening result. Holds {@code null} if location listening hasn't started 130 * or produced a result yet. The {@link LocationListeningResult#getLocation()} value can be 131 * {@code null} when location is uncertain. 132 */ 133 @GuardedBy("mLock") 134 private final ReferenceWithHistory<LocationListeningResult> mLastLocationListeningResult = 135 new ReferenceWithHistory<>(10, PiiLoggables.toPiiStringFunction()); 136 137 /** 138 * A token associated with the last location time zone lookup. Used to avoid unnecessary time 139 * zone lookups. Can be {@code null} when location is uncertain. 140 */ 141 @GuardedBy("mLock") 142 @Nullable 143 private LocationToken mLastLocationToken; 144 145 /** 146 * The last time zone provider result determined by the provider. This is used to determine 147 * whether suggestions need to be made to revoke a previous suggestion. It is cleared when the 148 * provider stops. 149 */ 150 @GuardedBy("mLock") 151 private final ReferenceWithHistory<TimeZoneProviderResult> mLastTimeZoneProviderResult = 152 new ReferenceWithHistory<>(10); 153 154 /** 155 * Indicates whether the provider is still within its initialization period. When it times out, 156 * if no suggestion has yet been made then an uncertain suggestion must be made. The reference 157 * can (and must) be used to cancel the timeout if it is no longer required. The reference 158 * must be cleared to indicate the initialization period is over. 159 */ 160 @GuardedBy("mLock") 161 @Nullable 162 private Cancellable mInitializationTimeoutCancellable; 163 164 /** Creates a new instance that uses the supplied {@link Environment}. */ 165 @NonNull create(@onNull Environment environment)166 public static OfflineLocationTimeZoneDelegate create(@NonNull Environment environment) { 167 return new OfflineLocationTimeZoneDelegate(environment); 168 } 169 170 // @VisibleForTesting OfflineLocationTimeZoneDelegate(@onNull Environment environment)171 OfflineLocationTimeZoneDelegate(@NonNull Environment environment) { 172 mEnvironment = Objects.requireNonNull(environment); 173 mLocationListeningAccountant = environment.getLocationListeningAccountant(); 174 175 synchronized (mLock) { 176 mCurrentMode.set(Mode.createStoppedMode()); 177 } 178 } 179 180 /** Called during {@link android.service.timezone.TimeZoneProviderService#onDestroy}. */ onDestroy()181 public void onDestroy() { 182 PiiLoggable entryCause = PiiLoggables.fromString("onDestroy() called"); 183 logDebug(entryCause); 184 185 synchronized (mLock) { 186 cancelTimeoutsAndLocationCallbacks(); 187 188 Mode currentMode = mCurrentMode.get(); 189 if (currentMode.mModeEnum == MODE_STARTED) { 190 sendTimeZoneUncertainResultIfNeeded(); 191 } 192 // The current mode can be set to MODE_DESTROYED in all cases, even from MODE_FAILED. 193 Mode newMode = new Mode(MODE_DESTROYED, entryCause); 194 mCurrentMode.set(newMode); 195 } 196 } 197 198 /** Called during {@link android.service.timezone.TimeZoneProviderService#onStartUpdates}. */ onStartUpdates(@onNull Duration initializationTimeout)199 public void onStartUpdates(@NonNull Duration initializationTimeout) { 200 Objects.requireNonNull(initializationTimeout); 201 202 PiiLoggable debugInfo = PiiLoggables.fromString("onStartUpdates()," 203 + " initializationTimeout=" + initializationTimeout); 204 logDebug(debugInfo); 205 206 synchronized (mLock) { 207 Mode currentMode = mCurrentMode.get(); 208 if (currentMode.mModeEnum == MODE_STOPPED) { 209 enterStartedMode(initializationTimeout, debugInfo); 210 } else { 211 logWarn("Unexpected onStarted() received when in currentMode=" + currentMode); 212 } 213 } 214 } 215 216 /** Called during {@link android.service.timezone.TimeZoneProviderService#onStopUpdates}. */ onStopUpdates()217 public void onStopUpdates() { 218 PiiLoggable debugInfo = PiiLoggables.fromString("onStopUpdates()"); 219 logDebug(debugInfo); 220 221 synchronized (mLock) { 222 Mode currentMode = mCurrentMode.get(); 223 if (currentMode.mModeEnum == MODE_STARTED) { 224 enterStoppedMode(debugInfo); 225 } else if (currentMode.mModeEnum == MODE_DESTROYED) { 226 // This can happen because onDestroy() and onStopUpdates() are handled by different 227 // threads: it is still logged, but at a lower priority than other unexpected 228 // transitions. 229 logDebug("Unexpected onStopUpdates() when currentMode=" + currentMode); 230 } else { 231 logWarn("Unexpected onStopUpdates() when currentMode=" + currentMode); 232 } 233 } 234 } 235 236 /** Called during {@link android.service.timezone.TimeZoneProviderService#dump}. */ dump(@onNull PrintWriter pw)237 public void dump(@NonNull PrintWriter pw) { 238 synchronized (mLock) { 239 // Output useful "current time" information to help with debugging. 240 pw.println("System clock=" + formatUtcTime(System.currentTimeMillis())); 241 pw.println("Elapsed realtime clock=" 242 + formatElapsedRealtimeMillis(mEnvironment.elapsedRealtimeMillis())); 243 244 // State and constants. 245 pw.println("mInitializationTimeoutCancellable=" + mInitializationTimeoutCancellable); 246 pw.println("mLocationListeningAccountant=" + mLocationListeningAccountant); 247 String locationTokenString = 248 mLastLocationToken == null ? "null" : mLastLocationToken.toPiiString(); 249 pw.println("mLastLocationToken=" + locationTokenString); 250 pw.println(); 251 pw.println("Mode history:"); 252 mCurrentMode.dump(pw); 253 pw.println(); 254 pw.println("Location history:"); 255 mLastLocationListeningResult.dump(pw); 256 pw.println(); 257 pw.println("TimeZoneProviderResult history:"); 258 mLastTimeZoneProviderResult.dump(pw); 259 } 260 } 261 262 /** Returns the current mode. Only intended for use in tests. */ getCurrentModeEnumForTests()263 public int getCurrentModeEnumForTests() { 264 synchronized (mLock) { 265 return mCurrentMode.get().mModeEnum; 266 } 267 } 268 269 /** 270 * Handles a {@link LocationListeningResult} from a period of active listening. The result may 271 * contain a location or {@code null}. 272 */ onActiveListeningResult(@onNull LocationListeningResult activeListeningResult)273 private void onActiveListeningResult(@NonNull LocationListeningResult activeListeningResult) { 274 synchronized (mLock) { 275 Mode currentMode = mCurrentMode.get(); 276 if (currentMode.mModeEnum != MODE_STARTED 277 || currentMode.mListenMode != LOCATION_LISTEN_MODE_ACTIVE) { 278 String unexpectedStateDebugInfo = "Unexpected call to onActiveListeningResult()," 279 + " activeListeningResult=" + activeListeningResult 280 + ", currentMode=" + currentMode; 281 reportUnexpectedLocationCallback(unexpectedStateDebugInfo); 282 return; 283 } 284 285 PiiLoggable debugInfo = PiiLoggables.fromTemplate("onActiveListeningResult()," 286 + " activeListeningResult=%s", activeListeningResult); 287 logDebug(debugInfo); 288 289 // Recover any active listening budget we didn't use. 290 Duration timeListening = activeListeningResult.getTotalEstimatedTimeListening(); 291 Duration activeListeningDuration = 292 activeListeningResult.getRequestedListeningDuration(); 293 Duration activeListeningDurationNotUsed = activeListeningDuration.minus(timeListening); 294 if (!activeListeningDurationNotUsed.isNegative()) { 295 mLocationListeningAccountant.depositActiveListeningAmount( 296 activeListeningDurationNotUsed); 297 } 298 299 // Handle the result. 300 if (activeListeningResult.isLocationKnown()) { 301 handleLocationKnown(activeListeningResult); 302 } else { 303 handleLocationNotKnown(activeListeningResult); 304 } 305 306 // Active listening returns only a single location and self-cancels so we need to start 307 // listening again. 308 startNextLocationListening(debugInfo); 309 } 310 } 311 312 /** 313 * Accepts the current location from passive listening. 314 */ onPassiveListeningResult(@onNull LocationListeningResult passiveListeningResult)315 private void onPassiveListeningResult(@NonNull LocationListeningResult passiveListeningResult) { 316 synchronized (mLock) { 317 Mode currentMode = mCurrentMode.get(); 318 if (currentMode.mModeEnum != MODE_STARTED 319 || currentMode.mListenMode != LOCATION_LISTEN_MODE_PASSIVE) { 320 String unexpectedStateDebugInfo = "Unexpected call to onPassiveListeningResult()," 321 + " passiveListeningResult=" + passiveListeningResult 322 + ", currentMode=" + currentMode; 323 reportUnexpectedLocationCallback(unexpectedStateDebugInfo); 324 return; 325 } 326 logDebug("onPassiveListeningResult()" 327 + ", passiveListeningResult=" + passiveListeningResult); 328 329 handleLocationKnown(passiveListeningResult); 330 } 331 } 332 333 @GuardedBy("mLock") handleLocationKnown(@onNull LocationListeningResult locationResult)334 private void handleLocationKnown(@NonNull LocationListeningResult locationResult) { 335 Objects.requireNonNull(locationResult); 336 Objects.requireNonNull(locationResult.getLocation()); 337 338 mLastLocationListeningResult.set(locationResult); 339 340 // Receiving a location means we will definitely send a suggestion, so the initialization 341 // timeout is not required. This is a no-op if the initialization timeout is already 342 // cancelled. 343 cancelInitializationTimeout(); 344 345 Mode currentMode = mCurrentMode.get(); 346 PiiLoggable debugInfo = PiiLoggables.fromTemplate( 347 "handleLocationKnown(), locationResult=%s" 348 + ", currentMode.mListenMode=" + prettyPrintListenModeEnum(currentMode.mListenMode), 349 locationResult); 350 logDebug(debugInfo); 351 352 try { 353 sendTimeZoneCertainResultIfNeeded(locationResult.getLocation()); 354 } catch (IOException e) { 355 // This should never happen. 356 PiiLoggable lookupFailureDebugInfo = PiiLoggables.fromTemplate( 357 "IOException while looking up location. previous debugInfo=%s", debugInfo); 358 logWarn(lookupFailureDebugInfo, e); 359 360 enterFailedMode(new IOException(lookupFailureDebugInfo.toString(), e), 361 lookupFailureDebugInfo); 362 } 363 } 364 365 /** 366 * Handles an explicit location not known. This can only happen with active listening; passive 367 * only returns non-null locations. 368 */ 369 @GuardedBy("mLock") handleLocationNotKnown(@onNull LocationListeningResult locationResult)370 private void handleLocationNotKnown(@NonNull LocationListeningResult locationResult) { 371 Objects.requireNonNull(locationResult); 372 if (locationResult.isLocationKnown()) { 373 throw new IllegalArgumentException(); 374 } 375 376 mLastLocationListeningResult.set(locationResult); 377 378 Mode currentMode = mCurrentMode.get(); 379 String debugInfo = "handleLocationNotKnown()" 380 + ", currentMode.mListenMode=" + prettyPrintListenModeEnum(currentMode.mListenMode); 381 logDebug(debugInfo); 382 383 sendTimeZoneUncertainResultIfNeeded(); 384 } 385 386 /** 387 * Handles a passive listening period ending naturally, i.e. not cancelled. 388 * 389 * @param duration the duration that listening took place for 390 */ onPassiveListeningEnded(@onNull Duration duration)391 private void onPassiveListeningEnded(@NonNull Duration duration) { 392 PiiLoggable debugInfo = PiiLoggables.fromString( 393 "onPassiveListeningEnded(), duration=" + duration); 394 logDebug(debugInfo); 395 396 synchronized (mLock) { 397 Mode currentMode = mCurrentMode.get(); 398 if (currentMode.mModeEnum != MODE_STARTED 399 || currentMode.mListenMode != LOCATION_LISTEN_MODE_PASSIVE) { 400 reportUnexpectedLocationCallback("Unexpected call to onPassiveListeningEnded()" 401 + ", currentMode=" + currentMode); 402 return; 403 } 404 405 // Track how long passive listening took place since this is what allows us to 406 // actively listen. 407 mLocationListeningAccountant.accrueActiveListeningBudget(duration); 408 409 // Begin the next period of listening. 410 startNextLocationListening(debugInfo); 411 } 412 } 413 414 /** 415 * Handles the timeout callback that fires when initialization period has elapsed without a 416 * location being detected. 417 */ onInitializationTimeout(@onNull String timeoutToken)418 private void onInitializationTimeout(@NonNull String timeoutToken) { 419 synchronized (mLock) { 420 Mode currentMode = mCurrentMode.get(); 421 String debugInfo = "onInitializationTimeout() timeoutToken=" + timeoutToken 422 + ", currentMode=" + currentMode; 423 logDebug(debugInfo); 424 425 mInitializationTimeoutCancellable = null; 426 427 // If the initialization timeout has been allowed to trigger without being cancelled 428 // then that should mean no location has been detected during the initialization period 429 // and the provider must declare it is uncertain. 430 if (mLastTimeZoneProviderResult.get() == null) { 431 TimeZoneProviderResult result = TimeZoneProviderResult.createUncertain(); 432 reportTimeZoneProviderResultInternal(result, null /* locationToken */); 433 } 434 } 435 } 436 437 /** Cancels the initialization timeout, if it is still set. */ 438 @GuardedBy("mLock") cancelInitializationTimeout()439 private void cancelInitializationTimeout() { 440 if (mInitializationTimeoutCancellable != null) { 441 mInitializationTimeoutCancellable.cancel(); 442 mInitializationTimeoutCancellable = null; 443 } 444 } 445 446 @GuardedBy("mLock") sendTimeZoneCertainResultIfNeeded(@onNull Location location)447 private void sendTimeZoneCertainResultIfNeeded(@NonNull Location location) 448 throws IOException { 449 try (GeoTimeZonesFinder geoTimeZonesFinder = mEnvironment.createGeoTimeZoneFinder()) { 450 // Convert the location to a LocationToken. 451 LocationToken locationToken = geoTimeZonesFinder.createLocationTokenForLatLng( 452 location.getLatitude(), location.getLongitude()); 453 454 // If the location token is the same as the last lookup, there is no need to do the 455 // lookup / send another suggestion. 456 if (locationToken.equals(mLastLocationToken)) { 457 logDebug("Location token has not changed."); 458 } else { 459 List<String> tzIds = 460 geoTimeZonesFinder.findTimeZonesForLocationToken(locationToken); 461 logDebug("tzIds found for locationToken=" + locationToken + ", tzIds=" + tzIds); 462 // Rather than use the current elapsed realtime clock, use the time associated with 463 // the location since that gives a more accurate answer. 464 long elapsedRealtimeMillis = 465 NANOSECONDS.toMillis(location.getElapsedRealtimeNanos()); 466 TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder() 467 .setTimeZoneIds(tzIds) 468 .setElapsedRealtimeMillis(elapsedRealtimeMillis) 469 .build(); 470 471 TimeZoneProviderResult result = 472 TimeZoneProviderResult.createSuggestion(suggestion); 473 reportTimeZoneProviderResultInternal(result, locationToken); 474 } 475 } 476 } 477 478 @GuardedBy("mLock") sendTimeZoneUncertainResultIfNeeded()479 private void sendTimeZoneUncertainResultIfNeeded() { 480 TimeZoneProviderResult lastResult = mLastTimeZoneProviderResult.get(); 481 482 if (mInitializationTimeoutCancellable != null) { 483 // If we're within the initialization timeout period, then the provider doesn't report 484 // uncertainty. When the initialization timeout triggers, then an uncertain suggestion 485 // will be sent if it's needed. 486 return; 487 } 488 489 // If the last result was uncertain, there is no need to send another. 490 if (lastResult == null 491 || lastResult.getType() != TimeZoneProviderResult.RESULT_TYPE_UNCERTAIN) { 492 TimeZoneProviderResult result = TimeZoneProviderResult.createUncertain(); 493 reportTimeZoneProviderResultInternal(result, null /* locationToken */); 494 } else { 495 logDebug("sendTimeZoneUncertainResultIfNeeded(): Last result=" + lastResult 496 + ", no need to send another."); 497 } 498 } 499 500 @GuardedBy("mLock") sendPermanentFailureResult(@onNull Throwable cause)501 private void sendPermanentFailureResult(@NonNull Throwable cause) { 502 TimeZoneProviderResult result = TimeZoneProviderResult.createPermanentFailure(cause); 503 reportTimeZoneProviderResultInternal(result, null /* locationToken */); 504 } 505 506 @GuardedBy("mLock") reportTimeZoneProviderResultInternal( @onNull TimeZoneProviderResult result, @Nullable LocationToken locationToken)507 private void reportTimeZoneProviderResultInternal( 508 @NonNull TimeZoneProviderResult result, 509 @Nullable LocationToken locationToken) { 510 mLastTimeZoneProviderResult.set(result); 511 mLastLocationToken = locationToken; 512 mEnvironment.reportTimeZoneProviderResult(result); 513 } 514 515 @GuardedBy("mLock") clearLocationState()516 private void clearLocationState() { 517 mLastLocationListeningResult.set(null); 518 mLastLocationToken = null; 519 mLastTimeZoneProviderResult.set(null); 520 } 521 522 /** Called when leaving the current mode to cancel all pending asynchronous operations. */ 523 @GuardedBy("mLock") cancelTimeoutsAndLocationCallbacks()524 private void cancelTimeoutsAndLocationCallbacks() { 525 cancelInitializationTimeout(); 526 527 Mode currentMode = mCurrentMode.get(); 528 currentMode.cancelLocationListening(); 529 } 530 531 @GuardedBy("mLock") reportUnexpectedLocationCallback(@onNull String debugInfo)532 private void reportUnexpectedLocationCallback(@NonNull String debugInfo) { 533 // Unexpected location callbacks can occur when location listening is cancelled, but a 534 // location is already available (e.g. the callback is already invoked but blocked or 535 // sitting in a handler queue). This is logged but generally ignored. 536 logDebug(debugInfo); 537 } 538 539 @GuardedBy("mLock") enterStartedMode( @onNull Duration initializationTimeout, @NonNull PiiLoggable entryCause)540 private void enterStartedMode( 541 @NonNull Duration initializationTimeout, @NonNull PiiLoggable entryCause) { 542 Objects.requireNonNull(initializationTimeout); 543 Objects.requireNonNull(entryCause); 544 545 // The request contains the initialization time in which the LTZP is given to provide the 546 // first result. We set a timeout to try to ensure that we do send a result. 547 String initializationToken = "initialization:" + initializationTimeout + "@" 548 + formatElapsedRealtimeMillis(mEnvironment.elapsedRealtimeMillis()); 549 mInitializationTimeoutCancellable = mEnvironment.requestDelayedCallback( 550 this::onInitializationTimeout, initializationToken, 551 initializationTimeout); 552 553 startNextLocationListening(entryCause); 554 } 555 556 @GuardedBy("mLock") enterFailedMode(@onNull Throwable failure, @NonNull PiiLoggable entryCause)557 private void enterFailedMode(@NonNull Throwable failure, @NonNull PiiLoggable entryCause) { 558 logDebug(entryCause); 559 560 cancelTimeoutsAndLocationCallbacks(); 561 562 // Avoid a transition from MODE_DESTROYED -> MODE_FAILED. 563 if (mCurrentMode.get().mModeEnum != MODE_DESTROYED) { 564 sendPermanentFailureResult(failure); 565 566 Mode newMode = new Mode(MODE_FAILED, entryCause); 567 mCurrentMode.set(newMode); 568 } 569 } 570 571 @GuardedBy("mLock") enterStoppedMode(@onNull PiiLoggable entryCause)572 private void enterStoppedMode(@NonNull PiiLoggable entryCause) { 573 logDebug("Provider entering stopped mode, entryCause=" + entryCause); 574 575 cancelTimeoutsAndLocationCallbacks(); 576 577 // Clear all location-derived state. The provider may be stopped due to the current user 578 // changing. 579 clearLocationState(); 580 581 Mode newMode = new Mode(MODE_STOPPED, entryCause); 582 mCurrentMode.set(newMode); 583 } 584 585 @GuardedBy("mLock") startNextLocationListening(@onNull PiiLoggable entryCause)586 private void startNextLocationListening(@NonNull PiiLoggable entryCause) { 587 logDebug("Provider entering location listening mode entryCause=" + entryCause); 588 589 Mode currentMode = mCurrentMode.get(); 590 // This method is safe to call on any mode, even when the mode doesn't use it. 591 currentMode.cancelLocationListening(); 592 593 // Obtain the instruction for what mode to use. 594 ListeningInstruction nextModeInstruction; 595 try { 596 // Hold a wake lock to prevent doze while the accountant does elapsed realtime millis 597 // calculations for things like last location result age, etc. and start the next 598 // period of listening. 599 mEnvironment.acquireWakeLock(); 600 601 long elapsedRealtimeMillis = mEnvironment.elapsedRealtimeMillis(); 602 nextModeInstruction = mLocationListeningAccountant.getNextListeningInstruction( 603 elapsedRealtimeMillis, mLastLocationListeningResult.get()); 604 605 Cancellable listeningCancellable; 606 if (nextModeInstruction.listenMode == LOCATION_LISTEN_MODE_PASSIVE) { 607 listeningCancellable = mEnvironment.startPassiveLocationListening( 608 nextModeInstruction.duration, 609 this::onPassiveListeningResult, 610 this::onPassiveListeningEnded); 611 } else { 612 listeningCancellable = mEnvironment.startActiveGetCurrentLocation( 613 nextModeInstruction.duration, this::onActiveListeningResult); 614 } 615 Mode newMode = new Mode( 616 MODE_STARTED, entryCause, nextModeInstruction.listenMode, listeningCancellable); 617 mCurrentMode.set(newMode); 618 } finally { 619 mEnvironment.releaseWakeLock(); 620 } 621 } 622 } 623