• 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}.
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 }