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