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 package com.android.settings.datetime;
17 
18 import static android.app.time.Capabilities.CAPABILITY_NOT_ALLOWED;
19 import static android.app.time.Capabilities.CAPABILITY_NOT_APPLICABLE;
20 import static android.app.time.Capabilities.CAPABILITY_NOT_SUPPORTED;
21 import static android.app.time.Capabilities.CAPABILITY_POSSESSED;
22 
23 import android.app.time.TimeManager;
24 import android.app.time.TimeZoneCapabilities;
25 import android.app.time.TimeZoneCapabilitiesAndConfig;
26 import android.app.time.TimeZoneConfiguration;
27 import android.content.Context;
28 
29 import androidx.preference.Preference;
30 import androidx.preference.PreferenceScreen;
31 
32 import com.android.settings.R;
33 import com.android.settings.core.InstrumentedPreferenceFragment;
34 import com.android.settings.core.TogglePreferenceController;
35 import com.android.settingslib.core.lifecycle.LifecycleObserver;
36 import com.android.settingslib.core.lifecycle.events.OnStart;
37 import com.android.settingslib.core.lifecycle.events.OnStop;
38 
39 import java.util.concurrent.Executor;
40 
41 /**
42  * The controller for the "location time zone detection" entry in the Location settings
43  * screen.
44  */
45 public class LocationTimeZoneDetectionPreferenceController
46         extends TogglePreferenceController
47         implements LifecycleObserver, OnStart, OnStop, TimeManager.TimeZoneDetectorListener {
48 
49     private static final String TAG = "location_time_zone_detection";
50 
51     private final TimeManager mTimeManager;
52     private TimeZoneCapabilitiesAndConfig mTimeZoneCapabilitiesAndConfig;
53     private InstrumentedPreferenceFragment mFragment;
54     private Preference mPreference;
55 
LocationTimeZoneDetectionPreferenceController(Context context)56     public LocationTimeZoneDetectionPreferenceController(Context context) {
57         super(context, TAG);
58         mTimeManager = context.getSystemService(TimeManager.class);
59     }
60 
setFragment(InstrumentedPreferenceFragment fragment)61     void setFragment(InstrumentedPreferenceFragment fragment) {
62         mFragment = fragment;
63     }
64 
65     @Override
isChecked()66     public boolean isChecked() {
67         TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
68                 getTimeZoneCapabilitiesAndConfig(/*forceRefresh=*/false);
69         TimeZoneConfiguration configuration = capabilitiesAndConfig.getConfiguration();
70         return configuration.isGeoDetectionEnabled();
71     }
72 
73     @Override
setChecked(boolean isChecked)74     public boolean setChecked(boolean isChecked) {
75         TimeZoneCapabilitiesAndConfig timeZoneCapabilitiesAndConfig =
76                 getTimeZoneCapabilitiesAndConfig(/*forceRefresh=*/false);
77         boolean isLocationEnabled =
78                 timeZoneCapabilitiesAndConfig.getCapabilities().isUseLocationEnabled();
79         if (isChecked && !isLocationEnabled) {
80             new LocationToggleDisabledDialogFragment()
81                     .show(mFragment.getFragmentManager(), TAG);
82             // Toggle status is not updated.
83             return false;
84         } else {
85             TimeZoneConfiguration configuration = new TimeZoneConfiguration.Builder()
86                     .setGeoDetectionEnabled(isChecked)
87                     .build();
88             return mTimeManager.updateTimeZoneConfiguration(configuration);
89         }
90     }
91 
92     @Override
displayPreference(PreferenceScreen screen)93     public void displayPreference(PreferenceScreen screen) {
94         super.displayPreference(screen);
95         mPreference = screen.findPreference(getPreferenceKey());
96     }
97 
98     @Override
onStart()99     public void onStart() {
100         // Register for updates to the user's time zone capabilities or configuration which could
101         // require UI changes.
102         Executor mainExecutor = mContext.getMainExecutor();
103         mTimeManager.addTimeZoneDetectorListener(mainExecutor, this);
104         // Setup the initial state of the summary.
105         refreshUi();
106     }
107 
108     @Override
onStop()109     public void onStop() {
110         mTimeManager.removeTimeZoneDetectorListener(this);
111     }
112 
113     @Override
isSliceable()114     public boolean isSliceable() {
115         // Prevent use in a slice, which would enable search to display a toggle in the search
116         // results: LocationToggleDisabledDialogFragment has to be shown under some circumstances
117         // which doesn't work when embedded in search. b/185906072
118         return false;
119     }
120 
121     @Override
getSliceHighlightMenuRes()122     public int getSliceHighlightMenuRes() {
123         // not needed since it's not sliceable
124         return NO_RES;
125     }
126 
127     @Override
getAvailabilityStatus()128     public int getAvailabilityStatus() {
129         TimeZoneCapabilities timeZoneCapabilities =
130                 getTimeZoneCapabilitiesAndConfig(/* forceRefresh= */ false).getCapabilities();
131         int capability = timeZoneCapabilities.getConfigureGeoDetectionEnabledCapability();
132 
133         // The preference only has two states: present and not present. The preference is never
134         // present but disabled.
135         if (capability == CAPABILITY_NOT_SUPPORTED || capability == CAPABILITY_NOT_ALLOWED) {
136             return UNSUPPORTED_ON_DEVICE;
137         } else if (capability == CAPABILITY_NOT_APPLICABLE || capability == CAPABILITY_POSSESSED) {
138             return AVAILABLE;
139         } else {
140             throw new IllegalStateException("Unknown capability=" + capability);
141         }
142     }
143 
144     @Override
getSummary()145     public CharSequence getSummary() {
146         TimeZoneCapabilitiesAndConfig timeZoneCapabilitiesAndConfig =
147                 getTimeZoneCapabilitiesAndConfig(/* forceRefresh= */ false);
148         TimeZoneCapabilities capabilities = timeZoneCapabilitiesAndConfig.getCapabilities();
149         int configureGeoDetectionEnabledCapability =
150                 capabilities.getConfigureGeoDetectionEnabledCapability();
151         TimeZoneConfiguration configuration = timeZoneCapabilitiesAndConfig.getConfiguration();
152 
153         int summaryResId;
154         if (configureGeoDetectionEnabledCapability == CAPABILITY_NOT_SUPPORTED) {
155             // The preference should not be visible, but text is referenced in case this changes.
156             summaryResId = R.string.location_time_zone_detection_not_supported;
157         } else if (configureGeoDetectionEnabledCapability == CAPABILITY_NOT_ALLOWED) {
158             // The preference should not be visible, but text is referenced in case this changes.
159             summaryResId = R.string.location_time_zone_detection_not_allowed;
160         } else if (configureGeoDetectionEnabledCapability == CAPABILITY_NOT_APPLICABLE) {
161             boolean isLocationEnabled =
162                     timeZoneCapabilitiesAndConfig.getCapabilities().isUseLocationEnabled();
163             // The TimeZoneCapabilities cannot provide implementation-specific information about why
164             // the user doesn't have the capability, but the user's "location enabled" being off and
165             // the global automatic detection setting will always be considered overriding reasons
166             // why location time zone detection cannot be used.
167             if (!isLocationEnabled) {
168                 summaryResId = R.string.location_app_permission_summary_location_off;
169             } else if (!configuration.isAutoDetectionEnabled()) {
170                 summaryResId = R.string.location_time_zone_detection_auto_is_off;
171             } else {
172                 // This is in case there are other reasons in future why location time zone
173                 // detection is not applicable.
174                 summaryResId = R.string.location_time_zone_detection_not_applicable;
175             }
176         } else if (configureGeoDetectionEnabledCapability == CAPABILITY_POSSESSED) {
177             // If capability is possessed, toggle status already tells all the information needed.
178             // Returning null will make previous text stick on toggling.
179             // See AbstractPreferenceController#refreshSummary.
180             summaryResId = R.string.location_time_zone_detection_auto_is_on;
181         } else {
182             // This is unexpected: getAvailabilityStatus() should ensure that the UI element isn't
183             // even shown for known cases, or the capability is unknown.
184             throw new IllegalStateException("Unexpected configureGeoDetectionEnabledCapability="
185                     + configureGeoDetectionEnabledCapability);
186         }
187         return mContext.getString(summaryResId);
188     }
189 
190     /**
191      * Implementation of {@link TimeManager.TimeZoneDetectorListener#onChange()}. Called by the
192      * system server after a change that affects {@link TimeZoneCapabilitiesAndConfig}.
193      */
194     @Override
onChange()195     public void onChange() {
196         refreshUi();
197     }
198 
refreshUi()199     private void refreshUi() {
200         // Force a refresh of cached user capabilities and config before refreshing the summary.
201         getTimeZoneCapabilitiesAndConfig(/* forceRefresh= */ true);
202         refreshSummary(mPreference);
203     }
204 
205     /**
206      * Returns the current user capabilities and configuration. {@code forceRefresh} can be {@code
207      * true} to discard any cached copy.
208      */
getTimeZoneCapabilitiesAndConfig(boolean forceRefresh)209     private TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig(boolean forceRefresh) {
210         if (forceRefresh || mTimeZoneCapabilitiesAndConfig == null) {
211             mTimeZoneCapabilitiesAndConfig = mTimeManager.getTimeZoneCapabilitiesAndConfig();
212         }
213         return mTimeZoneCapabilitiesAndConfig;
214     }
215 }
216