1 /* 2 * Copyright (C) 2020 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 com.android.server.timezonedetector; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.os.ShellCommand; 22 23 import java.io.PrintWriter; 24 import java.util.ArrayList; 25 import java.util.Arrays; 26 import java.util.Collections; 27 import java.util.List; 28 import java.util.Objects; 29 import java.util.StringTokenizer; 30 31 /** 32 * A time zone suggestion from a geolocation source. 33 * 34 * <p> Geolocation-based suggestions have the following properties: 35 * 36 * <ul> 37 * <li>{@code zoneIds}. When not {@code null}, {@code zoneIds} contains a list of suggested time 38 * zone IDs, e.g. ["America/Phoenix", "America/Denver"]. Usually there will be a single zoneId. 39 * When there are multiple, this indicates multiple answers are possible for the current 40 * location / accuracy, i.e. if there is a nearby time zone border. The detection logic 41 * receiving the suggestion is expected to use the first element in the absence of other 42 * information, but one of the others may be used if there is supporting evidence / preferences 43 * such as a device setting or corroborating signals from another source. 44 * <br />{@code zoneIds} can be empty if the current location has been determined to have no 45 * time zone. For example, oceans or disputed areas. This is considered a strong signal and the 46 * received need not look for time zone from other sources. 47 * <br />{@code zoneIds} can be {@code null} to indicate that the geolocation source has entered 48 * an "un-opinionated" state and any previous suggestion is being withdrawn. This indicates the 49 * source cannot provide a valid suggestion due to technical limitations. For example, a 50 * geolocation source may become un-opinionated if the device's location is no longer known with 51 * sufficient accuracy, or if the location is known but no time zone can be determined because 52 * no time zone mapping information is available.</li> 53 * <li>{@code debugInfo} contains debugging metadata associated with the suggestion. This is 54 * used to record why the suggestion exists and how it was obtained. This information exists 55 * only to aid in debugging and therefore is used by {@link #toString()}, but it is not for use 56 * in detection logic and is not considered in {@link #hashCode()} or {@link #equals(Object)}. 57 * </li> 58 * </ul> 59 * 60 * @hide 61 */ 62 public final class GeolocationTimeZoneSuggestion { 63 64 @Nullable private final List<String> mZoneIds; 65 @Nullable private ArrayList<String> mDebugInfo; 66 GeolocationTimeZoneSuggestion(@ullable List<String> zoneIds)67 public GeolocationTimeZoneSuggestion(@Nullable List<String> zoneIds) { 68 if (zoneIds == null) { 69 // Unopinionated 70 mZoneIds = null; 71 } else { 72 mZoneIds = Collections.unmodifiableList(new ArrayList<>(zoneIds)); 73 } 74 } 75 76 /** 77 * Returns the zone Ids being suggested. See {@link GeolocationTimeZoneSuggestion} for details. 78 */ 79 @Nullable getZoneIds()80 public List<String> getZoneIds() { 81 return mZoneIds; 82 } 83 84 /** Returns debug information. See {@link GeolocationTimeZoneSuggestion} for details. */ 85 @NonNull getDebugInfo()86 public List<String> getDebugInfo() { 87 return mDebugInfo == null 88 ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo); 89 } 90 91 /** 92 * Associates information with the instance that can be useful for debugging / logging. The 93 * information is present in {@link #toString()} but is not considered for 94 * {@link #equals(Object)} and {@link #hashCode()}. 95 */ addDebugInfo(String... debugInfos)96 public void addDebugInfo(String... debugInfos) { 97 if (mDebugInfo == null) { 98 mDebugInfo = new ArrayList<>(); 99 } 100 mDebugInfo.addAll(Arrays.asList(debugInfos)); 101 } 102 103 @Override equals(Object o)104 public boolean equals(Object o) { 105 if (this == o) { 106 return true; 107 } 108 if (o == null || getClass() != o.getClass()) { 109 return false; 110 } 111 GeolocationTimeZoneSuggestion 112 that = (GeolocationTimeZoneSuggestion) o; 113 return Objects.equals(mZoneIds, that.mZoneIds); 114 } 115 116 @Override hashCode()117 public int hashCode() { 118 return Objects.hash(mZoneIds); 119 } 120 121 @Override toString()122 public String toString() { 123 return "GeolocationTimeZoneSuggestion{" 124 + "mZoneIds=" + mZoneIds 125 + ", mDebugInfo=" + mDebugInfo 126 + '}'; 127 } 128 129 /** @hide */ parseCommandLineArg(@onNull ShellCommand cmd)130 public static GeolocationTimeZoneSuggestion parseCommandLineArg(@NonNull ShellCommand cmd) { 131 String zoneIdsString = null; 132 String opt; 133 while ((opt = cmd.getNextArg()) != null) { 134 switch (opt) { 135 case "--zone_ids": { 136 zoneIdsString = cmd.getNextArgRequired(); 137 break; 138 } 139 default: { 140 throw new IllegalArgumentException("Unknown option: " + opt); 141 } 142 } 143 } 144 List<String> zoneIds = parseZoneIdsArg(zoneIdsString); 145 GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(zoneIds); 146 suggestion.addDebugInfo("Command line injection"); 147 return suggestion; 148 } 149 parseZoneIdsArg(String zoneIdsString)150 private static List<String> parseZoneIdsArg(String zoneIdsString) { 151 if ("UNCERTAIN".equals(zoneIdsString)) { 152 return null; 153 } else if ("EMPTY".equals(zoneIdsString)) { 154 return Collections.emptyList(); 155 } else { 156 ArrayList<String> zoneIds = new ArrayList<>(); 157 StringTokenizer tokenizer = new StringTokenizer(zoneIdsString, ","); 158 while (tokenizer.hasMoreTokens()) { 159 zoneIds.add(tokenizer.nextToken()); 160 } 161 return zoneIds; 162 } 163 } 164 165 /** @hide */ printCommandLineOpts(@onNull PrintWriter pw)166 public static void printCommandLineOpts(@NonNull PrintWriter pw) { 167 pw.println("Geolocation suggestion options:"); 168 pw.println(" --zone_ids {UNCERTAIN|EMPTY|<Olson ID>+}"); 169 pw.println(); 170 pw.println("See " + GeolocationTimeZoneSuggestion.class.getName() 171 + " for more information"); 172 } 173 } 174