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 android.annotation.IntDef;
17 import android.content.Context;
18 import android.text.TextUtils;
19 import android.util.Log;
20 
21 import androidx.preference.Preference;
22 import androidx.preference.PreferenceScreen;
23 
24 import com.android.settings.search.SearchIndexableRaw;
25 import com.android.settings.slices.SliceData;
26 import com.android.settings.slices.Sliceable;
27 import com.android.settingslib.core.AbstractPreferenceController;
28 
29 import java.lang.annotation.Retention;
30 import java.lang.annotation.RetentionPolicy;
31 import java.lang.reflect.Constructor;
32 import java.lang.reflect.InvocationTargetException;
33 import java.util.List;
34 
35 /**
36  * Abstract class to consolidate utility between preference controllers and act as an interface
37  * for Slices. The abstract classes that inherit from this class will act as the direct interfaces
38  * for each type when plugging into Slices.
39  */
40 public abstract class BasePreferenceController extends AbstractPreferenceController implements
41         Sliceable {
42 
43     private static final String TAG = "SettingsPrefController";
44 
45     /**
46      * Denotes the availability of the Setting.
47      * <p>
48      * Used both explicitly and by the convenience methods {@link #isAvailable()} and
49      * {@link #isSupported()}.
50      */
51     @Retention(RetentionPolicy.SOURCE)
52     @IntDef({AVAILABLE, AVAILABLE_UNSEARCHABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_USER,
53             DISABLED_DEPENDENT_SETTING, CONDITIONALLY_UNAVAILABLE})
54     public @interface AvailabilityStatus {
55     }
56 
57     /**
58      * The setting is available, and searchable to all search clients.
59      */
60     public static final int AVAILABLE = 0;
61 
62     /**
63      * The setting is available, but is not searchable to any search client.
64      */
65     public static final int AVAILABLE_UNSEARCHABLE = 1;
66 
67     /**
68      * A generic catch for settings which are currently unavailable, but may become available in
69      * the future. You should use {@link #DISABLED_FOR_USER} or {@link #DISABLED_DEPENDENT_SETTING}
70      * if they describe the condition more accurately.
71      */
72     public static final int CONDITIONALLY_UNAVAILABLE = 2;
73 
74     /**
75      * The setting is not, and will not supported by this device.
76      * <p>
77      * There is no guarantee that the setting page exists, and any links to the Setting should take
78      * you to the home page of Settings.
79      */
80     public static final int UNSUPPORTED_ON_DEVICE = 3;
81 
82 
83     /**
84      * The setting cannot be changed by the current user.
85      * <p>
86      * Links to the Setting should take you to the page of the Setting, even if it cannot be
87      * changed.
88      */
89     public static final int DISABLED_FOR_USER = 4;
90 
91     /**
92      * The setting has a dependency in the Settings App which is currently blocking access.
93      * <p>
94      * It must be possible for the Setting to be enabled by changing the configuration of the device
95      * settings. That is, a setting that cannot be changed because of the state of another setting.
96      * This should not be used for a setting that would be hidden from the UI entirely.
97      * <p>
98      * Correct use: Intensity of night display should be {@link #DISABLED_DEPENDENT_SETTING} when
99      * night display is off.
100      * Incorrect use: Mobile Data is {@link #DISABLED_DEPENDENT_SETTING} when there is no
101      * data-enabled sim.
102      * <p>
103      * Links to the Setting should take you to the page of the Setting, even if it cannot be
104      * changed.
105      */
106     public static final int DISABLED_DEPENDENT_SETTING = 5;
107 
108 
109     protected final String mPreferenceKey;
110     protected UiBlockListener mUiBlockListener;
111 
112     /**
113      * Instantiate a controller as specified controller type and user-defined key.
114      * <p/>
115      * This is done through reflection. Do not use this method unless you know what you are doing.
116      */
createInstance(Context context, String controllerName, String key)117     public static BasePreferenceController createInstance(Context context,
118             String controllerName, String key) {
119         try {
120             final Class<?> clazz = Class.forName(controllerName);
121             final Constructor<?> preferenceConstructor =
122                     clazz.getConstructor(Context.class, String.class);
123             final Object[] params = new Object[]{context, key};
124             return (BasePreferenceController) preferenceConstructor.newInstance(params);
125         } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException |
126                 IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
127             throw new IllegalStateException(
128                     "Invalid preference controller: " + controllerName, e);
129         }
130     }
131 
132     /**
133      * Instantiate a controller as specified controller type.
134      * <p/>
135      * This is done through reflection. Do not use this method unless you know what you are doing.
136      */
createInstance(Context context, String controllerName)137     public static BasePreferenceController createInstance(Context context, String controllerName) {
138         try {
139             final Class<?> clazz = Class.forName(controllerName);
140             final Constructor<?> preferenceConstructor = clazz.getConstructor(Context.class);
141             final Object[] params = new Object[]{context};
142             return (BasePreferenceController) preferenceConstructor.newInstance(params);
143         } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException |
144                 IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
145             throw new IllegalStateException(
146                     "Invalid preference controller: " + controllerName, e);
147         }
148     }
149 
BasePreferenceController(Context context, String preferenceKey)150     public BasePreferenceController(Context context, String preferenceKey) {
151         super(context);
152         mPreferenceKey = preferenceKey;
153         if (TextUtils.isEmpty(mPreferenceKey)) {
154             throw new IllegalArgumentException("Preference key must be set");
155         }
156     }
157 
158     /**
159      * @return {@AvailabilityStatus} for the Setting. This status is used to determine if the
160      * Setting should be shown or disabled in Settings. Further, it can be used to produce
161      * appropriate error / warning Slice in the case of unavailability.
162      * </p>
163      * The status is used for the convenience methods: {@link #isAvailable()},
164      * {@link #isSupported()}
165      */
166     @AvailabilityStatus
getAvailabilityStatus()167     public abstract int getAvailabilityStatus();
168 
169     @Override
getPreferenceKey()170     public String getPreferenceKey() {
171         return mPreferenceKey;
172     }
173 
174     /**
175      * @return {@code true} when the controller can be changed on the device.
176      *
177      * <p>
178      * Will return true for {@link #AVAILABLE} and {@link #DISABLED_DEPENDENT_SETTING}.
179      * <p>
180      * When the availability status returned by {@link #getAvailabilityStatus()} is
181      * {@link #DISABLED_DEPENDENT_SETTING}, then the setting will be disabled by default in the
182      * DashboardFragment, and it is up to the {@link BasePreferenceController} to enable the
183      * preference at the right time.
184      *
185      * TODO (mfritze) Build a dependency mechanism to allow a controller to easily define the
186      * dependent setting.
187      */
188     @Override
isAvailable()189     public final boolean isAvailable() {
190         final int availabilityStatus = getAvailabilityStatus();
191         return (availabilityStatus == AVAILABLE
192                 || availabilityStatus == AVAILABLE_UNSEARCHABLE
193                 || availabilityStatus == DISABLED_DEPENDENT_SETTING);
194     }
195 
196     /**
197      * @return {@code false} if the setting is not applicable to the device. This covers both
198      * settings which were only introduced in future versions of android, or settings that have
199      * hardware dependencies.
200      * </p>
201      * Note that a return value of {@code true} does not mean that the setting is available.
202      */
isSupported()203     public final boolean isSupported() {
204         return getAvailabilityStatus() != UNSUPPORTED_ON_DEVICE;
205     }
206 
207     /**
208      * Displays preference in this controller.
209      */
210     @Override
displayPreference(PreferenceScreen screen)211     public void displayPreference(PreferenceScreen screen) {
212         super.displayPreference(screen);
213         if (getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
214             // Disable preference if it depends on another setting.
215             final Preference preference = screen.findPreference(getPreferenceKey());
216             if (preference != null) {
217                 preference.setEnabled(false);
218             }
219         }
220     }
221 
222     /**
223      * @return the UI type supported by the controller.
224      */
225     @SliceData.SliceType
getSliceType()226     public int getSliceType() {
227         return SliceData.SliceType.INTENT;
228     }
229 
230     /**
231      * Updates non-indexable keys for search provider.
232      *
233      * Called by SearchIndexProvider#getNonIndexableKeys
234      */
updateNonIndexableKeys(List<String> keys)235     public void updateNonIndexableKeys(List<String> keys) {
236         final boolean shouldSuppressFromSearch = !isAvailable()
237                 || getAvailabilityStatus() == AVAILABLE_UNSEARCHABLE;
238         if (shouldSuppressFromSearch) {
239             final String key = getPreferenceKey();
240             if (TextUtils.isEmpty(key)) {
241                 Log.w(TAG, "Skipping updateNonIndexableKeys due to empty key " + toString());
242                 return;
243             }
244             if (keys.contains(key)) {
245                 Log.w(TAG, "Skipping updateNonIndexableKeys, key already in list. " + toString());
246                 return;
247             }
248             keys.add(key);
249         }
250     }
251 
252     /**
253      * Updates raw data for search provider.
254      *
255      * Called by SearchIndexProvider#getRawDataToIndex
256      */
updateRawDataToIndex(List<SearchIndexableRaw> rawData)257     public void updateRawDataToIndex(List<SearchIndexableRaw> rawData) {
258     }
259 
260     /**
261      * Set {@link UiBlockListener}
262      *
263      * @param uiBlockListener listener to set
264      */
setUiBlockListener(UiBlockListener uiBlockListener)265     public void setUiBlockListener(UiBlockListener uiBlockListener) {
266         mUiBlockListener = uiBlockListener;
267     }
268 
269     /**
270      * Listener to invoke when background job is finished
271      */
272     public interface UiBlockListener {
273         /**
274          * To notify client that UI related background work is finished.
275          * (i.e. Slice is fully loaded.)
276          *
277          * @param controller Controller that contains background work
278          */
onBlockerWorkFinished(BasePreferenceController controller)279         void onBlockerWorkFinished(BasePreferenceController controller);
280     }
281 
282     /**
283      * Used for {@link BasePreferenceController} to decide whether it is ui blocker.
284      * If it is, entire UI will be invisible for a certain period until controller
285      * invokes {@link UiBlockListener}
286      *
287      * This won't block UI thread however has similar side effect. Please use it if you
288      * want to avoid janky animation(i.e. new preference is added in the middle of page).
289      *
290      * This music be used in {@link BasePreferenceController}
291      */
292     public interface UiBlocker {
293     }
294 }