1 /*
2  * Copyright (C) 2021 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 static libcore.io.IoUtils.closeQuietly;
20 
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.timezonedetector.ManualTimeZoneSuggestion;
25 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
26 import android.util.proto.ProtoOutputStream;
27 
28 import java.io.ByteArrayOutputStream;
29 import java.util.Arrays;
30 import java.util.List;
31 import java.util.Objects;
32 
33 /**
34  * A class that provides time zone detector state information for metrics.
35  *
36  * <p>
37  * Regarding time zone ID ordinals:
38  * <p>
39  * We don't want to leak user location information by reporting time zone IDs. Instead, time zone
40  * IDs are consistently identified within a given instance of this class by a numeric ID. This
41  * allows comparison of IDs without revealing what those IDs are.
42  */
43 public final class MetricsTimeZoneDetectorState {
44 
45     @IntDef(prefix = "DETECTION_MODE_",
46             value = { DETECTION_MODE_MANUAL, DETECTION_MODE_GEO, DETECTION_MODE_TELEPHONY})
47     @interface DetectionMode {};
48 
49     @DetectionMode
50     public static final int DETECTION_MODE_MANUAL = 0;
51     @DetectionMode
52     public static final int DETECTION_MODE_GEO = 1;
53     @DetectionMode
54     public static final int DETECTION_MODE_TELEPHONY = 2;
55 
56     @NonNull
57     private final ConfigurationInternal mConfigurationInternal;
58     @NonNull
59     private final int mDeviceTimeZoneIdOrdinal;
60     @Nullable
61     private final MetricsTimeZoneSuggestion mLatestManualSuggestion;
62     @Nullable
63     private final MetricsTimeZoneSuggestion mLatestTelephonySuggestion;
64     @Nullable
65     private final MetricsTimeZoneSuggestion mLatestGeolocationSuggestion;
66 
MetricsTimeZoneDetectorState( @onNull ConfigurationInternal configurationInternal, int deviceTimeZoneIdOrdinal, @Nullable MetricsTimeZoneSuggestion latestManualSuggestion, @Nullable MetricsTimeZoneSuggestion latestTelephonySuggestion, @Nullable MetricsTimeZoneSuggestion latestGeolocationSuggestion)67     private MetricsTimeZoneDetectorState(
68             @NonNull ConfigurationInternal configurationInternal,
69             int deviceTimeZoneIdOrdinal,
70             @Nullable MetricsTimeZoneSuggestion latestManualSuggestion,
71             @Nullable MetricsTimeZoneSuggestion latestTelephonySuggestion,
72             @Nullable MetricsTimeZoneSuggestion latestGeolocationSuggestion) {
73         mConfigurationInternal = Objects.requireNonNull(configurationInternal);
74         mDeviceTimeZoneIdOrdinal = deviceTimeZoneIdOrdinal;
75         mLatestManualSuggestion = latestManualSuggestion;
76         mLatestTelephonySuggestion = latestTelephonySuggestion;
77         mLatestGeolocationSuggestion = latestGeolocationSuggestion;
78     }
79 
80     /**
81      * Creates {@link MetricsTimeZoneDetectorState} from the supplied parameters, using the {@link
82      * OrdinalGenerator} to generate time zone ID ordinals.
83      */
create( @onNull OrdinalGenerator<String> tzIdOrdinalGenerator, @NonNull ConfigurationInternal configurationInternal, @NonNull String deviceTimeZoneId, @Nullable ManualTimeZoneSuggestion latestManualSuggestion, @Nullable TelephonyTimeZoneSuggestion latestTelephonySuggestion, @Nullable GeolocationTimeZoneSuggestion latestGeolocationSuggestion)84     public static MetricsTimeZoneDetectorState create(
85             @NonNull OrdinalGenerator<String> tzIdOrdinalGenerator,
86             @NonNull ConfigurationInternal configurationInternal,
87             @NonNull String deviceTimeZoneId,
88             @Nullable ManualTimeZoneSuggestion latestManualSuggestion,
89             @Nullable TelephonyTimeZoneSuggestion latestTelephonySuggestion,
90             @Nullable GeolocationTimeZoneSuggestion latestGeolocationSuggestion) {
91 
92         int deviceTimeZoneIdOrdinal =
93                 tzIdOrdinalGenerator.ordinal(Objects.requireNonNull(deviceTimeZoneId));
94         MetricsTimeZoneSuggestion latestObfuscatedManualSuggestion =
95                 createMetricsTimeZoneSuggestion(tzIdOrdinalGenerator, latestManualSuggestion);
96         MetricsTimeZoneSuggestion latestObfuscatedTelephonySuggestion =
97                 createMetricsTimeZoneSuggestion(tzIdOrdinalGenerator, latestTelephonySuggestion);
98         MetricsTimeZoneSuggestion latestObfuscatedGeolocationSuggestion =
99                 createMetricsTimeZoneSuggestion(tzIdOrdinalGenerator, latestGeolocationSuggestion);
100 
101         return new MetricsTimeZoneDetectorState(
102                 configurationInternal, deviceTimeZoneIdOrdinal, latestObfuscatedManualSuggestion,
103                 latestObfuscatedTelephonySuggestion, latestObfuscatedGeolocationSuggestion);
104     }
105 
106     /** Returns true if the device supports telephony time zone detection. */
isTelephonyDetectionSupported()107     public boolean isTelephonyDetectionSupported() {
108         return mConfigurationInternal.isTelephonyDetectionSupported();
109     }
110 
111     /** Returns true if the device supports geolocation time zone detection. */
isGeoDetectionSupported()112     public boolean isGeoDetectionSupported() {
113         return mConfigurationInternal.isGeoDetectionSupported();
114     }
115 
116     /** Returns true if user's location can be used generally. */
isUserLocationEnabled()117     public boolean isUserLocationEnabled() {
118         return mConfigurationInternal.isLocationEnabled();
119     }
120 
121     /** Returns the value of the geolocation time zone detection enabled setting. */
getGeoDetectionEnabledSetting()122     public boolean getGeoDetectionEnabledSetting() {
123         return mConfigurationInternal.getGeoDetectionEnabledSetting();
124     }
125 
126     /** Returns the value of the auto time zone detection enabled setting. */
getAutoDetectionEnabledSetting()127     public boolean getAutoDetectionEnabledSetting() {
128         return mConfigurationInternal.getAutoDetectionEnabledSetting();
129     }
130 
131     /**
132      * Returns the detection mode the device is currently using, which can be influenced by various
133      * things besides the user's setting.
134      */
135     @DetectionMode
getDetectionMode()136     public int getDetectionMode() {
137         if (!mConfigurationInternal.getAutoDetectionEnabledBehavior()) {
138             return DETECTION_MODE_MANUAL;
139         } else if (mConfigurationInternal.getGeoDetectionEnabledBehavior()) {
140             return DETECTION_MODE_GEO;
141         } else {
142             return DETECTION_MODE_TELEPHONY;
143         }
144     }
145 
146     /**
147      * Returns the ordinal for the device's currently set time zone ID.
148      * See {@link MetricsTimeZoneDetectorState} for information about ordinals.
149      */
150     @NonNull
getDeviceTimeZoneIdOrdinal()151     public int getDeviceTimeZoneIdOrdinal() {
152         return mDeviceTimeZoneIdOrdinal;
153     }
154 
155     /**
156      * Returns bytes[] for a {@link MetricsTimeZoneSuggestion} for the last manual
157      * suggestion received.
158      */
159     @Nullable
getLatestManualSuggestionProtoBytes()160     public byte[] getLatestManualSuggestionProtoBytes() {
161         return suggestionProtoBytes(mLatestManualSuggestion);
162     }
163 
164     /**
165      * Returns bytes[] for a {@link MetricsTimeZoneSuggestion} for the last, best
166      * telephony suggestion received.
167      */
168     @Nullable
getLatestTelephonySuggestionProtoBytes()169     public byte[] getLatestTelephonySuggestionProtoBytes() {
170         return suggestionProtoBytes(mLatestTelephonySuggestion);
171     }
172 
173     /**
174      * Returns bytes[] for a {@link MetricsTimeZoneSuggestion} for the last geolocation
175      * suggestion received.
176      */
177     @Nullable
getLatestGeolocationSuggestionProtoBytes()178     public byte[] getLatestGeolocationSuggestionProtoBytes() {
179         return suggestionProtoBytes(mLatestGeolocationSuggestion);
180     }
181 
182     @Override
equals(Object o)183     public boolean equals(Object o) {
184         if (this == o) {
185             return true;
186         }
187         if (o == null || getClass() != o.getClass()) {
188             return false;
189         }
190         MetricsTimeZoneDetectorState that = (MetricsTimeZoneDetectorState) o;
191         return mDeviceTimeZoneIdOrdinal == that.mDeviceTimeZoneIdOrdinal
192                 && mConfigurationInternal.equals(that.mConfigurationInternal)
193                 && Objects.equals(mLatestManualSuggestion, that.mLatestManualSuggestion)
194                 && Objects.equals(mLatestTelephonySuggestion, that.mLatestTelephonySuggestion)
195                 && Objects.equals(mLatestGeolocationSuggestion, that.mLatestGeolocationSuggestion);
196     }
197 
198     @Override
hashCode()199     public int hashCode() {
200         return Objects.hash(mConfigurationInternal, mDeviceTimeZoneIdOrdinal,
201                 mLatestManualSuggestion, mLatestTelephonySuggestion, mLatestGeolocationSuggestion);
202     }
203 
204     @Override
toString()205     public String toString() {
206         return "MetricsTimeZoneDetectorState{"
207                 + "mConfigurationInternal=" + mConfigurationInternal
208                 + ", mDeviceTimeZoneIdOrdinal=" + mDeviceTimeZoneIdOrdinal
209                 + ", mLatestManualSuggestion=" + mLatestManualSuggestion
210                 + ", mLatestTelephonySuggestion=" + mLatestTelephonySuggestion
211                 + ", mLatestGeolocationSuggestion=" + mLatestGeolocationSuggestion
212                 + '}';
213     }
214 
suggestionProtoBytes( @ullable MetricsTimeZoneSuggestion suggestion)215     private static byte[] suggestionProtoBytes(
216             @Nullable MetricsTimeZoneSuggestion suggestion) {
217         if (suggestion == null) {
218             return null;
219         }
220         return suggestion.toBytes();
221     }
222 
223     @Nullable
createMetricsTimeZoneSuggestion( @onNull OrdinalGenerator<String> zoneIdOrdinalGenerator, @NonNull ManualTimeZoneSuggestion manualSuggestion)224     private static MetricsTimeZoneSuggestion createMetricsTimeZoneSuggestion(
225             @NonNull OrdinalGenerator<String> zoneIdOrdinalGenerator,
226             @NonNull ManualTimeZoneSuggestion manualSuggestion) {
227         if (manualSuggestion == null) {
228             return null;
229         }
230 
231         int zoneIdOrdinal = zoneIdOrdinalGenerator.ordinal(manualSuggestion.getZoneId());
232         return MetricsTimeZoneSuggestion.createCertain(
233                 new int[] { zoneIdOrdinal });
234     }
235 
236     @Nullable
createMetricsTimeZoneSuggestion( @onNull OrdinalGenerator<String> zoneIdOrdinalGenerator, @NonNull TelephonyTimeZoneSuggestion telephonySuggestion)237     private static MetricsTimeZoneSuggestion createMetricsTimeZoneSuggestion(
238             @NonNull OrdinalGenerator<String> zoneIdOrdinalGenerator,
239             @NonNull TelephonyTimeZoneSuggestion telephonySuggestion) {
240         if (telephonySuggestion == null) {
241             return null;
242         }
243         if (telephonySuggestion.getZoneId() == null) {
244             return MetricsTimeZoneSuggestion.createUncertain();
245         }
246         int zoneIdOrdinal = zoneIdOrdinalGenerator.ordinal(telephonySuggestion.getZoneId());
247         return MetricsTimeZoneSuggestion.createCertain(new int[] { zoneIdOrdinal });
248     }
249 
250     @Nullable
createMetricsTimeZoneSuggestion( @onNull OrdinalGenerator<String> zoneIdOrdinalGenerator, @Nullable GeolocationTimeZoneSuggestion geolocationSuggestion)251     private static MetricsTimeZoneSuggestion createMetricsTimeZoneSuggestion(
252             @NonNull OrdinalGenerator<String> zoneIdOrdinalGenerator,
253             @Nullable GeolocationTimeZoneSuggestion geolocationSuggestion) {
254         if (geolocationSuggestion == null) {
255             return null;
256         }
257 
258         List<String> zoneIds = geolocationSuggestion.getZoneIds();
259         if (zoneIds == null) {
260             return MetricsTimeZoneSuggestion.createUncertain();
261         }
262         return MetricsTimeZoneSuggestion.createCertain(zoneIdOrdinalGenerator.ordinals(zoneIds));
263     }
264 
265     /**
266      * A Java class that closely matches the android.app.time.MetricsTimeZoneSuggestion
267      * proto definition.
268      */
269     private static final class MetricsTimeZoneSuggestion {
270         @Nullable
271         private final int[] mZoneIdOrdinals;
272 
MetricsTimeZoneSuggestion(@ullable int[] zoneIdOrdinals)273         MetricsTimeZoneSuggestion(@Nullable int[] zoneIdOrdinals) {
274             mZoneIdOrdinals = zoneIdOrdinals;
275         }
276 
277         @NonNull
createUncertain()278         static MetricsTimeZoneSuggestion createUncertain() {
279             return new MetricsTimeZoneSuggestion(null);
280         }
281 
createCertain( @onNull int[] zoneIdOrdinals)282         public static MetricsTimeZoneSuggestion createCertain(
283                 @NonNull int[] zoneIdOrdinals) {
284             return new MetricsTimeZoneSuggestion(zoneIdOrdinals);
285         }
286 
isCertain()287         boolean isCertain() {
288             return mZoneIdOrdinals != null;
289         }
290 
291         @Nullable
getZoneIdOrdinals()292         int[] getZoneIdOrdinals() {
293             return mZoneIdOrdinals;
294         }
295 
toBytes()296         byte[] toBytes() {
297             // We don't get access to the atoms.proto definition for nested proto fields, so we use
298             // an identically specified proto.
299             ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
300             ProtoOutputStream protoOutputStream = new ProtoOutputStream(byteArrayOutputStream);
301             int typeProtoValue = isCertain()
302                     ? android.app.time.MetricsTimeZoneSuggestion.CERTAIN
303                     : android.app.time.MetricsTimeZoneSuggestion.UNCERTAIN;
304             protoOutputStream.write(android.app.time.MetricsTimeZoneSuggestion.TYPE,
305                     typeProtoValue);
306             if (isCertain()) {
307                 for (int zoneIdOrdinal : getZoneIdOrdinals()) {
308                     protoOutputStream.write(
309                             android.app.time.MetricsTimeZoneSuggestion.TIME_ZONE_ORDINALS,
310                             zoneIdOrdinal);
311                 }
312             }
313             protoOutputStream.flush();
314             closeQuietly(byteArrayOutputStream);
315             return byteArrayOutputStream.toByteArray();
316         }
317 
318         @Override
equals(Object o)319         public boolean equals(Object o) {
320             if (this == o) {
321                 return true;
322             }
323             if (o == null || getClass() != o.getClass()) {
324                 return false;
325             }
326             MetricsTimeZoneSuggestion that = (MetricsTimeZoneSuggestion) o;
327             return Arrays.equals(mZoneIdOrdinals, that.mZoneIdOrdinals);
328         }
329 
330         @Override
hashCode()331         public int hashCode() {
332             return Arrays.hashCode(mZoneIdOrdinals);
333         }
334 
335         @Override
toString()336         public String toString() {
337             return "MetricsTimeZoneSuggestion{"
338                     + "mZoneIdOrdinals=" + Arrays.toString(mZoneIdOrdinals)
339                     + '}';
340         }
341     }
342 }
343