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 }