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