• 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.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     private boolean mIsForWork;
125     @Nullable
126     private UserHandle mWorkProfileUser;
127     private int mMetricsCategory;
128 
129     /**
130      * Instantiate a controller as specified controller type and user-defined key.
131      * <p/>
132      * This is done through reflection. Do not use this method unless you know what you are doing.
133      */
createInstance(Context context, String controllerName, String key)134     public static BasePreferenceController createInstance(Context context,
135             String controllerName, String key) {
136         try {
137             final Class<?> clazz = Class.forName(controllerName);
138             final Constructor<?> preferenceConstructor =
139                     clazz.getConstructor(Context.class, String.class);
140             final Object[] params = new Object[]{context, key};
141             return (BasePreferenceController) preferenceConstructor.newInstance(params);
142         } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException |
143                 IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
144             throw new IllegalStateException(
145                     "Invalid preference controller: " + controllerName, e);
146         }
147     }
148 
149     /**
150      * Instantiate a controller as specified controller type.
151      * <p/>
152      * This is done through reflection. Do not use this method unless you know what you are doing.
153      */
createInstance(Context context, String controllerName)154     public static BasePreferenceController createInstance(Context context, String controllerName) {
155         try {
156             final Class<?> clazz = Class.forName(controllerName);
157             final Constructor<?> preferenceConstructor = clazz.getConstructor(Context.class);
158             final Object[] params = new Object[]{context};
159             return (BasePreferenceController) preferenceConstructor.newInstance(params);
160         } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException |
161                 IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
162             throw new IllegalStateException(
163                     "Invalid preference controller: " + controllerName, e);
164         }
165     }
166 
167     /**
168      * Instantiate a controller as specified controller type and work profile
169      * <p/>
170      * This is done through reflection. Do not use this method unless you know what you are doing.
171      *
172      * @param context        application context
173      * @param controllerName class name of the {@link BasePreferenceController}
174      * @param key            attribute android:key of the {@link Preference}
175      * @param isWorkProfile  is this controller only for work profile user?
176      */
createInstance(Context context, String controllerName, String key, boolean isWorkProfile)177     public static BasePreferenceController createInstance(Context context, String controllerName,
178             String key, boolean isWorkProfile) {
179         try {
180             final Class<?> clazz = Class.forName(controllerName);
181             final Constructor<?> preferenceConstructor =
182                     clazz.getConstructor(Context.class, String.class);
183             final Object[] params = new Object[]{context, key};
184             final BasePreferenceController controller =
185                     (BasePreferenceController) preferenceConstructor.newInstance(params);
186             controller.setForWork(isWorkProfile);
187             return controller;
188         } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException
189                 | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
190             throw new IllegalStateException(
191                     "Invalid preference controller: " + controllerName, e);
192         }
193     }
194 
BasePreferenceController(Context context, String preferenceKey)195     public BasePreferenceController(Context context, String preferenceKey) {
196         super(context);
197         mPreferenceKey = preferenceKey;
198         if (TextUtils.isEmpty(mPreferenceKey)) {
199             throw new IllegalArgumentException("Preference key must be set");
200         }
201     }
202 
203     /**
204      * @return {@link AvailabilityStatus} for the Setting. This status is used to determine if the
205      * Setting should be shown or disabled in Settings. Further, it can be used to produce
206      * appropriate error / warning Slice in the case of unavailability.
207      * </p>
208      * The status is used for the convenience methods: {@link #isAvailable()},
209      * {@link #isSupported()}
210      * </p>
211      * The inherited class doesn't need to check work profile if
212      * android:forWork="true" is set in preference xml.
213      */
214     @AvailabilityStatus
getAvailabilityStatus()215     public abstract int getAvailabilityStatus();
216 
217     @Override
getPreferenceKey()218     public String getPreferenceKey() {
219         return mPreferenceKey;
220     }
221 
222     @Override
getSliceUri()223     public Uri getSliceUri() {
224         return new Uri.Builder()
225                 .scheme(ContentResolver.SCHEME_CONTENT)
226                 // Default to non-platform authority. Platform Slices will override authority
227                 // accordingly.
228                 .authority(SettingsSliceProvider.SLICE_AUTHORITY)
229                 // Default to action based slices. Intent based slices will override accordingly.
230                 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
231                 .appendPath(getPreferenceKey())
232                 .build();
233     }
234 
235     /**
236      * @return {@code true} when the controller can be changed on the device.
237      *
238      * <p>
239      * Will return true for {@link #AVAILABLE} and {@link #DISABLED_DEPENDENT_SETTING}.
240      * <p>
241      * When the availability status returned by {@link #getAvailabilityStatus()} is
242      * {@link #DISABLED_DEPENDENT_SETTING}, then the setting will be disabled by default in the
243      * DashboardFragment, and it is up to the {@link BasePreferenceController} to enable the
244      * preference at the right time.
245      * <p>
246      * This function also check if work profile is existed when android:forWork="true" is set for
247      * the controller in preference xml.
248      * TODO (mfritze) Build a dependency mechanism to allow a controller to easily define the
249      * dependent setting.
250      */
251     @Override
isAvailable()252     public final boolean isAvailable() {
253         if (mIsForWork && mWorkProfileUser == null) {
254             return false;
255         }
256 
257         final int availabilityStatus = getAvailabilityStatus();
258         return (availabilityStatus == AVAILABLE
259                 || availabilityStatus == AVAILABLE_UNSEARCHABLE
260                 || availabilityStatus == DISABLED_DEPENDENT_SETTING);
261     }
262 
263     /**
264      * @return {@code false} if the setting is not applicable to the device. This covers both
265      * settings which were only introduced in future versions of android, or settings that have
266      * hardware dependencies.
267      * </p>
268      * Note that a return value of {@code true} does not mean that the setting is available.
269      */
isSupported()270     public final boolean isSupported() {
271         return getAvailabilityStatus() != UNSUPPORTED_ON_DEVICE;
272     }
273 
274     /**
275      * Displays preference in this controller.
276      */
277     @Override
displayPreference(PreferenceScreen screen)278     public void displayPreference(PreferenceScreen screen) {
279         super.displayPreference(screen);
280         if (getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
281             // Disable preference if it depends on another setting.
282             final Preference preference = screen.findPreference(getPreferenceKey());
283             if (preference != null) {
284                 preference.setEnabled(false);
285             }
286         }
287     }
288 
289     /**
290      * @return the UI type supported by the controller.
291      */
292     @SliceData.SliceType
getSliceType()293     public int getSliceType() {
294         return SliceData.SliceType.INTENT;
295     }
296 
297     /**
298      * Updates non-indexable keys for search provider.
299      *
300      * Called by SearchIndexProvider#getNonIndexableKeys
301      */
updateNonIndexableKeys(List<String> keys)302     public void updateNonIndexableKeys(List<String> keys) {
303         final boolean shouldSuppressFromSearch = !isAvailable()
304                 || getAvailabilityStatus() == AVAILABLE_UNSEARCHABLE;
305         if (shouldSuppressFromSearch) {
306             final String key = getPreferenceKey();
307             if (TextUtils.isEmpty(key)) {
308                 Log.w(TAG, "Skipping updateNonIndexableKeys due to empty key " + toString());
309                 return;
310             }
311             if (keys.contains(key)) {
312                 Log.w(TAG, "Skipping updateNonIndexableKeys, key already in list. " + toString());
313                 return;
314             }
315             keys.add(key);
316         }
317     }
318 
319     /**
320      * Indicates this controller is only for work profile user
321      */
setForWork(boolean forWork)322     void setForWork(boolean forWork) {
323         mIsForWork = forWork;
324         if (mIsForWork) {
325             mWorkProfileUser = Utils.getManagedProfile(UserManager.get(mContext));
326         }
327     }
328 
329     /**
330      * Launches the specified fragment for the work profile user if the associated
331      * {@link Preference} is clicked.  Otherwise just forward it to the super class.
332      *
333      * @param preference the preference being clicked.
334      * @return {@code true} if handled.
335      */
336     @Override
handlePreferenceTreeClick(Preference preference)337     public boolean handlePreferenceTreeClick(Preference preference) {
338         if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) {
339             return super.handlePreferenceTreeClick(preference);
340         }
341         if (!mIsForWork || mWorkProfileUser == null) {
342             return super.handlePreferenceTreeClick(preference);
343         }
344         final Bundle extra = preference.getExtras();
345         extra.putInt(EXTRA_USER_ID, mWorkProfileUser.getIdentifier());
346         new SubSettingLauncher(preference.getContext())
347                 .setDestination(preference.getFragment())
348                 .setSourceMetricsCategory(preference.getExtras().getInt(CATEGORY,
349                         SettingsEnums.PAGE_UNKNOWN))
350                 .setArguments(preference.getExtras())
351                 .setUserHandle(mWorkProfileUser)
352                 .launch();
353         return true;
354     }
355 
356     /**
357      * Updates raw data for search provider.
358      *
359      * Called by SearchIndexProvider#getRawDataToIndex
360      */
updateRawDataToIndex(List<SearchIndexableRaw> rawData)361     public void updateRawDataToIndex(List<SearchIndexableRaw> rawData) {
362     }
363 
364     /**
365      * Updates dynamic raw data for search provider.
366      *
367      * Called by SearchIndexProvider#getDynamicRawDataToIndex
368      */
updateDynamicRawDataToIndex(List<SearchIndexableRaw> rawData)369     public void updateDynamicRawDataToIndex(List<SearchIndexableRaw> rawData) {
370     }
371 
372     /**
373      * Set {@link UiBlockListener}
374      *
375      * @param uiBlockListener listener to set
376      */
setUiBlockListener(UiBlockListener uiBlockListener)377     public void setUiBlockListener(UiBlockListener uiBlockListener) {
378         mUiBlockListener = uiBlockListener;
379     }
380 
381     /**
382      * Listener to invoke when background job is finished
383      */
384     public interface UiBlockListener {
385         /**
386          * To notify client that UI related background work is finished.
387          * (i.e. Slice is fully loaded.)
388          *
389          * @param controller Controller that contains background work
390          */
onBlockerWorkFinished(BasePreferenceController controller)391         void onBlockerWorkFinished(BasePreferenceController controller);
392     }
393 
394     /**
395      * Used for {@link BasePreferenceController} to decide whether it is ui blocker.
396      * If it is, entire UI will be invisible for a certain period until controller
397      * invokes {@link UiBlockListener}
398      *
399      * This won't block UI thread however has similar side effect. Please use it if you
400      * want to avoid janky animation(i.e. new preference is added in the middle of page).
401      *
402      * This must be used in {@link BasePreferenceController}
403      */
404     public interface UiBlocker {
405     }
406 
407     /**
408      * Set the metrics category of the parent fragment.
409      *
410      * Called by DashboardFragment#onAttach
411      */
setMetricsCategory(int metricsCategory)412     public void setMetricsCategory(int metricsCategory) {
413         mMetricsCategory = metricsCategory;
414     }
415 
416     /**
417      * @return the metrics category of the parent fragment.
418      */
getMetricsCategory()419     protected int getMetricsCategory() {
420         return mMetricsCategory;
421     }
422 
423     /**
424      * @return Non-{@code null} {@link UserHandle} when a work profile is enabled.
425      * Otherwise {@code null}.
426      */
427     @Nullable
getWorkProfileUser()428     protected UserHandle getWorkProfileUser() {
429         return mWorkProfileUser;
430     }
431 }
432