blob: 5db12c7ac872389f246886876493cd907111629f [file] [log] [blame]
Neil Fuller3352cfc2019-11-07 15:35:05 +00001/*
2 * Copyright 2019 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 */
16package com.android.server.timezonedetector;
17
18import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID;
19import static android.app.timezonedetector.PhoneTimeZoneSuggestion.MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY;
20import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
21import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
22import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
23
Neil Fuller2c6d5102019-11-29 09:02:39 +000024import android.annotation.IntDef;
Neil Fuller3352cfc2019-11-07 15:35:05 +000025import android.annotation.NonNull;
26import android.annotation.Nullable;
Neil Fuller2c6d5102019-11-29 09:02:39 +000027import android.app.timezonedetector.ManualTimeZoneSuggestion;
Neil Fuller3352cfc2019-11-07 15:35:05 +000028import android.app.timezonedetector.PhoneTimeZoneSuggestion;
29import android.content.Context;
30import android.util.ArrayMap;
31import android.util.LocalLog;
32import android.util.Slog;
33
34import com.android.internal.annotations.GuardedBy;
35import com.android.internal.annotations.VisibleForTesting;
36import com.android.internal.util.IndentingPrintWriter;
37
38import java.io.PrintWriter;
Neil Fuller2c6d5102019-11-29 09:02:39 +000039import java.lang.annotation.Retention;
40import java.lang.annotation.RetentionPolicy;
Neil Fuller3352cfc2019-11-07 15:35:05 +000041import java.util.LinkedList;
42import java.util.Map;
43import java.util.Objects;
44
45/**
Neil Fuller2c6d5102019-11-29 09:02:39 +000046 * A singleton, stateful time zone detection strategy that is aware of user (manual) suggestions and
47 * suggestions from multiple phone devices. Suggestions are acted on or ignored as needed, dependent
48 * on the current "auto time zone detection" setting.
49 *
50 * <p>For automatic detection it keeps track of the most recent suggestion from each phone it uses
51 * the best suggestion based on a scoring algorithm. If several phones provide the same score then
52 * the phone with the lowest numeric ID "wins". If the situation changes and it is no longer
53 * possible to be confident about the time zone, phones must submit an empty suggestion in order to
54 * "withdraw" their previous suggestion.
Neil Fuller3352cfc2019-11-07 15:35:05 +000055 */
56public class TimeZoneDetectorStrategy {
57
58 /**
59 * Used by {@link TimeZoneDetectorStrategy} to interact with the surrounding service. It can be
60 * faked for tests.
Neil Fuller2c6d5102019-11-29 09:02:39 +000061 *
62 * <p>Note: Because the system properties-derived values like
63 * {@link #isAutoTimeZoneDetectionEnabled()}, {@link #isAutoTimeZoneDetectionEnabled()},
64 * {@link #getDeviceTimeZone()} can be modified independently and from different threads (and
65 * processes!), their use are prone to race conditions. That will be true until the
66 * responsibility for setting their values is moved to {@link TimeZoneDetectorStrategy}.
Neil Fuller3352cfc2019-11-07 15:35:05 +000067 */
68 @VisibleForTesting
69 public interface Callback {
70
71 /**
72 * Returns true if automatic time zone detection is enabled in settings.
73 */
Neil Fuller2c6d5102019-11-29 09:02:39 +000074 boolean isAutoTimeZoneDetectionEnabled();
Neil Fuller3352cfc2019-11-07 15:35:05 +000075
76 /**
77 * Returns true if the device has had an explicit time zone set.
78 */
79 boolean isDeviceTimeZoneInitialized();
80
81 /**
82 * Returns the device's currently configured time zone.
83 */
84 String getDeviceTimeZone();
85
86 /**
87 * Sets the device's time zone.
88 */
Neil Fuller2c6d5102019-11-29 09:02:39 +000089 void setDeviceTimeZone(@NonNull String zoneId, boolean sendNetworkBroadcast);
Neil Fuller3352cfc2019-11-07 15:35:05 +000090 }
91
Neil Fuller2c6d5102019-11-29 09:02:39 +000092 private static final String LOG_TAG = "TimeZoneDetectorStrategy";
93 private static final boolean DBG = false;
94
95 @IntDef({ ORIGIN_PHONE, ORIGIN_MANUAL })
96 @Retention(RetentionPolicy.SOURCE)
97 public @interface Origin {}
98
99 /** Used when a time value originated from a telephony signal. */
100 @Origin
101 private static final int ORIGIN_PHONE = 1;
102
103 /** Used when a time value originated from a user / manual settings. */
104 @Origin
105 private static final int ORIGIN_MANUAL = 2;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000106
107 /**
Neil Fuller2c6d5102019-11-29 09:02:39 +0000108 * The abstract score for an empty or invalid phone suggestion.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000109 *
Neil Fuller2c6d5102019-11-29 09:02:39 +0000110 * Used to score phone suggestions where there is no zone.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000111 */
112 @VisibleForTesting
Neil Fuller2c6d5102019-11-29 09:02:39 +0000113 public static final int PHONE_SCORE_NONE = 0;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000114
115 /**
Neil Fuller2c6d5102019-11-29 09:02:39 +0000116 * The abstract score for a low quality phone suggestion.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000117 *
118 * Used to score suggestions where:
119 * The suggested zone ID is one of several possibilities, and the possibilities have different
120 * offsets.
121 *
122 * You would have to be quite desperate to want to use this choice.
123 */
124 @VisibleForTesting
Neil Fuller2c6d5102019-11-29 09:02:39 +0000125 public static final int PHONE_SCORE_LOW = 1;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000126
127 /**
Neil Fuller2c6d5102019-11-29 09:02:39 +0000128 * The abstract score for a medium quality phone suggestion.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000129 *
130 * Used for:
131 * The suggested zone ID is one of several possibilities but at least the possibilities have the
132 * same offset. Users would get the correct time but for the wrong reason. i.e. their device may
133 * switch to DST at the wrong time and (for example) their calendar events.
134 */
135 @VisibleForTesting
Neil Fuller2c6d5102019-11-29 09:02:39 +0000136 public static final int PHONE_SCORE_MEDIUM = 2;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000137
138 /**
Neil Fuller2c6d5102019-11-29 09:02:39 +0000139 * The abstract score for a high quality phone suggestion.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000140 *
141 * Used for:
142 * The suggestion was for one zone ID and the answer was unambiguous and likely correct given
143 * the info available.
144 */
145 @VisibleForTesting
Neil Fuller2c6d5102019-11-29 09:02:39 +0000146 public static final int PHONE_SCORE_HIGH = 3;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000147
148 /**
Neil Fuller2c6d5102019-11-29 09:02:39 +0000149 * The abstract score for a highest quality phone suggestion.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000150 *
151 * Used for:
152 * Suggestions that must "win" because they constitute test or emulator zone ID.
153 */
154 @VisibleForTesting
Neil Fuller2c6d5102019-11-29 09:02:39 +0000155 public static final int PHONE_SCORE_HIGHEST = 4;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000156
Neil Fuller2c6d5102019-11-29 09:02:39 +0000157 /**
158 * The threshold at which phone suggestions are good enough to use to set the device's time
159 * zone.
160 */
Neil Fuller3352cfc2019-11-07 15:35:05 +0000161 @VisibleForTesting
Neil Fuller2c6d5102019-11-29 09:02:39 +0000162 public static final int PHONE_SCORE_USAGE_THRESHOLD = PHONE_SCORE_MEDIUM;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000163
164 /** The number of previous phone suggestions to keep for each ID (for use during debugging). */
Neil Fuller2c6d5102019-11-29 09:02:39 +0000165 private static final int KEEP_PHONE_SUGGESTION_HISTORY_SIZE = 30;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000166
167 @NonNull
168 private final Callback mCallback;
169
170 /**
171 * A log that records the decisions / decision metadata that affected the device's time zone
172 * (for use during debugging).
173 */
174 @NonNull
175 private final LocalLog mTimeZoneChangesLog = new LocalLog(30);
176
177 /**
Neil Fuller2c6d5102019-11-29 09:02:39 +0000178 * A mapping from phoneId to a linked list of phone time zone suggestions (the head being the
179 * latest). We typically expect one or two entries in this Map: devices will have a small number
Neil Fuller3352cfc2019-11-07 15:35:05 +0000180 * of telephony devices and phoneIds are assumed to be stable. The LinkedList associated with
Neil Fuller2c6d5102019-11-29 09:02:39 +0000181 * the ID will not exceed {@link #KEEP_PHONE_SUGGESTION_HISTORY_SIZE} in size.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000182 */
183 @GuardedBy("this")
184 private ArrayMap<Integer, LinkedList<QualifiedPhoneTimeZoneSuggestion>> mSuggestionByPhoneId =
185 new ArrayMap<>();
186
187 /**
Neil Fuller3352cfc2019-11-07 15:35:05 +0000188 * Creates a new instance of {@link TimeZoneDetectorStrategy}.
189 */
190 public static TimeZoneDetectorStrategy create(Context context) {
191 Callback timeZoneDetectionServiceHelper = new TimeZoneDetectorCallbackImpl(context);
192 return new TimeZoneDetectorStrategy(timeZoneDetectionServiceHelper);
193 }
194
195 @VisibleForTesting
196 public TimeZoneDetectorStrategy(Callback callback) {
197 mCallback = Objects.requireNonNull(callback);
198 }
199
Neil Fuller2c6d5102019-11-29 09:02:39 +0000200 /** Process the suggested manually- / user-entered time zone. */
201 public synchronized void suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion suggestion) {
202 Objects.requireNonNull(suggestion);
203
204 String timeZoneId = suggestion.getZoneId();
205 String cause = "Manual time suggestion received: suggestion=" + suggestion;
206 setDeviceTimeZoneIfRequired(ORIGIN_MANUAL, timeZoneId, cause);
207 }
208
Neil Fuller3352cfc2019-11-07 15:35:05 +0000209 /**
210 * Suggests a time zone for the device, or withdraws a previous suggestion if
211 * {@link PhoneTimeZoneSuggestion#getZoneId()} is {@code null}. The suggestion is scoped to a
212 * specific {@link PhoneTimeZoneSuggestion#getPhoneId() phone}.
213 * See {@link PhoneTimeZoneSuggestion} for an explanation of the metadata associated with a
Neil Fuller2c6d5102019-11-29 09:02:39 +0000214 * suggestion. The strategy uses suggestions to decide whether to modify the device's time zone
Neil Fuller3352cfc2019-11-07 15:35:05 +0000215 * setting and what to set it to.
216 */
Neil Fuller2c6d5102019-11-29 09:02:39 +0000217 public synchronized void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion) {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000218 if (DBG) {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000219 Slog.d(LOG_TAG, "Phone suggestion received. newSuggestion=" + suggestion);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000220 }
Neil Fuller2c6d5102019-11-29 09:02:39 +0000221 Objects.requireNonNull(suggestion);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000222
Neil Fuller2c6d5102019-11-29 09:02:39 +0000223 // Score the suggestion.
224 int score = scorePhoneSuggestion(suggestion);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000225 QualifiedPhoneTimeZoneSuggestion scoredSuggestion =
Neil Fuller2c6d5102019-11-29 09:02:39 +0000226 new QualifiedPhoneTimeZoneSuggestion(suggestion, score);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000227
Neil Fuller2c6d5102019-11-29 09:02:39 +0000228 // Store the suggestion against the correct phoneId.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000229 LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions =
Neil Fuller2c6d5102019-11-29 09:02:39 +0000230 mSuggestionByPhoneId.get(suggestion.getPhoneId());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000231 if (suggestions == null) {
232 suggestions = new LinkedList<>();
Neil Fuller2c6d5102019-11-29 09:02:39 +0000233 mSuggestionByPhoneId.put(suggestion.getPhoneId(), suggestions);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000234 }
235 suggestions.addFirst(scoredSuggestion);
Neil Fuller2c6d5102019-11-29 09:02:39 +0000236 if (suggestions.size() > KEEP_PHONE_SUGGESTION_HISTORY_SIZE) {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000237 suggestions.removeLast();
238 }
239
Neil Fuller2c6d5102019-11-29 09:02:39 +0000240 // Now perform auto time zone detection. The new suggestion may be used to modify the time
241 // zone setting.
242 String reason = "New phone time suggested. suggestion=" + suggestion;
243 doAutoTimeZoneDetection(reason);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000244 }
245
Neil Fuller2c6d5102019-11-29 09:02:39 +0000246 private static int scorePhoneSuggestion(@NonNull PhoneTimeZoneSuggestion suggestion) {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000247 int score;
248 if (suggestion.getZoneId() == null) {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000249 score = PHONE_SCORE_NONE;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000250 } else if (suggestion.getMatchType() == MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY
251 || suggestion.getMatchType() == MATCH_TYPE_EMULATOR_ZONE_ID) {
252 // Handle emulator / test cases : These suggestions should always just be used.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000253 score = PHONE_SCORE_HIGHEST;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000254 } else if (suggestion.getQuality() == QUALITY_SINGLE_ZONE) {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000255 score = PHONE_SCORE_HIGH;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000256 } else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET) {
257 // The suggestion may be wrong, but at least the offset should be correct.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000258 score = PHONE_SCORE_MEDIUM;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000259 } else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS) {
260 // The suggestion has a good chance of being wrong.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000261 score = PHONE_SCORE_LOW;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000262 } else {
263 throw new AssertionError();
264 }
265 return score;
266 }
267
268 /**
269 * Finds the best available time zone suggestion from all phones. If it is high-enough quality
270 * and automatic time zone detection is enabled then it will be set on the device. The outcome
Neil Fuller2c6d5102019-11-29 09:02:39 +0000271 * can be that this strategy becomes / remains un-opinionated and nothing is set.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000272 */
273 @GuardedBy("this")
Neil Fuller2c6d5102019-11-29 09:02:39 +0000274 private void doAutoTimeZoneDetection(@NonNull String detectionReason) {
275 if (!mCallback.isAutoTimeZoneDetectionEnabled()) {
276 // Avoid doing unnecessary work with this (race-prone) check.
277 return;
278 }
279
280 QualifiedPhoneTimeZoneSuggestion bestPhoneSuggestion = findBestPhoneSuggestion();
Neil Fuller3352cfc2019-11-07 15:35:05 +0000281
282 // Work out what to do with the best suggestion.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000283 if (bestPhoneSuggestion == null) {
284 // There is no phone suggestion available at all. Become un-opinionated.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000285 if (DBG) {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000286 Slog.d(LOG_TAG, "Could not determine time zone: No best phone suggestion."
287 + " detectionReason=" + detectionReason);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000288 }
Neil Fuller3352cfc2019-11-07 15:35:05 +0000289 return;
290 }
291
292 // Special case handling for uninitialized devices. This should only happen once.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000293 String newZoneId = bestPhoneSuggestion.suggestion.getZoneId();
Neil Fuller3352cfc2019-11-07 15:35:05 +0000294 if (newZoneId != null && !mCallback.isDeviceTimeZoneInitialized()) {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000295 String cause = "Device has no time zone set. Attempting to set the device to the best"
296 + " available suggestion."
297 + " bestPhoneSuggestion=" + bestPhoneSuggestion
298 + ", detectionReason=" + detectionReason;
299 Slog.i(LOG_TAG, cause);
300 setDeviceTimeZoneIfRequired(ORIGIN_PHONE, newZoneId, cause);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000301 return;
302 }
303
Neil Fuller2c6d5102019-11-29 09:02:39 +0000304 boolean suggestionGoodEnough = bestPhoneSuggestion.score >= PHONE_SCORE_USAGE_THRESHOLD;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000305 if (!suggestionGoodEnough) {
306 if (DBG) {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000307 Slog.d(LOG_TAG, "Best suggestion not good enough."
308 + " bestPhoneSuggestion=" + bestPhoneSuggestion
309 + ", detectionReason=" + detectionReason);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000310 }
Neil Fuller3352cfc2019-11-07 15:35:05 +0000311 return;
312 }
313
314 // Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time
315 // zone ID.
316 if (newZoneId == null) {
317 Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:"
Neil Fuller2c6d5102019-11-29 09:02:39 +0000318 + " bestPhoneSuggestion=" + bestPhoneSuggestion
319 + " detectionReason=" + detectionReason);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000320 return;
321 }
322
Neil Fuller2c6d5102019-11-29 09:02:39 +0000323 String zoneId = bestPhoneSuggestion.suggestion.getZoneId();
324 String cause = "Found good suggestion."
325 + ", bestPhoneSuggestion=" + bestPhoneSuggestion
326 + ", detectionReason=" + detectionReason;
327 setDeviceTimeZoneIfRequired(ORIGIN_PHONE, zoneId, cause);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000328 }
329
Neil Fuller2c6d5102019-11-29 09:02:39 +0000330 @GuardedBy("this")
331 private void setDeviceTimeZoneIfRequired(
332 @Origin int origin, @NonNull String newZoneId, @NonNull String cause) {
333 Objects.requireNonNull(newZoneId);
334 Objects.requireNonNull(cause);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000335
Neil Fuller2c6d5102019-11-29 09:02:39 +0000336 boolean sendNetworkBroadcast = (origin == ORIGIN_PHONE);
337 boolean isOriginAutomatic = isOriginAutomatic(origin);
338 if (isOriginAutomatic) {
339 if (!mCallback.isAutoTimeZoneDetectionEnabled()) {
340 if (DBG) {
341 Slog.d(LOG_TAG, "Auto time zone detection is not enabled."
342 + " origin=" + origin
343 + ", newZoneId=" + newZoneId
344 + ", cause=" + cause);
345 }
346 return;
347 }
348 } else {
349 if (mCallback.isAutoTimeZoneDetectionEnabled()) {
350 if (DBG) {
351 Slog.d(LOG_TAG, "Auto time zone detection is enabled."
352 + " origin=" + origin
353 + ", newZoneId=" + newZoneId
354 + ", cause=" + cause);
355 }
356 return;
357 }
Neil Fuller3352cfc2019-11-07 15:35:05 +0000358 }
359
Neil Fuller2c6d5102019-11-29 09:02:39 +0000360 String currentZoneId = mCallback.getDeviceTimeZone();
361
Neil Fuller3352cfc2019-11-07 15:35:05 +0000362 // Avoid unnecessary changes / intents.
363 if (newZoneId.equals(currentZoneId)) {
364 // No need to set the device time zone - the setting is already what we would be
365 // suggesting.
366 if (DBG) {
Neil Fuller2c6d5102019-11-29 09:02:39 +0000367 Slog.d(LOG_TAG, "No need to change the time zone;"
Neil Fuller3352cfc2019-11-07 15:35:05 +0000368 + " device is already set to the suggested zone."
Neil Fuller2c6d5102019-11-29 09:02:39 +0000369 + " origin=" + origin
370 + ", newZoneId=" + newZoneId
371 + ", cause=" + cause);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000372 }
373 return;
374 }
375
Neil Fuller2c6d5102019-11-29 09:02:39 +0000376 mCallback.setDeviceTimeZone(newZoneId, sendNetworkBroadcast);
377 String msg = "Set device time zone."
378 + " origin=" + origin
379 + ", currentZoneId=" + currentZoneId
380 + ", newZoneId=" + newZoneId
381 + ", sendNetworkBroadcast" + sendNetworkBroadcast
382 + ", cause=" + cause;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000383 if (DBG) {
384 Slog.d(LOG_TAG, msg);
385 }
386 mTimeZoneChangesLog.log(msg);
Neil Fuller2c6d5102019-11-29 09:02:39 +0000387 }
388
389 private static boolean isOriginAutomatic(@Origin int origin) {
390 return origin == ORIGIN_PHONE;
Neil Fuller3352cfc2019-11-07 15:35:05 +0000391 }
392
393 @GuardedBy("this")
394 @Nullable
Neil Fuller2c6d5102019-11-29 09:02:39 +0000395 private QualifiedPhoneTimeZoneSuggestion findBestPhoneSuggestion() {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000396 QualifiedPhoneTimeZoneSuggestion bestSuggestion = null;
397
398 // Iterate over the latest QualifiedPhoneTimeZoneSuggestion objects received for each phone
399 // and find the best. Note that we deliberately do not look at age: the caller can
400 // rate-limit so age is not a strong indicator of confidence. Instead, the callers are
401 // expected to withdraw suggestions they no longer have confidence in.
402 for (int i = 0; i < mSuggestionByPhoneId.size(); i++) {
403 LinkedList<QualifiedPhoneTimeZoneSuggestion> phoneSuggestions =
404 mSuggestionByPhoneId.valueAt(i);
405 if (phoneSuggestions == null) {
406 // Unexpected
407 continue;
408 }
409 QualifiedPhoneTimeZoneSuggestion candidateSuggestion = phoneSuggestions.getFirst();
410 if (candidateSuggestion == null) {
411 // Unexpected
412 continue;
413 }
414
415 if (bestSuggestion == null) {
416 bestSuggestion = candidateSuggestion;
417 } else if (candidateSuggestion.score > bestSuggestion.score) {
418 bestSuggestion = candidateSuggestion;
419 } else if (candidateSuggestion.score == bestSuggestion.score) {
420 // Tie! Use the suggestion with the lowest phoneId.
421 int candidatePhoneId = candidateSuggestion.suggestion.getPhoneId();
422 int bestPhoneId = bestSuggestion.suggestion.getPhoneId();
423 if (candidatePhoneId < bestPhoneId) {
424 bestSuggestion = candidateSuggestion;
425 }
426 }
427 }
428 return bestSuggestion;
429 }
430
431 /**
Neil Fuller2c6d5102019-11-29 09:02:39 +0000432 * Returns the current best phone suggestion. Not intended for general use: it is used during
433 * tests to check strategy behavior.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000434 */
435 @VisibleForTesting
436 @Nullable
Neil Fuller2c6d5102019-11-29 09:02:39 +0000437 public synchronized QualifiedPhoneTimeZoneSuggestion findBestPhoneSuggestionForTests() {
438 return findBestPhoneSuggestion();
Neil Fuller3352cfc2019-11-07 15:35:05 +0000439 }
440
441 /**
Neil Fuller2c6d5102019-11-29 09:02:39 +0000442 * Called when there has been a change to the automatic time zone detection setting.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000443 */
444 @VisibleForTesting
Neil Fuller2c6d5102019-11-29 09:02:39 +0000445 public synchronized void handleAutoTimeZoneDetectionChange() {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000446 if (DBG) {
447 Slog.d(LOG_TAG, "handleTimeZoneDetectionChange() called");
448 }
Neil Fuller2c6d5102019-11-29 09:02:39 +0000449 if (mCallback.isAutoTimeZoneDetectionEnabled()) {
Neil Fuller3352cfc2019-11-07 15:35:05 +0000450 // When the user enabled time zone detection, run the time zone detection and change the
451 // device time zone if possible.
Neil Fuller2c6d5102019-11-29 09:02:39 +0000452 String reason = "Auto time zone detection setting enabled.";
453 doAutoTimeZoneDetection(reason);
Neil Fuller3352cfc2019-11-07 15:35:05 +0000454 }
455 }
456
457 /**
Neil Fuller2c6d5102019-11-29 09:02:39 +0000458 * Dumps internal state such as field values.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000459 */
Neil Fuller2c6d5102019-11-29 09:02:39 +0000460 public synchronized void dumpState(PrintWriter pw, String[] args) {
461 pw.println("TimeZoneDetectorStrategy:");
462 pw.println("mCallback.isTimeZoneDetectionEnabled()="
463 + mCallback.isAutoTimeZoneDetectionEnabled());
464 pw.println("mCallback.isDeviceTimeZoneInitialized()="
465 + mCallback.isDeviceTimeZoneInitialized());
466 pw.println("mCallback.getDeviceTimeZone()="
467 + mCallback.getDeviceTimeZone());
Neil Fuller3352cfc2019-11-07 15:35:05 +0000468
Neil Fuller2c6d5102019-11-29 09:02:39 +0000469 IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
Neil Fuller3352cfc2019-11-07 15:35:05 +0000470 ipw.println("Time zone change log:");
471 ipw.increaseIndent(); // level 2
472 mTimeZoneChangesLog.dump(ipw);
473 ipw.decreaseIndent(); // level 2
474
475 ipw.println("Phone suggestion history:");
476 ipw.increaseIndent(); // level 2
477 for (Map.Entry<Integer, LinkedList<QualifiedPhoneTimeZoneSuggestion>> entry
478 : mSuggestionByPhoneId.entrySet()) {
479 ipw.println("Phone " + entry.getKey());
480
481 ipw.increaseIndent(); // level 3
482 for (QualifiedPhoneTimeZoneSuggestion suggestion : entry.getValue()) {
483 ipw.println(suggestion);
484 }
485 ipw.decreaseIndent(); // level 3
486 }
487 ipw.decreaseIndent(); // level 2
488 ipw.decreaseIndent(); // level 1
Neil Fuller2c6d5102019-11-29 09:02:39 +0000489 ipw.flush();
Neil Fuller3352cfc2019-11-07 15:35:05 +0000490
Neil Fuller3352cfc2019-11-07 15:35:05 +0000491 pw.flush();
492 }
493
494 /**
Neil Fuller2c6d5102019-11-29 09:02:39 +0000495 * A method used to inspect strategy state during tests. Not intended for general use.
Neil Fuller3352cfc2019-11-07 15:35:05 +0000496 */
497 @VisibleForTesting
498 public synchronized QualifiedPhoneTimeZoneSuggestion getLatestPhoneSuggestion(int phoneId) {
499 LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions =
500 mSuggestionByPhoneId.get(phoneId);
501 if (suggestions == null) {
502 return null;
503 }
504 return suggestions.getFirst();
505 }
506
507 /**
508 * A {@link PhoneTimeZoneSuggestion} with additional qualifying metadata.
509 */
510 @VisibleForTesting
511 public static class QualifiedPhoneTimeZoneSuggestion {
512
513 @VisibleForTesting
514 public final PhoneTimeZoneSuggestion suggestion;
515
516 /**
517 * The score the suggestion has been given. This can be used to rank against other
518 * suggestions of the same type.
519 */
520 @VisibleForTesting
521 public final int score;
522
523 @VisibleForTesting
524 public QualifiedPhoneTimeZoneSuggestion(PhoneTimeZoneSuggestion suggestion, int score) {
525 this.suggestion = suggestion;
526 this.score = score;
527 }
528
529 @Override
530 public boolean equals(Object o) {
531 if (this == o) {
532 return true;
533 }
534 if (o == null || getClass() != o.getClass()) {
535 return false;
536 }
537 QualifiedPhoneTimeZoneSuggestion that = (QualifiedPhoneTimeZoneSuggestion) o;
538 return score == that.score
539 && suggestion.equals(that.suggestion);
540 }
541
542 @Override
543 public int hashCode() {
544 return Objects.hash(score, suggestion);
545 }
546
547 @Override
548 public String toString() {
549 return "QualifiedPhoneTimeZoneSuggestion{"
550 + "suggestion=" + suggestion
551 + ", score=" + score
552 + '}';
553 }
554 }
555}