1 /* 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 */ 16 17 package android.app.timezonedetector; 18 19 import android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 import android.os.ShellCommand; 25 import android.text.TextUtils; 26 27 import java.io.PrintWriter; 28 import java.lang.annotation.Retention; 29 import java.lang.annotation.RetentionPolicy; 30 import java.util.ArrayList; 31 import java.util.Collections; 32 import java.util.List; 33 import java.util.Objects; 34 35 /** 36 * A time zone suggestion from an identified telephony source, e.g. from MCC and NITZ information 37 * associated with a specific radio. 38 * 39 * <p>{@code slotIndex} identifies the suggestion source. This enables detection logic to identify 40 * suggestions from the same source when there are several in use. 41 * 42 * <p>{@code zoneId}. When not {@code null}, {@code zoneId} contains the suggested time zone ID, 43 * e.g. "America/Los_Angeles". Suggestion metadata like {@code matchType} and {@code quality} can be 44 * used to judge quality / certainty of the suggestion. {@code zoneId} can be {@code null} to 45 * indicate that the telephony source has entered an "un-opinionated" state and any previous 46 * suggestion from the same source is being withdrawn. 47 * 48 * <p>{@code matchType} must be set to {@link #MATCH_TYPE_NA} when {@code zoneId} is {@code null}, 49 * and one of the other {@code MATCH_TYPE_} values when it is not {@code null}. 50 * 51 * <p>{@code quality} must be set to {@link #QUALITY_NA} when {@code zoneId} is {@code null}, 52 * and one of the other {@code QUALITY_} values when it is not {@code null}. 53 * 54 * <p>{@code debugInfo} contains debugging metadata associated with the suggestion. This is used to 55 * record why the suggestion exists, e.g. what triggered it to be made and what heuristic was used 56 * to determine the time zone or its absence. This information exists only to aid in debugging and 57 * therefore is used by {@link #toString()}, but it is not for use in detection logic and is not 58 * considered in {@link #hashCode()} or {@link #equals(Object)}. 59 * 60 * @hide 61 */ 62 public final class TelephonyTimeZoneSuggestion implements Parcelable { 63 64 /** @hide */ 65 @NonNull 66 public static final Creator<TelephonyTimeZoneSuggestion> CREATOR = 67 new Creator<TelephonyTimeZoneSuggestion>() { 68 public TelephonyTimeZoneSuggestion createFromParcel(Parcel in) { 69 return TelephonyTimeZoneSuggestion.createFromParcel(in); 70 } 71 72 public TelephonyTimeZoneSuggestion[] newArray(int size) { 73 return new TelephonyTimeZoneSuggestion[size]; 74 } 75 }; 76 77 /** 78 * Creates an empty time zone suggestion, i.e. one that will cancel previous suggestions with 79 * the same {@code slotIndex}. 80 */ 81 @NonNull createEmptySuggestion( int slotIndex, @NonNull String debugInfo)82 public static TelephonyTimeZoneSuggestion createEmptySuggestion( 83 int slotIndex, @NonNull String debugInfo) { 84 return new Builder(slotIndex).addDebugInfo(debugInfo).build(); 85 } 86 87 /** @hide */ 88 @IntDef({ MATCH_TYPE_NA, MATCH_TYPE_NETWORK_COUNTRY_ONLY, MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, 89 MATCH_TYPE_EMULATOR_ZONE_ID, MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY }) 90 @Retention(RetentionPolicy.SOURCE) 91 public @interface MatchType {} 92 93 /** Used when match type is not applicable. */ 94 public static final int MATCH_TYPE_NA = 0; 95 96 /** 97 * Only the network country is known. 98 */ 99 public static final int MATCH_TYPE_NETWORK_COUNTRY_ONLY = 2; 100 101 /** 102 * Both the network county and offset were known. 103 */ 104 public static final int MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET = 3; 105 106 /** 107 * The device is running in an emulator and an NITZ signal was simulated containing an 108 * Android extension with an explicit Olson ID. 109 */ 110 public static final int MATCH_TYPE_EMULATOR_ZONE_ID = 4; 111 112 /** 113 * The phone is most likely running in a test network not associated with a country (this is 114 * distinct from the country just not being known yet). 115 * Historically, Android has just picked an arbitrary time zone with the correct offset when 116 * on a test network. 117 */ 118 public static final int MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY = 5; 119 120 /** @hide */ 121 @IntDef({ QUALITY_NA, QUALITY_SINGLE_ZONE, QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, 122 QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS }) 123 @Retention(RetentionPolicy.SOURCE) 124 public @interface Quality {} 125 126 /** Used when quality is not applicable. */ 127 public static final int QUALITY_NA = 0; 128 129 /** There is only one answer */ 130 public static final int QUALITY_SINGLE_ZONE = 1; 131 132 /** 133 * There are multiple answers, but they all shared the same offset / DST state at the time 134 * the suggestion was created. i.e. it might be the wrong zone but the user won't notice 135 * immediately if it is wrong. 136 */ 137 public static final int QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET = 2; 138 139 /** 140 * There are multiple answers with different offsets. The one given is just one possible. 141 */ 142 public static final int QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS = 3; 143 144 private final int mSlotIndex; 145 @Nullable private final String mZoneId; 146 @MatchType private final int mMatchType; 147 @Quality private final int mQuality; 148 @Nullable private List<String> mDebugInfo; 149 TelephonyTimeZoneSuggestion(Builder builder)150 private TelephonyTimeZoneSuggestion(Builder builder) { 151 mSlotIndex = builder.mSlotIndex; 152 mZoneId = builder.mZoneId; 153 mMatchType = builder.mMatchType; 154 mQuality = builder.mQuality; 155 mDebugInfo = builder.mDebugInfo != null ? new ArrayList<>(builder.mDebugInfo) : null; 156 } 157 158 @SuppressWarnings("unchecked") createFromParcel(Parcel in)159 private static TelephonyTimeZoneSuggestion createFromParcel(Parcel in) { 160 // Use the Builder so we get validation during build(). 161 int slotIndex = in.readInt(); 162 TelephonyTimeZoneSuggestion suggestion = new Builder(slotIndex) 163 .setZoneId(in.readString()) 164 .setMatchType(in.readInt()) 165 .setQuality(in.readInt()) 166 .build(); 167 List<String> debugInfo = 168 in.readArrayList(TelephonyTimeZoneSuggestion.class.getClassLoader(), java.lang.String.class); 169 if (debugInfo != null) { 170 suggestion.addDebugInfo(debugInfo); 171 } 172 return suggestion; 173 } 174 175 @Override writeToParcel(@onNull Parcel dest, int flags)176 public void writeToParcel(@NonNull Parcel dest, int flags) { 177 dest.writeInt(mSlotIndex); 178 dest.writeString(mZoneId); 179 dest.writeInt(mMatchType); 180 dest.writeInt(mQuality); 181 dest.writeList(mDebugInfo); 182 } 183 184 @Override describeContents()185 public int describeContents() { 186 return 0; 187 } 188 189 /** 190 * Returns an identifier for the source of this suggestion. 191 * 192 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code slotIndex}. 193 */ getSlotIndex()194 public int getSlotIndex() { 195 return mSlotIndex; 196 } 197 198 /** 199 * Returns the suggested time zone Olson ID, e.g. "America/Los_Angeles". {@code null} means that 200 * the caller is no longer sure what the current time zone is. 201 * 202 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code zoneId}. 203 */ 204 @Nullable getZoneId()205 public String getZoneId() { 206 return mZoneId; 207 } 208 209 /** 210 * Returns information about how the suggestion was determined which could be used to rank 211 * suggestions when several are available from different sources. 212 * 213 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code matchType}. 214 */ 215 @MatchType getMatchType()216 public int getMatchType() { 217 return mMatchType; 218 } 219 220 /** 221 * Returns information about the likelihood of the suggested zone being correct. 222 * 223 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code quality}. 224 */ 225 @Quality getQuality()226 public int getQuality() { 227 return mQuality; 228 } 229 230 /** 231 * Returns debug metadata for the suggestion. 232 * 233 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code debugInfo}. 234 */ 235 @NonNull getDebugInfo()236 public List<String> getDebugInfo() { 237 return mDebugInfo == null 238 ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo); 239 } 240 241 /** 242 * Associates information with the instance that can be useful for debugging / logging. 243 * 244 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code debugInfo}. 245 */ addDebugInfo(@onNull String debugInfo)246 public void addDebugInfo(@NonNull String debugInfo) { 247 if (mDebugInfo == null) { 248 mDebugInfo = new ArrayList<>(); 249 } 250 mDebugInfo.add(debugInfo); 251 } 252 253 /** 254 * Associates information with the instance that can be useful for debugging / logging. 255 * 256 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code debugInfo}. 257 */ addDebugInfo(@onNull List<String> debugInfo)258 public void addDebugInfo(@NonNull List<String> debugInfo) { 259 if (mDebugInfo == null) { 260 mDebugInfo = new ArrayList<>(debugInfo.size()); 261 } 262 mDebugInfo.addAll(debugInfo); 263 } 264 265 @Override equals(@ullable Object o)266 public boolean equals(@Nullable Object o) { 267 if (this == o) { 268 return true; 269 } 270 if (o == null || getClass() != o.getClass()) { 271 return false; 272 } 273 TelephonyTimeZoneSuggestion that = (TelephonyTimeZoneSuggestion) o; 274 return mSlotIndex == that.mSlotIndex 275 && mMatchType == that.mMatchType 276 && mQuality == that.mQuality 277 && Objects.equals(mZoneId, that.mZoneId); 278 } 279 280 @Override hashCode()281 public int hashCode() { 282 return Objects.hash(mSlotIndex, mZoneId, mMatchType, mQuality); 283 } 284 285 @Override toString()286 public String toString() { 287 return "TelephonyTimeZoneSuggestion{" 288 + "mSlotIndex=" + mSlotIndex 289 + ", mZoneId='" + mZoneId + '\'' 290 + ", mMatchType=" + mMatchType 291 + ", mQuality=" + mQuality 292 + ", mDebugInfo=" + mDebugInfo 293 + '}'; 294 } 295 296 /** 297 * Builds {@link TelephonyTimeZoneSuggestion} instances. 298 * 299 * @hide 300 */ 301 public static final class Builder { 302 private final int mSlotIndex; 303 @Nullable private String mZoneId; 304 @MatchType private int mMatchType; 305 @Quality private int mQuality; 306 @Nullable private List<String> mDebugInfo; 307 308 /** 309 * Creates a builder with the specified {@code slotIndex}. 310 * 311 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code slotIndex}. 312 */ Builder(int slotIndex)313 public Builder(int slotIndex) { 314 mSlotIndex = slotIndex; 315 } 316 317 /** 318 * Returns the builder for call chaining. 319 * 320 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code zoneId}. 321 */ 322 @NonNull setZoneId(@ullable String zoneId)323 public Builder setZoneId(@Nullable String zoneId) { 324 mZoneId = zoneId; 325 return this; 326 } 327 328 /** 329 * Returns the builder for call chaining. 330 * 331 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code matchType}. 332 */ 333 @NonNull setMatchType(@atchType int matchType)334 public Builder setMatchType(@MatchType int matchType) { 335 mMatchType = matchType; 336 return this; 337 } 338 339 /** 340 * Returns the builder for call chaining. 341 * 342 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code quality}. 343 */ 344 @NonNull setQuality(@uality int quality)345 public Builder setQuality(@Quality int quality) { 346 mQuality = quality; 347 return this; 348 } 349 350 /** 351 * Returns the builder for call chaining. 352 * 353 * <p>See {@link TelephonyTimeZoneSuggestion} for more information about {@code debugInfo}. 354 */ 355 @NonNull addDebugInfo(@onNull String debugInfo)356 public Builder addDebugInfo(@NonNull String debugInfo) { 357 if (mDebugInfo == null) { 358 mDebugInfo = new ArrayList<>(); 359 } 360 mDebugInfo.add(debugInfo); 361 return this; 362 } 363 364 /** 365 * Performs basic structural validation of this instance. e.g. Are all the fields populated 366 * that must be? Are the enum ints set to valid values? 367 */ validate()368 void validate() { 369 int quality = mQuality; 370 int matchType = mMatchType; 371 if (mZoneId == null) { 372 if (quality != QUALITY_NA || matchType != MATCH_TYPE_NA) { 373 throw new RuntimeException("Invalid quality or match type for null zone ID." 374 + " quality=" + quality + ", matchType=" + matchType); 375 } 376 } else { 377 boolean qualityValid = (quality == QUALITY_SINGLE_ZONE 378 || quality == QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET 379 || quality == QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS); 380 boolean matchTypeValid = (matchType == MATCH_TYPE_NETWORK_COUNTRY_ONLY 381 || matchType == MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET 382 || matchType == MATCH_TYPE_EMULATOR_ZONE_ID 383 || matchType == MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY); 384 if (!qualityValid || !matchTypeValid) { 385 throw new RuntimeException("Invalid quality or match type with zone ID." 386 + " quality=" + quality + ", matchType=" + matchType); 387 } 388 } 389 } 390 391 /** Returns the {@link TelephonyTimeZoneSuggestion}. */ 392 @NonNull build()393 public TelephonyTimeZoneSuggestion build() { 394 validate(); 395 return new TelephonyTimeZoneSuggestion(this); 396 } 397 } 398 399 /** @hide */ parseCommandLineArg(@onNull ShellCommand cmd)400 public static TelephonyTimeZoneSuggestion parseCommandLineArg(@NonNull ShellCommand cmd) 401 throws IllegalArgumentException { 402 Integer slotIndex = null; 403 String zoneId = null; 404 Integer quality = null; 405 Integer matchType = null; 406 String opt; 407 while ((opt = cmd.getNextArg()) != null) { 408 switch (opt) { 409 case "--slot_index": { 410 slotIndex = Integer.parseInt(cmd.getNextArgRequired()); 411 break; 412 } 413 case "--zone_id": { 414 zoneId = cmd.getNextArgRequired(); 415 break; 416 } 417 case "--quality": { 418 quality = parseQualityCommandLineArg(cmd.getNextArgRequired()); 419 break; 420 } 421 case "--match_type": { 422 matchType = parseMatchTypeCommandLineArg(cmd.getNextArgRequired()); 423 break; 424 } 425 default: { 426 throw new IllegalArgumentException("Unknown option: " + opt); 427 } 428 } 429 } 430 431 if (slotIndex == null) { 432 throw new IllegalArgumentException("No slotIndex specified."); 433 } 434 435 Builder builder = new Builder(slotIndex); 436 if (!(TextUtils.isEmpty(zoneId) || "_".equals(zoneId))) { 437 builder.setZoneId(zoneId); 438 } 439 if (quality != null) { 440 builder.setQuality(quality); 441 } 442 if (matchType != null) { 443 builder.setMatchType(matchType); 444 } 445 builder.addDebugInfo("Command line injection"); 446 return builder.build(); 447 } 448 parseQualityCommandLineArg(@onNull String arg)449 private static int parseQualityCommandLineArg(@NonNull String arg) { 450 switch (arg) { 451 case "single": 452 return QUALITY_SINGLE_ZONE; 453 case "multiple_same": 454 return QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET; 455 case "multiple_different": 456 return QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS; 457 default: 458 throw new IllegalArgumentException("Unrecognized quality: " + arg); 459 } 460 } 461 parseMatchTypeCommandLineArg(@onNull String arg)462 private static int parseMatchTypeCommandLineArg(@NonNull String arg) { 463 switch (arg) { 464 case "emulator": 465 return MATCH_TYPE_EMULATOR_ZONE_ID; 466 case "country_with_offset": 467 return MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET; 468 case "country": 469 return MATCH_TYPE_NETWORK_COUNTRY_ONLY; 470 case "test_network": 471 return MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY; 472 default: 473 throw new IllegalArgumentException("Unrecognized match_type: " + arg); 474 } 475 } 476 477 /** @hide */ printCommandLineOpts(@onNull PrintWriter pw)478 public static void printCommandLineOpts(@NonNull PrintWriter pw) { 479 pw.println("Telephony suggestion options:"); 480 pw.println(" --slot_index <number>"); 481 pw.println(" To withdraw a previous suggestion:"); 482 pw.println(" [--zone_id \"_\"]"); 483 pw.println(" To make a new suggestion:"); 484 pw.println(" --zone_id <Olson ID>"); 485 pw.println(" --quality <single|multiple_same|multiple_different>"); 486 pw.println(" --match_type <emulator|country_with_offset|country|test_network>"); 487 pw.println(); 488 pw.println("See " + TelephonyTimeZoneSuggestion.class.getName() + " for more information"); 489 } 490 } 491