• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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