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 @TimeZoneConfidence int currentConfidence = getTimeZoneConfidence(); 137 if (currentTimeZoneId == null || !currentTimeZoneId.equals(timeZoneId)) { 138 SystemProperties.set(TIME_ZONE_SYSTEM_PROPERTY, timeZoneId); 139 if (DEBUG) { 140 Slog.v(TAG, "Time zone changed: " + currentTimeZoneId + ", new=" + timeZoneId); 141 } 142 timeZoneChanged = true; 143 } 144 boolean timeZoneConfidenceChanged = setTimeZoneConfidence(confidence); 145 if (timeZoneChanged || timeZoneConfidenceChanged) { 146 String logMsg = "Time zone or confidence set: " 147 + " (new) timeZoneId=" + timeZoneId 148 + ", (new) confidence=" + confidence 149 + ", (old) timeZoneId=" + currentTimeZoneId 150 + ", (old) confidence=" + currentConfidence 151 + ", logInfo=" + logInfo; 152 addDebugLogEntry(logMsg); 153 } 154 } 155 156 return timeZoneChanged; 157 } 158 159 /** 160 * Sets the time zone confidence value if required. See {@link TimeZoneConfidence} for details. 161 */ setTimeZoneConfidence(@imeZoneConfidence int newConfidence)162 private static boolean setTimeZoneConfidence(@TimeZoneConfidence int newConfidence) { 163 int currentConfidence = getTimeZoneConfidence(); 164 if (currentConfidence != newConfidence) { 165 SystemProperties.set( 166 TIME_ZONE_CONFIDENCE_SYSTEM_PROPERTY, Integer.toString(newConfidence)); 167 if (DEBUG) { 168 Slog.v(TAG, "Time zone confidence changed: old=" + currentConfidence 169 + ", newConfidence=" + newConfidence); 170 } 171 return true; 172 } 173 return false; 174 } 175 176 /** Returns the time zone confidence value. See {@link TimeZoneConfidence} for details. */ getTimeZoneConfidence()177 public static @TimeZoneConfidence int getTimeZoneConfidence() { 178 int confidence = SystemProperties.getInt( 179 TIME_ZONE_CONFIDENCE_SYSTEM_PROPERTY, TIME_ZONE_CONFIDENCE_LOW); 180 if (!isValidTimeZoneConfidence(confidence)) { 181 confidence = TIME_ZONE_CONFIDENCE_LOW; 182 } 183 return confidence; 184 } 185 186 /** Returns the device's time zone ID setting. */ getTimeZoneId()187 public static String getTimeZoneId() { 188 return SystemProperties.get(TIME_ZONE_SYSTEM_PROPERTY); 189 } 190 191 /** 192 * Dumps information about recent time zone decisions / changes to the supplied writer. 193 */ dump(PrintWriter writer)194 public static void dump(PrintWriter writer) { 195 sTimeZoneDebugLog.dump(writer); 196 } 197 isValidTimeZoneConfidence(@imeZoneConfidence int confidence)198 private static boolean isValidTimeZoneConfidence(@TimeZoneConfidence int confidence) { 199 return confidence >= TIME_ZONE_CONFIDENCE_LOW && confidence <= TIME_ZONE_CONFIDENCE_HIGH; 200 } 201 isValidTimeZoneId(String timeZoneId)202 private static boolean isValidTimeZoneId(String timeZoneId) { 203 return timeZoneId != null 204 && !timeZoneId.isEmpty() 205 && ZoneInfoDb.getInstance().hasTimeZone(timeZoneId); 206 } 207 } 208