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.NonNull; 33 import androidx.annotation.Nullable; 34 import androidx.preference.Preference; 35 import androidx.preference.PreferenceScreen; 36 37 import com.android.settings.Utils; 38 import com.android.settings.slices.SettingsSliceProvider; 39 import com.android.settings.slices.SliceData; 40 import com.android.settings.slices.Sliceable; 41 import com.android.settingslib.core.AbstractPreferenceController; 42 import com.android.settingslib.search.SearchIndexableRaw; 43 44 import java.lang.annotation.Retention; 45 import java.lang.annotation.RetentionPolicy; 46 import java.lang.reflect.Constructor; 47 import java.lang.reflect.InvocationTargetException; 48 import java.util.List; 49 50 /** 51 * Abstract class to consolidate utility between preference controllers and act as an interface 52 * for Slices. The abstract classes that inherit from this class will act as the direct interfaces 53 * for each type when plugging into Slices. 54 */ 55 public abstract class BasePreferenceController extends AbstractPreferenceController implements 56 Sliceable { 57 58 private static final String TAG = "SettingsPrefController"; 59 60 /** 61 * Denotes the availability of the Setting. 62 * <p> 63 * Used both explicitly and by the convenience methods {@link #isAvailable()} and 64 * {@link #isSupported()}. 65 */ 66 @Retention(RetentionPolicy.SOURCE) 67 @IntDef({AVAILABLE, AVAILABLE_UNSEARCHABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_USER, 68 DISABLED_DEPENDENT_SETTING, CONDITIONALLY_UNAVAILABLE}) 69 public @interface AvailabilityStatus { 70 } 71 72 /** 73 * The setting is available, and searchable to all search clients. 74 */ 75 public static final int AVAILABLE = 0; 76 77 /** 78 * The setting is available, but is not searchable to any search client. 79 */ 80 public static final int AVAILABLE_UNSEARCHABLE = 1; 81 82 /** 83 * A generic catch for settings which are currently unavailable, but may become available in 84 * the future. You should use {@link #DISABLED_FOR_USER} or {@link #DISABLED_DEPENDENT_SETTING} 85 * if they describe the condition more accurately. 86 */ 87 public static final int CONDITIONALLY_UNAVAILABLE = 2; 88 89 /** 90 * The setting is not, and will not supported by this device. 91 * <p> 92 * There is no guarantee that the setting page exists, and any links to the Setting should take 93 * you to the home page of Settings. 94 */ 95 public static final int UNSUPPORTED_ON_DEVICE = 3; 96 97 98 /** 99 * The setting cannot be changed by the current user. 100 * <p> 101 * Links to the Setting should take you to the page of the Setting, even if it cannot be 102 * changed. 103 */ 104 public static final int DISABLED_FOR_USER = 4; 105 106 /** 107 * The setting has a dependency in the Settings App which is currently blocking access. 108 * <p> 109 * It must be possible for the Setting to be enabled by changing the configuration of the device 110 * settings. That is, a setting that cannot be changed because of the state of another setting. 111 * This should not be used for a setting that would be hidden from the UI entirely. 112 * <p> 113 * Correct use: Intensity of night display should be {@link #DISABLED_DEPENDENT_SETTING} when 114 * night display is off. 115 * Incorrect use: Mobile Data is {@link #DISABLED_DEPENDENT_SETTING} when there is no 116 * data-enabled sim. 117 * <p> 118 * Links to the Setting should take you to the page of the Setting, even if it cannot be 119 * changed. 120 */ 121 public static final int DISABLED_DEPENDENT_SETTING = 5; 122 123 @NonNull 124 protected final String mPreferenceKey; 125 @Nullable 126 protected UiBlockListener mUiBlockListener; 127 protected boolean mUiBlockerFinished; 128 private boolean mIsForWork; 129 @Nullable 130 private UserHandle mWorkProfileUser; 131 private int mMetricsCategory; 132 private boolean mPrefVisibility; 133 134 /** 135 * Instantiate a controller as specified controller type and user-defined key. 136 * <p/> 137 * This is done through reflection. Do not use this method unless you know what you are doing. 138 */ createInstance(Context context, String controllerName, String key)139 public static BasePreferenceController createInstance(Context context, 140 String controllerName, String key) { 141 try { 142 final Class<?> clazz = Class.forName(controllerName); 143 final Constructor<?> preferenceConstructor = 144 clazz.getConstructor(Context.class, String.class); 145 final Object[] params = new Object[]{context, key}; 146 return (BasePreferenceController) preferenceConstructor.newInstance(params); 147 } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | 148 IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { 149 throw new IllegalStateException( 150 "Invalid preference controller: " + controllerName, e); 151 } 152 } 153 154 /** 155 * Instantiate a controller as specified controller type. 156 * <p/> 157 * This is done through reflection. Do not use this method unless you know what you are doing. 158 */ createInstance(Context context, String controllerName)159 public static BasePreferenceController createInstance(Context context, String controllerName) { 160 try { 161 final Class<?> clazz = Class.forName(controllerName); 162 final Constructor<?> preferenceConstructor = clazz.getConstructor(Context.class); 163 final Object[] params = new Object[]{context}; 164 return (BasePreferenceController) preferenceConstructor.newInstance(params); 165 } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | 166 IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { 167 throw new IllegalStateException( 168 "Invalid preference controller: " + controllerName, e); 169 } 170 } 171 172 /** 173 * Instantiate a controller as specified controller type and work profile 174 * <p/> 175 * This is done through reflection. Do not use this method unless you know what you are doing. 176 * 177 * @param context application context 178 * @param controllerName class name of the {@link BasePreferenceController} 179 * @param key attribute android:key of the {@link Preference} 180 * @param isWorkProfile is this controller only for work profile user? 181 */ createInstance(Context context, String controllerName, String key, boolean isWorkProfile)182 public static BasePreferenceController createInstance(Context context, String controllerName, 183 String key, boolean isWorkProfile) { 184 try { 185 final Class<?> clazz = Class.forName(controllerName); 186 final Constructor<?> preferenceConstructor = 187 clazz.getConstructor(Context.class, String.class); 188 final Object[] params = new Object[]{context, key}; 189 final BasePreferenceController controller = 190 (BasePreferenceController) preferenceConstructor.newInstance(params); 191 controller.setForWork(isWorkProfile); 192 return controller; 193 } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException 194 | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { 195 throw new IllegalStateException( 196 "Invalid preference controller: " + controllerName, e); 197 } 198 } 199 BasePreferenceController(@onNull Context context, @NonNull String preferenceKey)200 public BasePreferenceController(@NonNull Context context, @NonNull String preferenceKey) { 201 super(context); 202 mPreferenceKey = preferenceKey; 203 mPrefVisibility = true; 204 if (TextUtils.isEmpty(mPreferenceKey)) { 205 throw new IllegalArgumentException("Preference key must be set"); 206 } 207 } 208 209 /** 210 * @return {@link AvailabilityStatus} for the Setting. This status is used to determine if the 211 * Setting should be shown or disabled in Settings. Further, it can be used to produce 212 * appropriate error / warning Slice in the case of unavailability. 213 * </p> 214 * The status is used for the convenience methods: {@link #isAvailable()}, 215 * {@link #isSupported()} 216 * </p> 217 * The inherited class doesn't need to check work profile if 218 * android:forWork="true" is set in preference xml. 219 */ 220 @AvailabilityStatus getAvailabilityStatus()221 public abstract int getAvailabilityStatus(); 222 223 @Override getPreferenceKey()224 public String getPreferenceKey() { 225 return mPreferenceKey; 226 } 227 228 @Override getSliceUri()229 public Uri getSliceUri() { 230 return new Uri.Builder() 231 .scheme(ContentResolver.SCHEME_CONTENT) 232 // Default to non-platform authority. Platform Slices will override authority 233 // accordingly. 234 .authority(SettingsSliceProvider.SLICE_AUTHORITY) 235 // Default to action based slices. Intent based slices will override accordingly. 236 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) 237 .appendPath(getPreferenceKey()) 238 .build(); 239 } 240 241 /** 242 * @return {@code true} when the controller can be changed on the device. 243 * 244 * <p> 245 * Will return true for {@link #AVAILABLE} and {@link #DISABLED_DEPENDENT_SETTING}. 246 * <p> 247 * When the availability status returned by {@link #getAvailabilityStatus()} is 248 * {@link #DISABLED_DEPENDENT_SETTING}, then the setting will be disabled by default in the 249 * DashboardFragment, and it is up to the {@link BasePreferenceController} to enable the 250 * preference at the right time. 251 * <p> 252 * This function also check if work profile is existed when android:forWork="true" is set for 253 * the controller in preference xml. 254 * TODO (mfritze) Build a dependency mechanism to allow a controller to easily define the 255 * dependent setting. 256 */ 257 @Override isAvailable()258 public final boolean isAvailable() { 259 if (mIsForWork && mWorkProfileUser == null) { 260 return false; 261 } 262 263 final int availabilityStatus = getAvailabilityStatus(); 264 return (availabilityStatus == AVAILABLE 265 || availabilityStatus == AVAILABLE_UNSEARCHABLE 266 || availabilityStatus == DISABLED_DEPENDENT_SETTING); 267 } 268 isAvailableForSearch()269 private boolean isAvailableForSearch() { 270 if (mIsForWork && mWorkProfileUser == null) { 271 return false; 272 } 273 274 final int availabilityStatus = getAvailabilityStatus(); 275 return (availabilityStatus == AVAILABLE 276 || availabilityStatus == DISABLED_DEPENDENT_SETTING); 277 } 278 279 /** 280 * @return {@code false} if the setting is not applicable to the device. This covers both 281 * settings which were only introduced in future versions of android, or settings that have 282 * hardware dependencies. 283 * </p> 284 * Note that a return value of {@code true} does not mean that the setting is available. 285 */ isSupported()286 public final boolean isSupported() { 287 return getAvailabilityStatus() != UNSUPPORTED_ON_DEVICE; 288 } 289 290 /** 291 * Displays preference in this controller. 292 */ 293 @Override displayPreference(PreferenceScreen screen)294 public void displayPreference(PreferenceScreen screen) { 295 super.displayPreference(screen); 296 if (getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) { 297 // Disable preference if it depends on another setting. 298 final Preference preference = screen.findPreference(getPreferenceKey()); 299 if (preference != null) { 300 preference.setEnabled(false); 301 } 302 } 303 } 304 305 /** 306 * @return the UI type supported by the controller. 307 */ 308 @SliceData.SliceType getSliceType()309 public int getSliceType() { 310 return SliceData.SliceType.INTENT; 311 } 312 313 /** 314 * Updates non-indexable keys for search provider. 315 * 316 * Called by SearchIndexProvider#getNonIndexableKeys 317 */ updateNonIndexableKeys(List<String> keys)318 public void updateNonIndexableKeys(List<String> keys) { 319 final String key = getPreferenceKey(); 320 if (TextUtils.isEmpty(key)) { 321 Log.w(TAG, "Skipping updateNonIndexableKeys due to empty key " + this); 322 return; 323 } 324 if (!keys.contains(key) && !isAvailableForSearch()) { 325 keys.add(key); 326 } 327 } 328 329 /** 330 * Indicates this controller is only for work profile user 331 */ setForWork(boolean forWork)332 void setForWork(boolean forWork) { 333 mIsForWork = forWork; 334 if (mIsForWork) { 335 mWorkProfileUser = Utils.getManagedProfile(UserManager.get(mContext)); 336 } 337 } 338 339 /** 340 * Launches the specified fragment for the work profile user if the associated 341 * {@link Preference} is clicked. Otherwise just forward it to the super class. 342 * 343 * @param preference the preference being clicked. 344 * @return {@code true} if handled. 345 */ 346 @Override handlePreferenceTreeClick(Preference preference)347 public boolean handlePreferenceTreeClick(Preference preference) { 348 if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { 349 return super.handlePreferenceTreeClick(preference); 350 } 351 if (!mIsForWork || mWorkProfileUser == null) { 352 return super.handlePreferenceTreeClick(preference); 353 } 354 final Bundle extra = preference.getExtras(); 355 extra.putInt(EXTRA_USER_ID, mWorkProfileUser.getIdentifier()); 356 new SubSettingLauncher(preference.getContext()) 357 .setDestination(preference.getFragment()) 358 .setSourceMetricsCategory(preference.getExtras().getInt(CATEGORY, 359 SettingsEnums.PAGE_UNKNOWN)) 360 .setArguments(preference.getExtras()) 361 .setUserHandle(mWorkProfileUser) 362 .launch(); 363 return true; 364 } 365 366 /** 367 * Updates raw data for search provider. 368 * 369 * Called by SearchIndexProvider#getRawDataToIndex 370 */ updateRawDataToIndex(List<SearchIndexableRaw> rawData)371 public void updateRawDataToIndex(List<SearchIndexableRaw> rawData) { 372 } 373 374 /** 375 * Updates dynamic raw data for search provider. 376 * 377 * Called by SearchIndexProvider#getDynamicRawDataToIndex 378 */ updateDynamicRawDataToIndex(List<SearchIndexableRaw> rawData)379 public void updateDynamicRawDataToIndex(List<SearchIndexableRaw> rawData) { 380 } 381 382 /** 383 * Set {@link UiBlockListener} 384 * 385 * @param uiBlockListener listener to set 386 */ setUiBlockListener(UiBlockListener uiBlockListener)387 public void setUiBlockListener(UiBlockListener uiBlockListener) { 388 mUiBlockListener = uiBlockListener; 389 } 390 setUiBlockerFinished(boolean isFinished)391 public void setUiBlockerFinished(boolean isFinished) { 392 mUiBlockerFinished = isFinished; 393 } 394 getSavedPrefVisibility()395 public boolean getSavedPrefVisibility() { 396 return mPrefVisibility; 397 } 398 399 /** 400 * Listener to invoke when background job is finished 401 */ 402 public interface UiBlockListener { 403 /** 404 * To notify client that UI related background work is finished. 405 * (i.e. Slice is fully loaded.) 406 * 407 * @param controller Controller that contains background work 408 */ onBlockerWorkFinished(BasePreferenceController controller)409 void onBlockerWorkFinished(BasePreferenceController controller); 410 } 411 412 /** 413 * Used for {@link BasePreferenceController} to decide whether it is ui blocker. 414 * If it is, entire UI will be invisible for a certain period until controller 415 * invokes {@link UiBlockListener} 416 * 417 * This won't block UI thread however has similar side effect. Please use it if you 418 * want to avoid janky animation(i.e. new preference is added in the middle of page). 419 * 420 * This must be used in {@link BasePreferenceController} 421 */ 422 public interface UiBlocker { 423 } 424 425 /** 426 * Set the metrics category of the parent fragment. 427 * 428 * Called by DashboardFragment#onAttach 429 */ setMetricsCategory(int metricsCategory)430 public void setMetricsCategory(int metricsCategory) { 431 mMetricsCategory = metricsCategory; 432 } 433 434 /** 435 * @return the metrics category of the parent fragment. 436 */ getMetricsCategory()437 protected int getMetricsCategory() { 438 return mMetricsCategory; 439 } 440 441 /** 442 * @return Non-{@code null} {@link UserHandle} when a work profile is enabled. 443 * Otherwise {@code null}. 444 */ 445 @Nullable getWorkProfileUser()446 protected UserHandle getWorkProfileUser() { 447 return mWorkProfileUser; 448 } 449 450 /** 451 * Used for {@link BasePreferenceController} that implements {@link UiBlocker} to control the 452 * preference visibility. 453 */ updatePreferenceVisibilityDelegate(Preference preference, boolean isVisible)454 protected void updatePreferenceVisibilityDelegate(Preference preference, boolean isVisible) { 455 if (mUiBlockerFinished) { 456 preference.setVisible(isVisible); 457 return; 458 } 459 460 savePrefVisibility(isVisible); 461 462 // Preferences that should be invisible have a high priority to be updated since the 463 // whole UI should be blocked/invisible. While those that should be visible will be 464 // updated once the blocker work is finished. That's done in DashboardFragment. 465 if (!isVisible) { 466 preference.setVisible(false); 467 } 468 } 469 savePrefVisibility(boolean isVisible)470 private void savePrefVisibility(boolean isVisible) { 471 mPrefVisibility = isVisible; 472 } 473 } 474