1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 package com.android.settings.core;
15 
16 import static android.content.Intent.EXTRA_USER_ID;
17 
18 import static com.android.settings.dashboard.DashboardFragment.CATEGORY;
19 
20 import android.annotation.IntDef;
21 import android.app.settings.SettingsEnums;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.net.Uri;
25 import android.os.Bundle;
26 import android.os.UserHandle;
27 import android.os.UserManager;
28 import android.provider.SettingsSlicesContract;
29 import android.text.TextUtils;
30 import android.util.Log;
31 
32 import androidx.annotation.Nullable;
33 import androidx.preference.Preference;
34 import androidx.preference.PreferenceScreen;
35 
36 import com.android.settings.Utils;
37 import com.android.settings.slices.SettingsSliceProvider;
38 import com.android.settings.slices.SliceData;
39 import com.android.settings.slices.Sliceable;
40 import com.android.settingslib.core.AbstractPreferenceController;
41 import com.android.settingslib.search.SearchIndexableRaw;
42 
43 import java.lang.annotation.Retention;
44 import java.lang.annotation.RetentionPolicy;
45 import java.lang.reflect.Constructor;
46 import java.lang.reflect.InvocationTargetException;
47 import java.util.List;
48 
49 /**
50  * Abstract class to consolidate utility between preference controllers and act as an interface
51  * for Slices. The abstract classes that inherit from this class will act as the direct interfaces
52  * for each type when plugging into Slices.
53  */
54 public abstract class BasePreferenceController extends AbstractPreferenceController implements
55         Sliceable {
56 
57     private static final String TAG = "SettingsPrefController";
58 
59     /**
60      * Denotes the availability of the Setting.
61      * <p>
62      * Used both explicitly and by the convenience methods {@link #isAvailable()} and
63      * {@link #isSupported()}.
64      */
65     @Retention(RetentionPolicy.SOURCE)
66     @IntDef({AVAILABLE, AVAILABLE_UNSEARCHABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_USER,
67             DISABLED_DEPENDENT_SETTING, CONDITIONALLY_UNAVAILABLE})
68     public @interface AvailabilityStatus {
69     }
70 
71     /**
72      * The setting is available, and searchable to all search clients.
73      */
74     public static final int AVAILABLE = 0;
75 
76     /**
77      * The setting is available, but is not searchable to any search client.
78      */
79     public static final int AVAILABLE_UNSEARCHABLE = 1;
80 
81     /**
82      * A generic catch for settings which are currently unavailable, but may become available in
83      * the future. You should use {@link #DISABLED_FOR_USER} or {@link #DISABLED_DEPENDENT_SETTING}
84      * if they describe the condition more accurately.
85      */
86     public static final int CONDITIONALLY_UNAVAILABLE = 2;
87 
88     /**
89      * The setting is not, and will not supported by this device.
90      * <p>
91      * There is no guarantee that the setting page exists, and any links to the Setting should take
92      * you to the home page of Settings.
93      */
94     public static final int UNSUPPORTED_ON_DEVICE = 3;
95 
96 
97     /**
98      * The setting cannot be changed by the current user.
99      * <p>
100      * Links to the Setting should take you to the page of the Setting, even if it cannot be
101      * changed.
102      */
103     public static final int DISABLED_FOR_USER = 4;
104 
105     /**
106      * The setting has a dependency in the Settings App which is currently blocking access.
107      * <p>
108      * It must be possible for the Setting to be enabled by changing the configuration of the device
109      * settings. That is, a setting that cannot be changed because of the state of another setting.
110      * This should not be used for a setting that would be hidden from the UI entirely.
111      * <p>
112      * Correct use: Intensity of night display should be {@link #DISABLED_DEPENDENT_SETTING} when
113      * night display is off.
114      * Incorrect use: Mobile Data is {@link #DISABLED_DEPENDENT_SETTING} when there is no
115      * data-enabled sim.
116      * <p>
117      * Links to the Setting should take you to the page of the Setting, even if it cannot be
118      * changed.
119      */
120     public static final int DISABLED_DEPENDENT_SETTING = 5;
121 
122     protected final String mPreferenceKey;
123     protected UiBlockListener mUiBlockListener;
124     protected boolean mUiBlockerFinished;
125     private boolean mIsForWork;
126     @Nullable
127     private UserHandle mWorkProfileUser;
128     private int mMetricsCategory;
129     private boolean mPrefVisibility;
130 
131     /**
132      * Instantiate a controller as specified controller type and user-defined key.
133      * <p/>
134      * This is done through reflection. Do not use this method unless you know what you are doing.
135      */
createInstance(Context context, String controllerName, String key)136     public static BasePreferenceController createInstance(Context context,
137             String controllerName, String key) {
138         try {
139             final Class<?> clazz = Class.forName(controllerName);
140             final Constructor<?> preferenceConstructor =
141                     clazz.getConstructor(Context.class, String.class);
142             final Object[] params = new Object[]{context, key};
143             return (BasePreferenceController) preferenceConstructor.newInstance(params);
144         } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException |
145                 IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
146             throw new IllegalStateException(
147                     "Invalid preference controller: " + controllerName, e);
148         }
149     }
150 
151     /**
152      * Instantiate a controller as specified controller type.
153      * <p/>
154      * This is done through reflection. Do not use this method unless you know what you are doing.
155      */
createInstance(Context context, String controllerName)156     public static BasePreferenceController createInstance(Context context, String controllerName) {
157         try {
158             final Class<?> clazz = Class.forName(controllerName);
159             final Constructor<?> preferenceConstructor = clazz.getConstructor(Context.class);
160             final Object[] params = new Object[]{context};
161             return (BasePreferenceController) preferenceConstructor.newInstance(params);
162         } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException |
163                 IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
164             throw new IllegalStateException(
165                     "Invalid preference controller: " + controllerName, e);
166         }
167     }
168 
169     /**
170      * Instantiate a controller as specified controller type and work profile
171      * <p/>
172      * This is done through reflection. Do not use this method unless you know what you are doing.
173      *
174      * @param context        application context
175      * @param controllerName class name of the {@link BasePreferenceController}
176      * @param key            attribute android:key of the {@link Preference}
177      * @param isWorkProfile  is this controller only for work profile user?
178      */
createInstance(Context context, String controllerName, String key, boolean isWorkProfile)179     public static BasePreferenceController createInstance(Context context, String controllerName,
180             String key, boolean isWorkProfile) {
181         try {
182             final Class<?> clazz = Class.forName(controllerName);
183             final Constructor<?> preferenceConstructor =
184                     clazz.getConstructor(Context.class, String.class);
185             final Object[] params = new Object[]{context, key};
186             final BasePreferenceController controller =
187                     (BasePreferenceController) preferenceConstructor.newInstance(params);
188             controller.setForWork(isWorkProfile);
189             return controller;
190         } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException
191                 | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
192             throw new IllegalStateException(
193                     "Invalid preference controller: " + controllerName, e);
194         }
195     }
196 
BasePreferenceController(Context context, String preferenceKey)197     public BasePreferenceController(Context context, String preferenceKey) {
198         super(context);
199         mPreferenceKey = preferenceKey;
200         mPrefVisibility = true;
201         if (TextUtils.isEmpty(mPreferenceKey)) {
202             throw new IllegalArgumentException("Preference key must be set");
203         }
204     }
205 
206     /**
207      * @return {@link AvailabilityStatus} for the Setting. This status is used to determine if the
208      * Setting should be shown or disabled in Settings. Further, it can be used to produce
209      * appropriate error / warning Slice in the case of unavailability.
210      * </p>
211      * The status is used for the convenience methods: {@link #isAvailable()},
212      * {@link #isSupported()}
213      * </p>
214      * The inherited class doesn't need to check work profile if
215      * android:forWork="true" is set in preference xml.
216      */
217     @AvailabilityStatus
getAvailabilityStatus()218     public abstract int getAvailabilityStatus();
219 
220     @Override
getPreferenceKey()221     public String getPreferenceKey() {
222         return mPreferenceKey;
223     }
224 
225     @Override
getSliceUri()226     public Uri getSliceUri() {
227         return new Uri.Builder()
228                 .scheme(ContentResolver.SCHEME_CONTENT)
229                 // Default to non-platform authority. Platform Slices will override authority
230                 // accordingly.
231                 .authority(SettingsSliceProvider.SLICE_AUTHORITY)
232                 // Default to action based slices. Intent based slices will override accordingly.
233                 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
234                 .appendPath(getPreferenceKey())
235                 .build();
236     }
237 
238     /**
239      * @return {@code true} when the controller can be changed on the device.
240      *
241      * <p>
242      * Will return true for {@link #AVAILABLE} and {@link #DISABLED_DEPENDENT_SETTING}.
243      * <p>
244      * When the availability status returned by {@link #getAvailabilityStatus()} is
245      * {@link #DISABLED_DEPENDENT_SETTING}, then the setting will be disabled by default in the
246      * DashboardFragment, and it is up to the {@link BasePreferenceController} to enable the
247      * preference at the right time.
248      * <p>
249      * This function also check if work profile is existed when android:forWork="true" is set for
250      * the controller in preference xml.
251      * TODO (mfritze) Build a dependency mechanism to allow a controller to easily define the
252      * dependent setting.
253      */
254     @Override
isAvailable()255     public final boolean isAvailable() {
256         if (mIsForWork && mWorkProfileUser == null) {
257             return false;
258         }
259 
260         final int availabilityStatus = getAvailabilityStatus();
261         return (availabilityStatus == AVAILABLE
262                 || availabilityStatus == AVAILABLE_UNSEARCHABLE
263                 || availabilityStatus == DISABLED_DEPENDENT_SETTING);
264     }
265 
266     /**
267      * @return {@code false} if the setting is not applicable to the device. This covers both
268      * settings which were only introduced in future versions of android, or settings that have
269      * hardware dependencies.
270      * </p>
271      * Note that a return value of {@code true} does not mean that the setting is available.
272      */
isSupported()273     public final boolean isSupported() {
274         return getAvailabilityStatus() != UNSUPPORTED_ON_DEVICE;
275     }
276 
277     /**
278      * Displays preference in this controller.
279      */
280     @Override
displayPreference(PreferenceScreen screen)281     public void displayPreference(PreferenceScreen screen) {
282         super.displayPreference(screen);
283         if (getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
284             // Disable preference if it depends on another setting.
285             final Preference preference = screen.findPreference(getPreferenceKey());
286             if (preference != null) {
287                 preference.setEnabled(false);
288             }
289         }
290     }
291 
292     /**
293      * @return the UI type supported by the controller.
294      */
295     @SliceData.SliceType
getSliceType()296     public int getSliceType() {
297         return SliceData.SliceType.INTENT;
298     }
299 
300     /**
301      * Updates non-indexable keys for search provider.
302      *
303      * Called by SearchIndexProvider#getNonIndexableKeys
304      */
updateNonIndexableKeys(List<String> keys)305     public void updateNonIndexableKeys(List<String> keys) {
306         final boolean shouldSuppressFromSearch = !isAvailable()
307                 || getAvailabilityStatus() == AVAILABLE_UNSEARCHABLE;
308         if (shouldSuppressFromSearch) {
309             final String key = getPreferenceKey();
310             if (TextUtils.isEmpty(key)) {
311                 Log.w(TAG, "Skipping updateNonIndexableKeys due to empty key " + toString());
312                 return;
313             }
314             if (keys.contains(key)) {
315                 Log.w(TAG, "Skipping updateNonIndexableKeys, key already in list. " + toString());
316                 return;
317             }
318             keys.add(key);
319         }
320     }
321 
322     /**
323      * Indicates this controller is only for work profile user
324      */
setForWork(boolean forWork)325     void setForWork(boolean forWork) {
326         mIsForWork = forWork;
327         if (mIsForWork) {
328             mWorkProfileUser = Utils.getManagedProfile(UserManager.get(mContext));
329         }
330     }
331 
332     /**
333      * Launches the specified fragment for the work profile user if the associated
334      * {@link Preference} is clicked.  Otherwise just forward it to the super class.
335      *
336      * @param preference the preference being clicked.
337      * @return {@code true} if handled.
338      */
339     @Override
handlePreferenceTreeClick(Preference preference)340     public boolean handlePreferenceTreeClick(Preference preference) {
341         if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) {
342             return super.handlePreferenceTreeClick(preference);
343         }
344         if (!mIsForWork || mWorkProfileUser == null) {
345             return super.handlePreferenceTreeClick(preference);
346         }
347         final Bundle extra = preference.getExtras();
348         extra.putInt(EXTRA_USER_ID, mWorkProfileUser.getIdentifier());
349         new SubSettingLauncher(preference.getContext())
350                 .setDestination(preference.getFragment())
351                 .setSourceMetricsCategory(preference.getExtras().getInt(CATEGORY,
352                         SettingsEnums.PAGE_UNKNOWN))
353                 .setArguments(preference.getExtras())
354                 .setUserHandle(mWorkProfileUser)
355                 .launch();
356         return true;
357     }
358 
359     /**
360      * Updates raw data for search provider.
361      *
362      * Called by SearchIndexProvider#getRawDataToIndex
363      */
updateRawDataToIndex(List<SearchIndexableRaw> rawData)364     public void updateRawDataToIndex(List<SearchIndexableRaw> rawData) {
365     }
366 
367     /**
368      * Updates dynamic raw data for search provider.
369      *
370      * Called by SearchIndexProvider#getDynamicRawDataToIndex
371      */
updateDynamicRawDataToIndex(List<SearchIndexableRaw> rawData)372     public void updateDynamicRawDataToIndex(List<SearchIndexableRaw> rawData) {
373     }
374 
375     /**
376      * Set {@link UiBlockListener}
377      *
378      * @param uiBlockListener listener to set
379      */
setUiBlockListener(UiBlockListener uiBlockListener)380     public void setUiBlockListener(UiBlockListener uiBlockListener) {
381         mUiBlockListener = uiBlockListener;
382     }
383 
setUiBlockerFinished(boolean isFinished)384     public void setUiBlockerFinished(boolean isFinished) {
385         mUiBlockerFinished = isFinished;
386     }
387 
getSavedPrefVisibility()388     public boolean getSavedPrefVisibility() {
389         return mPrefVisibility;
390     }
391 
392     /**
393      * Listener to invoke when background job is finished
394      */
395     public interface UiBlockListener {
396         /**
397          * To notify client that UI related background work is finished.
398          * (i.e. Slice is fully loaded.)
399          *
400          * @param controller Controller that contains background work
401          */
onBlockerWorkFinished(BasePreferenceController controller)402         void onBlockerWorkFinished(BasePreferenceController controller);
403     }
404 
405     /**
406      * Used for {@link BasePreferenceController} to decide whether it is ui blocker.
407      * If it is, entire UI will be invisible for a certain period until controller
408      * invokes {@link UiBlockListener}
409      *
410      * This won't block UI thread however has similar side effect. Please use it if you
411      * want to avoid janky animation(i.e. new preference is added in the middle of page).
412      *
413      * This must be used in {@link BasePreferenceController}
414      */
415     public interface UiBlocker {
416     }
417 
418     /**
419      * Set the metrics category of the parent fragment.
420      *
421      * Called by DashboardFragment#onAttach
422      */
setMetricsCategory(int metricsCategory)423     public void setMetricsCategory(int metricsCategory) {
424         mMetricsCategory = metricsCategory;
425     }
426 
427     /**
428      * @return the metrics category of the parent fragment.
429      */
getMetricsCategory()430     protected int getMetricsCategory() {
431         return mMetricsCategory;
432     }
433 
434     /**
435      * @return Non-{@code null} {@link UserHandle} when a work profile is enabled.
436      * Otherwise {@code null}.
437      */
438     @Nullable
getWorkProfileUser()439     protected UserHandle getWorkProfileUser() {
440         return mWorkProfileUser;
441     }
442 
443     /**
444      * Used for {@link BasePreferenceController} that implements {@link UiBlocker} to control the
445      * preference visibility.
446      */
updatePreferenceVisibilityDelegate(Preference preference, boolean isVisible)447     protected void updatePreferenceVisibilityDelegate(Preference preference, boolean isVisible) {
448         if (mUiBlockerFinished) {
449             preference.setVisible(isVisible);
450             return;
451         }
452 
453         savePrefVisibility(isVisible);
454 
455         // Preferences that should be invisible have a high priority to be updated since the
456         // whole UI should be blocked/invisible. While those that should be visible will be
457         // updated once the blocker work is finished. That's done in DashboardFragment.
458         if (!isVisible) {
459             preference.setVisible(false);
460         }
461     }
462 
savePrefVisibility(boolean isVisible)463     private void savePrefVisibility(boolean isVisible) {
464         mPrefVisibility = isVisible;
465     }
466 }
467