blob: 1867ee207958b6c6fcab394a97bdee119206326f [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.timezonedetector;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.ShellCommand;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.StringTokenizer;
/**
* A time zone suggestion from a geolocation source.
*
* <p> Geolocation-based suggestions have the following properties:
*
* <ul>
* <li>{@code zoneIds}. When not {@code null}, {@code zoneIds} contains a list of suggested time
* zone IDs, e.g. ["America/Phoenix", "America/Denver"]. Usually there will be a single zoneId.
* When there are multiple, this indicates multiple answers are possible for the current
* location / accuracy, i.e. if there is a nearby time zone border. The detection logic
* receiving the suggestion is expected to use the first element in the absence of other
* information, but one of the others may be used if there is supporting evidence / preferences
* such as a device setting or corroborating signals from another source.
* <br />{@code zoneIds} can be empty if the current location has been determined to have no
* time zone. For example, oceans or disputed areas. This is considered a strong signal and the
* received need not look for time zone from other sources.
* <br />{@code zoneIds} can be {@code null} to indicate that the geolocation source has entered
* an "un-opinionated" state and any previous suggestion is being withdrawn. This indicates the
* source cannot provide a valid suggestion due to technical limitations. For example, a
* geolocation source may become un-opinionated if the device's location is no longer known with
* sufficient accuracy, or if the location is known but no time zone can be determined because
* no time zone mapping information is available.</li>
* <li>{@code debugInfo} contains debugging metadata associated with the suggestion. This is
* used to record why the suggestion exists and how it was obtained. This information exists
* only to aid in debugging and therefore is used by {@link #toString()}, but it is not for use
* in detection logic and is not considered in {@link #hashCode()} or {@link #equals(Object)}.
* </li>
* </ul>
*
* @hide
*/
public final class GeolocationTimeZoneSuggestion {
@Nullable private final List<String> mZoneIds;
@Nullable private ArrayList<String> mDebugInfo;
public GeolocationTimeZoneSuggestion(@Nullable List<String> zoneIds) {
if (zoneIds == null) {
// Unopinionated
mZoneIds = null;
} else {
mZoneIds = Collections.unmodifiableList(new ArrayList<>(zoneIds));
}
}
/**
* Returns the zone Ids being suggested. See {@link GeolocationTimeZoneSuggestion} for details.
*/
@Nullable
public List<String> getZoneIds() {
return mZoneIds;
}
/** Returns debug information. See {@link GeolocationTimeZoneSuggestion} for details. */
@NonNull
public List<String> getDebugInfo() {
return mDebugInfo == null
? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
}
/**
* Associates information with the instance that can be useful for debugging / logging. The
* information is present in {@link #toString()} but is not considered for
* {@link #equals(Object)} and {@link #hashCode()}.
*/
public void addDebugInfo(String... debugInfos) {
if (mDebugInfo == null) {
mDebugInfo = new ArrayList<>();
}
mDebugInfo.addAll(Arrays.asList(debugInfos));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
GeolocationTimeZoneSuggestion
that = (GeolocationTimeZoneSuggestion) o;
return Objects.equals(mZoneIds, that.mZoneIds);
}
@Override
public int hashCode() {
return Objects.hash(mZoneIds);
}
@Override
public String toString() {
return "GeolocationTimeZoneSuggestion{"
+ "mZoneIds=" + mZoneIds
+ ", mDebugInfo=" + mDebugInfo
+ '}';
}
/** @hide */
public static GeolocationTimeZoneSuggestion parseCommandLineArg(@NonNull ShellCommand cmd) {
String zoneIdsString = null;
String opt;
while ((opt = cmd.getNextArg()) != null) {
switch (opt) {
case "--zone_ids": {
zoneIdsString = cmd.getNextArgRequired();
break;
}
default: {
throw new IllegalArgumentException("Unknown option: " + opt);
}
}
}
List<String> zoneIds = parseZoneIdsArg(zoneIdsString);
GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(zoneIds);
suggestion.addDebugInfo("Command line injection");
return suggestion;
}
private static List<String> parseZoneIdsArg(String zoneIdsString) {
if ("UNCERTAIN".equals(zoneIdsString)) {
return null;
} else if ("EMPTY".equals(zoneIdsString)) {
return Collections.emptyList();
} else {
ArrayList<String> zoneIds = new ArrayList<>();
StringTokenizer tokenizer = new StringTokenizer(zoneIdsString, ",");
while (tokenizer.hasMoreTokens()) {
zoneIds.add(tokenizer.nextToken());
}
return zoneIds;
}
}
/** @hide */
public static void printCommandLineOpts(@NonNull PrintWriter pw) {
pw.println("Geolocation suggestion options:");
pw.println(" --zone_ids {UNCERTAIN|EMPTY|<Olson ID>+}");
pw.println();
pw.println("See " + GeolocationTimeZoneSuggestion.class.getName()
+ " for more information");
}
}