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