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 static android.app.time.Capabilities.CAPABILITY_NOT_ALLOWED;
20 import static android.app.time.Capabilities.CAPABILITY_NOT_APPLICABLE;
21 import static android.app.time.Capabilities.CAPABILITY_NOT_SUPPORTED;
22 import static android.app.time.Capabilities.CAPABILITY_POSSESSED;
23 
24 import android.annotation.IntDef;
25 import android.annotation.NonNull;
26 import android.annotation.UserIdInt;
27 import android.app.time.Capabilities.CapabilityState;
28 import android.app.time.TimeZoneCapabilities;
29 import android.app.time.TimeZoneConfiguration;
30 import android.os.UserHandle;
31 
32 import java.lang.annotation.ElementType;
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.lang.annotation.Target;
36 import java.util.Objects;
37 
38 /**
39  * Holds configuration values that affect user-facing time zone behavior and some associated logic.
40  * Some configuration is global, some is user scoped, but this class deliberately doesn't make a
41  * distinction for simplicity.
42  */
43 public final class ConfigurationInternal {
44 
45     @IntDef(prefix = "DETECTION_MODE_",
46             value = { DETECTION_MODE_UNKNOWN, DETECTION_MODE_MANUAL, DETECTION_MODE_GEO,
47                     DETECTION_MODE_TELEPHONY }
48     )
49     @Retention(RetentionPolicy.SOURCE)
50     @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER })
51     @interface DetectionMode {};
52 
53     public static final @DetectionMode int DETECTION_MODE_UNKNOWN = 0;
54     public static final @DetectionMode int DETECTION_MODE_MANUAL = 1;
55     public static final @DetectionMode int DETECTION_MODE_GEO = 2;
56     public static final @DetectionMode int DETECTION_MODE_TELEPHONY = 3;
57 
58     private final boolean mTelephonyDetectionSupported;
59     private final boolean mGeoDetectionSupported;
60     private final boolean mTelephonyFallbackSupported;
61     private final boolean mGeoDetectionRunInBackgroundEnabled;
62     private final boolean mEnhancedMetricsCollectionEnabled;
63     private final boolean mAutoDetectionEnabledSetting;
64     private final @UserIdInt int mUserId;
65     private final boolean mUserConfigAllowed;
66     private final boolean mLocationEnabledSetting;
67     private final boolean mGeoDetectionEnabledSetting;
68 
ConfigurationInternal(Builder builder)69     private ConfigurationInternal(Builder builder) {
70         mTelephonyDetectionSupported = builder.mTelephonyDetectionSupported;
71         mGeoDetectionSupported = builder.mGeoDetectionSupported;
72         mTelephonyFallbackSupported = builder.mTelephonyFallbackSupported;
73         mGeoDetectionRunInBackgroundEnabled = builder.mGeoDetectionRunInBackgroundEnabled;
74         mEnhancedMetricsCollectionEnabled = builder.mEnhancedMetricsCollectionEnabled;
75         mAutoDetectionEnabledSetting = builder.mAutoDetectionEnabledSetting;
76 
77         mUserId = Objects.requireNonNull(builder.mUserId, "userId must be set");
78         mUserConfigAllowed = builder.mUserConfigAllowed;
79         mLocationEnabledSetting = builder.mLocationEnabledSetting;
80         mGeoDetectionEnabledSetting = builder.mGeoDetectionEnabledSetting;
81     }
82 
83     /** Returns true if the device supports any form of auto time zone detection. */
isAutoDetectionSupported()84     public boolean isAutoDetectionSupported() {
85         return mTelephonyDetectionSupported || mGeoDetectionSupported;
86     }
87 
88     /** Returns true if the device supports telephony time zone detection. */
isTelephonyDetectionSupported()89     public boolean isTelephonyDetectionSupported() {
90         return mTelephonyDetectionSupported;
91     }
92 
93     /** Returns true if the device supports geolocation time zone detection. */
isGeoDetectionSupported()94     public boolean isGeoDetectionSupported() {
95         return mGeoDetectionSupported;
96     }
97 
98     /**
99      * Returns true if the device supports time zone detection falling back to telephony detection
100      * under certain circumstances.
101      */
isTelephonyFallbackSupported()102     public boolean isTelephonyFallbackSupported() {
103         return mTelephonyFallbackSupported;
104     }
105 
106     /**
107      * Returns {@code true} if location time zone detection should run when auto time zone detection
108      * is enabled on supported devices, even when the user has not enabled the algorithm explicitly
109      * in settings. Enabled for internal testing only. See {@link #isGeoDetectionExecutionEnabled()}
110      * and {@link #getDetectionMode()} for details.
111      */
getGeoDetectionRunInBackgroundEnabledSetting()112     boolean getGeoDetectionRunInBackgroundEnabledSetting() {
113         return mGeoDetectionRunInBackgroundEnabled;
114     }
115 
116     /**
117      * Returns {@code true} if the device can collect / report extra metrics information for QA
118      * / testers. These metrics might involve logging more expensive or more revealing data that
119      * would not be collected from the set of public users.
120      */
isEnhancedMetricsCollectionEnabled()121     public boolean isEnhancedMetricsCollectionEnabled() {
122         return mEnhancedMetricsCollectionEnabled;
123     }
124 
125     /** Returns the value of the auto time zone detection enabled setting. */
getAutoDetectionEnabledSetting()126     public boolean getAutoDetectionEnabledSetting() {
127         return mAutoDetectionEnabledSetting;
128     }
129 
130     /**
131      * Returns true if auto time zone detection behavior is actually enabled, which can be distinct
132      * from the raw setting value.
133      */
getAutoDetectionEnabledBehavior()134     public boolean getAutoDetectionEnabledBehavior() {
135         return isAutoDetectionSupported() && getAutoDetectionEnabledSetting();
136     }
137 
138     /** Returns the ID of the user this configuration is associated with. */
getUserId()139     public @UserIdInt int getUserId() {
140         return mUserId;
141     }
142 
143     /** Returns the handle of the user this configuration is associated with. */
144     @NonNull
getUserHandle()145     public UserHandle getUserHandle() {
146         return UserHandle.of(mUserId);
147     }
148 
149     /**
150      * Returns true if the user is allowed to modify time zone configuration, e.g. can be false due
151      * to device policy (enterprise).
152      *
153      * <p>See also {@link #asCapabilities(boolean)} for situations where this value is ignored.
154      */
isUserConfigAllowed()155     public boolean isUserConfigAllowed() {
156         return mUserConfigAllowed;
157     }
158 
159     /** Returns true if user's location can be used generally. */
getLocationEnabledSetting()160     public boolean getLocationEnabledSetting() {
161         return mLocationEnabledSetting;
162     }
163 
164     /** Returns the value of the geolocation time zone detection enabled setting. */
getGeoDetectionEnabledSetting()165     public boolean getGeoDetectionEnabledSetting() {
166         return mGeoDetectionEnabledSetting;
167     }
168 
169     /**
170      * Returns the detection mode to use, i.e. which suggestions to use to determine the device's
171      * time zone.
172      */
getDetectionMode()173     public @DetectionMode int getDetectionMode() {
174         if (!isAutoDetectionSupported()) {
175             // Handle the easy case first: No auto detection algorithms supported must mean manual.
176             return DETECTION_MODE_MANUAL;
177         } else if (!getAutoDetectionEnabledSetting()) {
178             // Auto detection algorithms are supported, but disabled by the user.
179             return DETECTION_MODE_MANUAL;
180         } else if (getGeoDetectionEnabledBehavior()) {
181             return DETECTION_MODE_GEO;
182         } else if (isTelephonyDetectionSupported()) {
183             return DETECTION_MODE_TELEPHONY;
184         } else {
185             // On devices with telephony detection support, telephony is used instead of geo when
186             // geo cannot be used. This "unknown" case can occur on devices with only the location
187             // detection algorithm supported when the user's master location setting prevents its
188             // use.
189             return DETECTION_MODE_UNKNOWN;
190         }
191     }
192 
getGeoDetectionEnabledBehavior()193     private boolean getGeoDetectionEnabledBehavior() {
194         // isAutoDetectionSupported() should already have been checked before calling this method.
195         if (isGeoDetectionSupported() && getLocationEnabledSetting()) {
196             if (isTelephonyDetectionSupported()) {
197                 // This is the "normal" case for smartphones that have both telephony and geo
198                 // detection: the user chooses which type of detection to use.
199                 return getGeoDetectionEnabledSetting();
200             } else {
201                 // When only geo detection is supported then there is no choice for the user to
202                 // make between detection modes, so no user setting is consulted.
203                 return true;
204             }
205         }
206         return false;
207     }
208 
209     /**
210      * Returns true if geolocation time zone detection behavior can execute. Typically, this will
211      * agree with {@link #getDetectionMode()}, but under rare circumstances the geolocation detector
212      * may be run in the background if the user's settings allow.
213      */
isGeoDetectionExecutionEnabled()214     public boolean isGeoDetectionExecutionEnabled() {
215         return getDetectionMode() == DETECTION_MODE_GEO
216                 || getGeoDetectionRunInBackgroundEnabledBehavior();
217     }
218 
getGeoDetectionRunInBackgroundEnabledBehavior()219     private boolean getGeoDetectionRunInBackgroundEnabledBehavior() {
220         return isGeoDetectionSupported()
221                 && getLocationEnabledSetting()
222                 && getAutoDetectionEnabledSetting()
223                 && getGeoDetectionRunInBackgroundEnabledSetting();
224     }
225 
226     @NonNull
asCapabilities(boolean bypassUserPolicyChecks)227     public TimeZoneCapabilities asCapabilities(boolean bypassUserPolicyChecks) {
228         UserHandle userHandle = UserHandle.of(mUserId);
229         TimeZoneCapabilities.Builder builder = new TimeZoneCapabilities.Builder(userHandle);
230 
231         boolean allowConfigDateTime = isUserConfigAllowed() || bypassUserPolicyChecks;
232 
233         // Automatic time zone detection is only supported on devices if there is a telephony
234         // network available or geolocation time zone detection is possible.
235         boolean deviceHasAutoTimeZoneDetection = isAutoDetectionSupported();
236 
237         final @CapabilityState int configureAutoDetectionEnabledCapability;
238         if (!deviceHasAutoTimeZoneDetection) {
239             configureAutoDetectionEnabledCapability = CAPABILITY_NOT_SUPPORTED;
240         } else if (!allowConfigDateTime) {
241             configureAutoDetectionEnabledCapability = CAPABILITY_NOT_ALLOWED;
242         } else {
243             configureAutoDetectionEnabledCapability = CAPABILITY_POSSESSED;
244         }
245         builder.setConfigureAutoDetectionEnabledCapability(configureAutoDetectionEnabledCapability);
246 
247         builder.setUseLocationEnabled(mLocationEnabledSetting);
248 
249         boolean deviceHasLocationTimeZoneDetection = isGeoDetectionSupported();
250         boolean deviceHasTelephonyDetection = isTelephonyDetectionSupported();
251 
252         // Note: allowConfigDateTime does not restrict the ability to change location time zone
253         // detection enabled. This is intentional as it has user privacy implications and so it
254         // makes sense to leave this under a user's control. The only time this is not true is
255         // on devices that only support location-based detection and the main auto detection setting
256         // is used to influence whether location can be used.
257         final @CapabilityState int configureGeolocationDetectionEnabledCapability;
258         if (!deviceHasLocationTimeZoneDetection || !deviceHasTelephonyDetection) {
259             // If the device doesn't have geolocation detection support OR it ONLY has geolocation
260             // detection support (no telephony) then the user doesn't need the ability to toggle the
261             // location-based detection on and off (the auto detection toggle is considered
262             // sufficient).
263             configureGeolocationDetectionEnabledCapability = CAPABILITY_NOT_SUPPORTED;
264         } else if (!mAutoDetectionEnabledSetting || !getLocationEnabledSetting()) {
265             configureGeolocationDetectionEnabledCapability = CAPABILITY_NOT_APPLICABLE;
266         } else {
267             configureGeolocationDetectionEnabledCapability = CAPABILITY_POSSESSED;
268         }
269         builder.setConfigureGeoDetectionEnabledCapability(
270                 configureGeolocationDetectionEnabledCapability);
271 
272         // The ability to make manual time zone suggestions can also be restricted by policy. With
273         // the current logic above, this could lead to a situation where a device hardware does not
274         // support auto detection, the device has been forced into "auto" mode by an admin and the
275         // user is unable to disable auto detection.
276         final @CapabilityState int suggestManualTimeZoneCapability;
277         if (!allowConfigDateTime) {
278             suggestManualTimeZoneCapability = CAPABILITY_NOT_ALLOWED;
279         } else if (getAutoDetectionEnabledBehavior()) {
280             suggestManualTimeZoneCapability = CAPABILITY_NOT_APPLICABLE;
281         } else {
282             suggestManualTimeZoneCapability = CAPABILITY_POSSESSED;
283         }
284         builder.setSetManualTimeZoneCapability(suggestManualTimeZoneCapability);
285 
286         return builder.build();
287     }
288 
289     /** Returns a {@link TimeZoneConfiguration} from the configuration values. */
asConfiguration()290     public TimeZoneConfiguration asConfiguration() {
291         return new TimeZoneConfiguration.Builder()
292                 .setAutoDetectionEnabled(getAutoDetectionEnabledSetting())
293                 .setGeoDetectionEnabled(getGeoDetectionEnabledSetting())
294                 .build();
295     }
296 
297     /**
298      * Merges the configuration values from this with any properties set in {@code
299      * newConfiguration}. The new configuration has precedence. Used to apply user updates to
300      * internal configuration.
301      */
merge(TimeZoneConfiguration newConfiguration)302     public ConfigurationInternal merge(TimeZoneConfiguration newConfiguration) {
303         Builder builder = new Builder(this);
304         if (newConfiguration.hasIsAutoDetectionEnabled()) {
305             builder.setAutoDetectionEnabledSetting(newConfiguration.isAutoDetectionEnabled());
306         }
307         if (newConfiguration.hasIsGeoDetectionEnabled()) {
308             builder.setGeoDetectionEnabledSetting(newConfiguration.isGeoDetectionEnabled());
309         }
310         return builder.build();
311     }
312 
313     @Override
equals(Object o)314     public boolean equals(Object o) {
315         if (this == o) {
316             return true;
317         }
318         if (o == null || getClass() != o.getClass()) {
319             return false;
320         }
321         ConfigurationInternal that = (ConfigurationInternal) o;
322         return mUserId == that.mUserId
323                 && mUserConfigAllowed == that.mUserConfigAllowed
324                 && mTelephonyDetectionSupported == that.mTelephonyDetectionSupported
325                 && mGeoDetectionSupported == that.mGeoDetectionSupported
326                 && mTelephonyFallbackSupported == that.mTelephonyFallbackSupported
327                 && mGeoDetectionRunInBackgroundEnabled == that.mGeoDetectionRunInBackgroundEnabled
328                 && mEnhancedMetricsCollectionEnabled == that.mEnhancedMetricsCollectionEnabled
329                 && mAutoDetectionEnabledSetting == that.mAutoDetectionEnabledSetting
330                 && mLocationEnabledSetting == that.mLocationEnabledSetting
331                 && mGeoDetectionEnabledSetting == that.mGeoDetectionEnabledSetting;
332     }
333 
334     @Override
hashCode()335     public int hashCode() {
336         return Objects.hash(mUserId, mUserConfigAllowed, mTelephonyDetectionSupported,
337                 mGeoDetectionSupported, mTelephonyFallbackSupported,
338                 mGeoDetectionRunInBackgroundEnabled, mEnhancedMetricsCollectionEnabled,
339                 mAutoDetectionEnabledSetting, mLocationEnabledSetting, mGeoDetectionEnabledSetting);
340     }
341 
342     @Override
toString()343     public String toString() {
344         return "ConfigurationInternal{"
345                 + "mUserId=" + mUserId
346                 + ", mUserConfigAllowed=" + mUserConfigAllowed
347                 + ", mTelephonyDetectionSupported=" + mTelephonyDetectionSupported
348                 + ", mGeoDetectionSupported=" + mGeoDetectionSupported
349                 + ", mTelephonyFallbackSupported=" + mTelephonyFallbackSupported
350                 + ", mGeoDetectionRunInBackgroundEnabled=" + mGeoDetectionRunInBackgroundEnabled
351                 + ", mEnhancedMetricsCollectionEnabled=" + mEnhancedMetricsCollectionEnabled
352                 + ", mAutoDetectionEnabledSetting=" + mAutoDetectionEnabledSetting
353                 + ", mLocationEnabledSetting=" + mLocationEnabledSetting
354                 + ", mGeoDetectionEnabledSetting=" + mGeoDetectionEnabledSetting
355                 + '}';
356     }
357 
358     /**
359      * A Builder for {@link ConfigurationInternal}.
360      */
361     public static class Builder {
362 
363         private @UserIdInt Integer mUserId;
364         private boolean mUserConfigAllowed;
365         private boolean mTelephonyDetectionSupported;
366         private boolean mGeoDetectionSupported;
367         private boolean mTelephonyFallbackSupported;
368         private boolean mGeoDetectionRunInBackgroundEnabled;
369         private boolean mEnhancedMetricsCollectionEnabled;
370         private boolean mAutoDetectionEnabledSetting;
371         private boolean mLocationEnabledSetting;
372         private boolean mGeoDetectionEnabledSetting;
373 
374         /**
375          * Creates a new Builder.
376          */
Builder()377         public Builder() {}
378 
379         /**
380          * Creates a new Builder by copying values from an existing instance.
381          */
Builder(ConfigurationInternal toCopy)382         public Builder(ConfigurationInternal toCopy) {
383             this.mUserId = toCopy.mUserId;
384             this.mUserConfigAllowed = toCopy.mUserConfigAllowed;
385             this.mTelephonyDetectionSupported = toCopy.mTelephonyDetectionSupported;
386             this.mTelephonyFallbackSupported = toCopy.mTelephonyFallbackSupported;
387             this.mGeoDetectionSupported = toCopy.mGeoDetectionSupported;
388             this.mGeoDetectionRunInBackgroundEnabled = toCopy.mGeoDetectionRunInBackgroundEnabled;
389             this.mEnhancedMetricsCollectionEnabled = toCopy.mEnhancedMetricsCollectionEnabled;
390             this.mAutoDetectionEnabledSetting = toCopy.mAutoDetectionEnabledSetting;
391             this.mLocationEnabledSetting = toCopy.mLocationEnabledSetting;
392             this.mGeoDetectionEnabledSetting = toCopy.mGeoDetectionEnabledSetting;
393         }
394 
395         /**
396          * Sets the user ID the configuration is for.
397          */
setUserId(@serIdInt int userId)398         public Builder setUserId(@UserIdInt int userId) {
399             mUserId = userId;
400             return this;
401         }
402 
403         /**
404          * Sets whether the user is allowed to configure time zone settings on this device.
405          */
setUserConfigAllowed(boolean configAllowed)406         public Builder setUserConfigAllowed(boolean configAllowed) {
407             mUserConfigAllowed = configAllowed;
408             return this;
409         }
410 
411         /**
412          * Sets whether telephony time zone detection is supported on this device.
413          */
setTelephonyDetectionFeatureSupported(boolean supported)414         public Builder setTelephonyDetectionFeatureSupported(boolean supported) {
415             mTelephonyDetectionSupported = supported;
416             return this;
417         }
418 
419         /**
420          * Sets whether geolocation time zone detection is supported on this device.
421          */
setGeoDetectionFeatureSupported(boolean supported)422         public Builder setGeoDetectionFeatureSupported(boolean supported) {
423             mGeoDetectionSupported = supported;
424             return this;
425         }
426 
427         /**
428          * Sets whether time zone detection supports falling back to telephony detection under
429          * certain circumstances.
430          */
setTelephonyFallbackSupported(boolean supported)431         public Builder setTelephonyFallbackSupported(boolean supported) {
432             mTelephonyFallbackSupported = supported;
433             return this;
434         }
435 
436         /**
437          * Sets whether location time zone detection should run when auto time zone detection is
438          * enabled on supported devices, even when the user has not enabled the algorithm explicitly
439          * in settings. Enabled for internal testing only.
440          */
setGeoDetectionRunInBackgroundEnabled(boolean enabled)441         public Builder setGeoDetectionRunInBackgroundEnabled(boolean enabled) {
442             mGeoDetectionRunInBackgroundEnabled = enabled;
443             return this;
444         }
445 
446         /**
447          * Sets the value for enhanced metrics collection.
448          */
setEnhancedMetricsCollectionEnabled(boolean enabled)449         public Builder setEnhancedMetricsCollectionEnabled(boolean enabled) {
450             mEnhancedMetricsCollectionEnabled = enabled;
451             return this;
452         }
453 
454         /**
455          * Sets the value of the automatic time zone detection enabled setting for this device.
456          */
setAutoDetectionEnabledSetting(boolean enabled)457         public Builder setAutoDetectionEnabledSetting(boolean enabled) {
458             mAutoDetectionEnabledSetting = enabled;
459             return this;
460         }
461 
462         /**
463          * Sets the value of the location mode setting for this user.
464          */
setLocationEnabledSetting(boolean enabled)465         public Builder setLocationEnabledSetting(boolean enabled) {
466             mLocationEnabledSetting = enabled;
467             return this;
468         }
469 
470         /**
471          * Sets the value of the geolocation time zone detection setting for this user.
472          */
setGeoDetectionEnabledSetting(boolean enabled)473         public Builder setGeoDetectionEnabledSetting(boolean enabled) {
474             mGeoDetectionEnabledSetting = enabled;
475             return this;
476         }
477 
478         /** Returns a new {@link ConfigurationInternal}. */
479         @NonNull
build()480         public ConfigurationInternal build() {
481             return new ConfigurationInternal(this);
482         }
483     }
484 }
485