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