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.settings.display;
18 
19 import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
20 import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT;
21 
22 import android.app.admin.DevicePolicyManager;
23 import android.app.settings.SettingsEnums;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.res.Resources;
29 import android.graphics.drawable.Drawable;
30 import android.hardware.SensorPrivacyManager;
31 import android.os.PowerManager;
32 import android.os.UserHandle;
33 import android.provider.Settings;
34 import android.text.SpannableString;
35 import android.text.Spanned;
36 import android.text.style.ClickableSpan;
37 import android.util.Log;
38 import android.view.View;
39 
40 import androidx.annotation.NonNull;
41 import androidx.preference.Preference;
42 import androidx.preference.PreferenceScreen;
43 
44 import com.android.settings.R;
45 import com.android.settings.overlay.FeatureFactory;
46 import com.android.settings.search.BaseSearchIndexProvider;
47 import com.android.settings.support.actionbar.HelpResourceProvider;
48 import com.android.settings.widget.RadioButtonPickerFragment;
49 import com.android.settingslib.RestrictedLockUtils;
50 import com.android.settingslib.RestrictedLockUtilsInternal;
51 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
52 import com.android.settingslib.search.SearchIndexable;
53 import com.android.settingslib.search.SearchIndexableRaw;
54 import com.android.settingslib.widget.CandidateInfo;
55 import com.android.settingslib.widget.FooterPreference;
56 import com.android.settingslib.widget.RadioButtonPreference;
57 
58 import com.google.common.annotations.VisibleForTesting;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 
63 /**
64  * Fragment that is used to control screen timeout.
65  */
66 @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
67 public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements
68         HelpResourceProvider {
69     private static final String TAG = "ScreenTimeout";
70     /** If there is no setting in the provider, use this. */
71     public static final int FALLBACK_SCREEN_TIMEOUT_VALUE = 30000;
72 
73     private static final int DEFAULT_ORDER_OF_LOWEST_PREFERENCE = Integer.MAX_VALUE - 1;
74 
75     private CharSequence[] mInitialEntries;
76     private CharSequence[] mInitialValues;
77     private FooterPreference mPrivacyPreference;
78     private final MetricsFeatureProvider mMetricsFeatureProvider;
79     private SensorPrivacyManager mPrivacyManager;
80     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
81         @Override
82         public void onReceive(Context context, Intent intent) {
83             mAdaptiveSleepBatterySaverPreferenceController.updateVisibility();
84             mAdaptiveSleepController.updatePreference();
85         }
86     };
87 
88     @VisibleForTesting
89     Context mContext;
90 
91     @VisibleForTesting
92     RestrictedLockUtils.EnforcedAdmin mAdmin;
93 
94     @VisibleForTesting
95     Preference mDisableOptionsPreference;
96 
97     @VisibleForTesting
98     AdaptiveSleepPermissionPreferenceController mAdaptiveSleepPermissionController;
99 
100     @VisibleForTesting
101     AdaptiveSleepCameraStatePreferenceController mAdaptiveSleepCameraStatePreferenceController;
102 
103     @VisibleForTesting
104     AdaptiveSleepPreferenceController mAdaptiveSleepController;
105 
106     @VisibleForTesting
107     AdaptiveSleepBatterySaverPreferenceController mAdaptiveSleepBatterySaverPreferenceController;
108 
ScreenTimeoutSettings()109     public ScreenTimeoutSettings() {
110         super();
111         mMetricsFeatureProvider = FeatureFactory.getFactory(getContext())
112                 .getMetricsFeatureProvider();
113     }
114 
115     @Override
onAttach(Context context)116     public void onAttach(Context context) {
117         super.onAttach(context);
118         mContext = context;
119         mInitialEntries = getResources().getStringArray(R.array.screen_timeout_entries);
120         mInitialValues = getResources().getStringArray(R.array.screen_timeout_values);
121         mAdaptiveSleepController = new AdaptiveSleepPreferenceController(context);
122         mAdaptiveSleepPermissionController = new AdaptiveSleepPermissionPreferenceController(
123                 context);
124         mAdaptiveSleepCameraStatePreferenceController =
125                 new AdaptiveSleepCameraStatePreferenceController(context);
126         mAdaptiveSleepBatterySaverPreferenceController =
127                 new AdaptiveSleepBatterySaverPreferenceController(context);
128         mPrivacyPreference = new FooterPreference(context);
129         mPrivacyPreference.setIcon(R.drawable.ic_privacy_shield_24dp);
130         mPrivacyPreference.setTitle(R.string.adaptive_sleep_privacy);
131         mPrivacyPreference.setSelectable(false);
132         mPrivacyPreference.setLayoutResource(R.layout.preference_footer);
133         mPrivacyManager = SensorPrivacyManager.getInstance(context);
134         mPrivacyManager.addSensorPrivacyListener(CAMERA,
135                 (sensor, enabled) -> mAdaptiveSleepController.updatePreference());
136     }
137 
138     @Override
getCandidates()139     protected List<? extends CandidateInfo> getCandidates() {
140         final List<CandidateInfo> candidates = new ArrayList<>();
141         final long maxTimeout = getMaxScreenTimeout(getContext());
142         if (mInitialValues != null) {
143             for (int i = 0; i < mInitialValues.length; ++i) {
144                 if (Long.parseLong(mInitialValues[i].toString()) <= maxTimeout) {
145                     candidates.add(new TimeoutCandidateInfo(mInitialEntries[i],
146                             mInitialValues[i].toString(), true));
147                 }
148             }
149         } else {
150             Log.e(TAG, "Screen timeout options do not exist.");
151         }
152         return candidates;
153     }
154 
155     @Override
onStart()156     public void onStart() {
157         super.onStart();
158         mAdaptiveSleepPermissionController.updateVisibility();
159         mAdaptiveSleepCameraStatePreferenceController.updateVisibility();
160         mAdaptiveSleepBatterySaverPreferenceController.updateVisibility();
161         mAdaptiveSleepController.updatePreference();
162         mContext.registerReceiver(mReceiver,
163                 new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
164     }
165 
166     @Override
onStop()167     public void onStop() {
168         super.onStop();
169         mContext.unregisterReceiver(mReceiver);
170     }
171 
172     @Override
updateCandidates()173     public void updateCandidates() {
174         final String defaultKey = getDefaultKey();
175         final PreferenceScreen screen = getPreferenceScreen();
176         screen.removeAll();
177 
178         final List<? extends CandidateInfo> candidateList = getCandidates();
179         if (candidateList == null) {
180             return;
181         }
182 
183         for (CandidateInfo info : candidateList) {
184             RadioButtonPreference pref =
185                     new RadioButtonPreference(getPrefContext());
186             bindPreference(pref, info.getKey(), info, defaultKey);
187             screen.addPreference(pref);
188         }
189 
190         final long selectedTimeout = Long.parseLong(defaultKey);
191         final long maxTimeout = getMaxScreenTimeout(getContext());
192         if (!candidateList.isEmpty() && (selectedTimeout > maxTimeout)) {
193             // The selected time out value is longer than the max timeout allowed by the admin.
194             // Select the largest value from the list by default.
195             final RadioButtonPreference preferenceWithLargestTimeout =
196                     (RadioButtonPreference) screen.getPreference(candidateList.size() - 1);
197             preferenceWithLargestTimeout.setChecked(true);
198         }
199 
200         mPrivacyPreference = new FooterPreference(mContext);
201         mPrivacyPreference.setIcon(R.drawable.ic_privacy_shield_24dp);
202         mPrivacyPreference.setTitle(R.string.adaptive_sleep_privacy);
203         mPrivacyPreference.setSelectable(false);
204         mPrivacyPreference.setLayoutResource(R.layout.preference_footer);
205 
206         if (isScreenAttentionAvailable(getContext())) {
207             mAdaptiveSleepPermissionController.addToScreen(screen);
208             mAdaptiveSleepCameraStatePreferenceController.addToScreen(screen);
209             mAdaptiveSleepBatterySaverPreferenceController.addToScreen(screen);
210             mAdaptiveSleepController.addToScreen(screen);
211             screen.addPreference(mPrivacyPreference);
212         }
213 
214         if (mAdmin != null) {
215             setupDisabledFooterPreference();
216             screen.addPreference(mDisableOptionsPreference);
217         }
218     }
219 
220     @VisibleForTesting
setupDisabledFooterPreference()221     void setupDisabledFooterPreference() {
222         final String textDisabledByAdmin = getResources().getString(
223                 R.string.admin_disabled_other_options);
224         final String textMoreDetails = getResources().getString(R.string.admin_more_details);
225 
226         final SpannableString spannableString = new SpannableString(
227                 textDisabledByAdmin + System.lineSeparator()
228                         + System.lineSeparator() + textMoreDetails);
229         final ClickableSpan clickableSpan = new ClickableSpan() {
230             @Override
231             public void onClick(@NonNull View widget) {
232                 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), mAdmin);
233             }
234         };
235 
236         if (textDisabledByAdmin != null && textMoreDetails != null) {
237             spannableString.setSpan(clickableSpan, textDisabledByAdmin.length() + 1,
238                     textDisabledByAdmin.length() + textMoreDetails.length() + 2,
239                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
240         }
241 
242         mDisableOptionsPreference = new FooterPreference(getContext());
243         mDisableOptionsPreference.setLayoutResource(R.layout.preference_footer);
244         mDisableOptionsPreference.setTitle(spannableString);
245         mDisableOptionsPreference.setSelectable(false);
246         mDisableOptionsPreference.setIcon(R.drawable.ic_info_outline_24dp);
247 
248         // The 'disabled by admin' preference should always be at the end of the setting page.
249         mDisableOptionsPreference.setOrder(DEFAULT_ORDER_OF_LOWEST_PREFERENCE);
250         mPrivacyPreference.setOrder(DEFAULT_ORDER_OF_LOWEST_PREFERENCE - 1);
251     }
252 
253     @Override
getDefaultKey()254     protected String getDefaultKey() {
255         return getCurrentSystemScreenTimeout(getContext());
256     }
257 
258     @Override
setDefaultKey(String key)259     protected boolean setDefaultKey(String key) {
260         setCurrentSystemScreenTimeout(getContext(), key);
261         return true;
262     }
263 
264     @Override
getMetricsCategory()265     public int getMetricsCategory() {
266         return SettingsEnums.SCREEN_TIMEOUT;
267     }
268 
269     @Override
getPreferenceScreenResId()270     protected int getPreferenceScreenResId() {
271         return R.xml.screen_timeout_settings;
272     }
273 
274     @Override
getHelpResource()275     public int getHelpResource() {
276         return R.string.help_url_adaptive_sleep;
277     }
278 
getMaxScreenTimeout(Context context)279     private Long getMaxScreenTimeout(Context context) {
280         if (context == null) {
281             return Long.MAX_VALUE;
282         }
283         final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
284         if (dpm == null) {
285             return Long.MAX_VALUE;
286         }
287         mAdmin = RestrictedLockUtilsInternal.checkIfMaximumTimeToLockIsSet(context);
288         if (mAdmin != null) {
289             return dpm.getMaximumTimeToLock(null /* admin */, UserHandle.myUserId());
290         }
291         return Long.MAX_VALUE;
292     }
293 
getCurrentSystemScreenTimeout(Context context)294     private String getCurrentSystemScreenTimeout(Context context) {
295         if (context == null) {
296             return Long.toString(FALLBACK_SCREEN_TIMEOUT_VALUE);
297         } else {
298             return Long.toString(Settings.System.getLong(context.getContentResolver(),
299                     SCREEN_OFF_TIMEOUT, FALLBACK_SCREEN_TIMEOUT_VALUE));
300         }
301     }
302 
setCurrentSystemScreenTimeout(Context context, String key)303     private void setCurrentSystemScreenTimeout(Context context, String key) {
304         try {
305             if (context != null) {
306                 final long value = Long.parseLong(key);
307                 mMetricsFeatureProvider.action(context, SettingsEnums.ACTION_SCREEN_TIMEOUT_CHANGED,
308                         (int) value);
309                 Settings.System.putLong(context.getContentResolver(), SCREEN_OFF_TIMEOUT, value);
310             }
311         } catch (NumberFormatException e) {
312             Log.e(TAG, "could not persist screen timeout setting", e);
313         }
314     }
315 
isScreenAttentionAvailable(Context context)316     private static boolean isScreenAttentionAvailable(Context context) {
317         return AdaptiveSleepPreferenceController.isAdaptiveSleepSupported(context);
318     }
319 
320     private static class TimeoutCandidateInfo extends CandidateInfo {
321         private final CharSequence mLabel;
322         private final String mKey;
323 
TimeoutCandidateInfo(CharSequence label, String key, boolean enabled)324         TimeoutCandidateInfo(CharSequence label, String key, boolean enabled) {
325             super(enabled);
326             mLabel = label;
327             mKey = key;
328         }
329 
330         @Override
loadLabel()331         public CharSequence loadLabel() {
332             return mLabel;
333         }
334 
335         @Override
loadIcon()336         public Drawable loadIcon() {
337             return null;
338         }
339 
340         @Override
getKey()341         public String getKey() {
342             return mKey;
343         }
344     }
345 
346     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
347             new BaseSearchIndexProvider(R.xml.screen_timeout_settings) {
348                 public List<SearchIndexableRaw> getRawDataToIndex(Context context,
349                         boolean enabled) {
350                     if (!isScreenAttentionAvailable(context)) {
351                         return null;
352                     }
353                     final Resources res = context.getResources();
354                     final SearchIndexableRaw data = new SearchIndexableRaw(context);
355                     data.title = res.getString(R.string.adaptive_sleep_title);
356                     data.key = AdaptiveSleepPreferenceController.PREFERENCE_KEY;
357                     data.keywords = res.getString(R.string.adaptive_sleep_title);
358 
359                     final List<SearchIndexableRaw> result = new ArrayList<>(1);
360                     result.add(data);
361                     return result;
362                 }
363             };
364 }
365