1 /*
2  * Copyright 2022 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 package com.android.server;
17 
18 import static java.lang.annotation.ElementType.TYPE_USE;
19 import static java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.os.SystemProperties;
24 import android.text.TextUtils;
25 import android.util.LocalLog;
26 import android.util.Slog;
27 
28 import com.android.i18n.timezone.ZoneInfoDb;
29 
30 import java.io.PrintWriter;
31 import java.lang.annotation.Retention;
32 import java.lang.annotation.Target;
33 
34 /**
35  * A set of constants and static methods that encapsulate knowledge of how time zone and associated
36  * metadata are stored on Android.
37  */
38 public final class SystemTimeZone {
39 
40     private static final String TAG = "SystemTimeZone";
41     private static final boolean DEBUG = false;
42     private static final String TIME_ZONE_SYSTEM_PROPERTY = "persist.sys.timezone";
43     private static final String TIME_ZONE_CONFIDENCE_SYSTEM_PROPERTY =
44             "persist.sys.timezone_confidence";
45 
46     /**
47      * The "special" time zone ID used as a low-confidence default when the device's time zone
48      * is empty or invalid during boot.
49      */
50     private static final String DEFAULT_TIME_ZONE_ID = "GMT";
51 
52     /**
53      * An annotation that indicates a "time zone confidence" value is expected.
54      *
55      * <p>The confidence indicates whether the time zone is expected to be correct. The confidence
56      * can be upgraded or downgraded over time. It can be used to decide whether a user could /
57      * should be asked to confirm the time zone. For example, during device set up low confidence
58      * would describe a time zone that has been initialized by default or by using low quality
59      * or ambiguous signals. The user may then be asked to confirm the time zone, moving it to a
60      * high confidence.
61      */
62     @Retention(SOURCE)
63     @Target(TYPE_USE)
64     @IntDef(prefix = "TIME_ZONE_CONFIDENCE_",
65             value = { TIME_ZONE_CONFIDENCE_LOW, TIME_ZONE_CONFIDENCE_HIGH })
66     public @interface TimeZoneConfidence {
67     }
68 
69     /** Used when confidence is low and would (ideally) be confirmed by a user. */
70     public static final @TimeZoneConfidence int TIME_ZONE_CONFIDENCE_LOW = 0;
71     /**
72      * Used when confidence in the time zone is high and does not need to be confirmed by a user.
73      */
74     public static final @TimeZoneConfidence int TIME_ZONE_CONFIDENCE_HIGH = 100;
75 
76     /**
77      * An in-memory log that records the debug info related to the device's time zone setting.
78      * This is logged in bug reports to assist with debugging time zone detection issues.
79      */
80     @NonNull
81     private static final LocalLog sTimeZoneDebugLog =
82             new LocalLog(30, false /* useLocalTimestamps */);
83 
SystemTimeZone()84     private SystemTimeZone() {}
85 
86     /**
87      * Called during device boot to validate and set the time zone ID to a low-confidence default.
88      */
initializeTimeZoneSettingsIfRequired()89     public static void initializeTimeZoneSettingsIfRequired() {
90         String timezoneProperty = SystemProperties.get(TIME_ZONE_SYSTEM_PROPERTY);
91         if (!isValidTimeZoneId(timezoneProperty)) {
92             String logInfo = "initializeTimeZoneSettingsIfRequired():" + TIME_ZONE_SYSTEM_PROPERTY
93                     + " is not valid (" + timezoneProperty + "); setting to "
94                     + DEFAULT_TIME_ZONE_ID;
95             Slog.w(TAG, logInfo);
96             setTimeZoneId(DEFAULT_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW, logInfo);
97         }
98     }
99 
100     /**
101      * Adds an entry to the system time zone debug log that is included in bug reports. This method
102      * is intended to be used to record event that may lead to a time zone change, e.g. config or
103      * mode changes.
104      */
addDebugLogEntry(@onNull String logMsg)105     public static void addDebugLogEntry(@NonNull String logMsg) {
106         sTimeZoneDebugLog.log(logMsg);
107     }
108 
109     /**
110      * Updates the device's time zone system property, associated metadata and adds an entry to the
111      * debug log. Returns {@code true} if the device's time zone changed, {@code false} if the ID is
112      * invalid or the device is already set to the supplied ID.
113      *
114      * <p>This method ensures the confidence metadata is set to the supplied value if the supplied
115      * time zone ID is considered valid.
116      *
117      * <p>This method is intended only for use by the AlarmManager. When changing the device's time
118      * zone other system service components must use {@link
119      * AlarmManagerInternal#setTimeZone(String, int, String)} to ensure that important
120      * system-wide side effects occur.
121      */
setTimeZoneId( @onNull String timeZoneId, @TimeZoneConfidence int confidence, @NonNull String logInfo)122     public static boolean setTimeZoneId(
123             @NonNull String timeZoneId, @TimeZoneConfidence int confidence,
124             @NonNull String logInfo) {
125         if (TextUtils.isEmpty(timeZoneId) || !isValidTimeZoneId(timeZoneId)) {
126             addDebugLogEntry("setTimeZoneId: Invalid time zone ID."
127                     + " timeZoneId=" + timeZoneId
128                     + ", confidence=" + confidence
129                     + ", logInfo=" + logInfo);
130             return false;
131         }
132 
133         boolean timeZoneChanged = false;
134         synchronized (SystemTimeZone.class) {
135             String currentTimeZoneId = getTimeZoneId();
136             if (currentTimeZoneId == null || !currentTimeZoneId.equals(timeZoneId)) {
137                 SystemProperties.set(TIME_ZONE_SYSTEM_PROPERTY, timeZoneId);
138                 if (DEBUG) {
139                     Slog.v(TAG, "Time zone changed: " + currentTimeZoneId + ", new=" + timeZoneId);
140                 }
141                 timeZoneChanged = true;
142             }
143             boolean timeZoneConfidenceChanged = setTimeZoneConfidence(confidence);
144             if (timeZoneChanged || timeZoneConfidenceChanged) {
145                 String logMsg = "Time zone or confidence set: "
146                         + " (new) timeZoneId=" + timeZoneId
147                         + ", (new) confidence=" + confidence
148                         + ", logInfo=" + logInfo;
149                 addDebugLogEntry(logMsg);
150             }
151         }
152 
153         return timeZoneChanged;
154     }
155 
156     /**
157      * Sets the time zone confidence value if required. See {@link TimeZoneConfidence} for details.
158      */
setTimeZoneConfidence(@imeZoneConfidence int newConfidence)159     private static boolean setTimeZoneConfidence(@TimeZoneConfidence int newConfidence) {
160         int currentConfidence = getTimeZoneConfidence();
161         if (currentConfidence != newConfidence) {
162             SystemProperties.set(
163                     TIME_ZONE_CONFIDENCE_SYSTEM_PROPERTY, Integer.toString(newConfidence));
164             if (DEBUG) {
165                 Slog.v(TAG, "Time zone confidence changed: old=" + currentConfidence
166                         + ", newConfidence=" + newConfidence);
167             }
168             return true;
169         }
170         return false;
171     }
172 
173     /** Returns the time zone confidence value. See {@link TimeZoneConfidence} for details. */
getTimeZoneConfidence()174     public static @TimeZoneConfidence int getTimeZoneConfidence() {
175         int confidence = SystemProperties.getInt(
176                 TIME_ZONE_CONFIDENCE_SYSTEM_PROPERTY, TIME_ZONE_CONFIDENCE_LOW);
177         if (!isValidTimeZoneConfidence(confidence)) {
178             confidence = TIME_ZONE_CONFIDENCE_LOW;
179         }
180         return confidence;
181     }
182 
183     /** Returns the device's time zone ID setting. */
getTimeZoneId()184     public static String getTimeZoneId() {
185         return SystemProperties.get(TIME_ZONE_SYSTEM_PROPERTY);
186     }
187 
188     /**
189      * Dumps information about recent time zone decisions / changes to the supplied writer.
190      */
dump(PrintWriter writer)191     public static void dump(PrintWriter writer) {
192         sTimeZoneDebugLog.dump(writer);
193     }
194 
isValidTimeZoneConfidence(@imeZoneConfidence int confidence)195     private static boolean isValidTimeZoneConfidence(@TimeZoneConfidence int confidence) {
196         return confidence >= TIME_ZONE_CONFIDENCE_LOW && confidence <= TIME_ZONE_CONFIDENCE_HIGH;
197     }
198 
isValidTimeZoneId(String timeZoneId)199     private static boolean isValidTimeZoneId(String timeZoneId) {
200         return timeZoneId != null
201                 && !timeZoneId.isEmpty()
202                 && ZoneInfoDb.getInstance().hasTimeZone(timeZoneId);
203     }
204 }
205