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