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 protected boolean mUiBlockerFinished; 125 private boolean mIsForWork; 126 @Nullable 127 private UserHandle mWorkProfileUser; 128 private int mMetricsCategory; 129 private boolean mPrefVisibility; 130 131 /** 132 * Instantiate a controller as specified controller type and user-defined key. 133 * <p/> 134 * This is done through reflection. Do not use this method unless you know what you are doing. 135 */ createInstance(Context context, String controllerName, String key)136 public static BasePreferenceController createInstance(Context context, 137 String controllerName, String key) { 138 try { 139 final Class<?> clazz = Class.forName(controllerName); 140 final Constructor<?> preferenceConstructor = 141 clazz.getConstructor(Context.class, String.class); 142 final Object[] params = new Object[]{context, key}; 143 return (BasePreferenceController) preferenceConstructor.newInstance(params); 144 } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | 145 IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { 146 throw new IllegalStateException( 147 "Invalid preference controller: " + controllerName, e); 148 } 149 } 150 151 /** 152 * Instantiate a controller as specified controller type. 153 * <p/> 154 * This is done through reflection. Do not use this method unless you know what you are doing. 155 */ createInstance(Context context, String controllerName)156 public static BasePreferenceController createInstance(Context context, String controllerName) { 157 try { 158 final Class<?> clazz = Class.forName(controllerName); 159 final Constructor<?> preferenceConstructor = clazz.getConstructor(Context.class); 160 final Object[] params = new Object[]{context}; 161 return (BasePreferenceController) preferenceConstructor.newInstance(params); 162 } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | 163 IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { 164 throw new IllegalStateException( 165 "Invalid preference controller: " + controllerName, e); 166 } 167 } 168 169 /** 170 * Instantiate a controller as specified controller type and work profile 171 * <p/> 172 * This is done through reflection. Do not use this method unless you know what you are doing. 173 * 174 * @param context application context 175 * @param controllerName class name of the {@link BasePreferenceController} 176 * @param key attribute android:key of the {@link Preference} 177 * @param isWorkProfile is this controller only for work profile user? 178 */ createInstance(Context context, String controllerName, String key, boolean isWorkProfile)179 public static BasePreferenceController createInstance(Context context, String controllerName, 180 String key, boolean isWorkProfile) { 181 try { 182 final Class<?> clazz = Class.forName(controllerName); 183 final Constructor<?> preferenceConstructor = 184 clazz.getConstructor(Context.class, String.class); 185 final Object[] params = new Object[]{context, key}; 186 final BasePreferenceController controller = 187 (BasePreferenceController) preferenceConstructor.newInstance(params); 188 controller.setForWork(isWorkProfile); 189 return controller; 190 } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException 191 | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { 192 throw new IllegalStateException( 193 "Invalid preference controller: " + controllerName, e); 194 } 195 } 196 BasePreferenceController(Context context, String preferenceKey)197 public BasePreferenceController(Context context, String preferenceKey) { 198 super(context); 199 mPreferenceKey = preferenceKey; 200 mPrefVisibility = true; 201 if (TextUtils.isEmpty(mPreferenceKey)) { 202 throw new IllegalArgumentException("Preference key must be set"); 203 } 204 } 205 206 /** 207 * @return {@link AvailabilityStatus} for the Setting. This status is used to determine if the 208 * Setting should be shown or disabled in Settings. Further, it can be used to produce 209 * appropriate error / warning Slice in the case of unavailability. 210 * </p> 211 * The status is used for the convenience methods: {@link #isAvailable()}, 212 * {@link #isSupported()} 213 * </p> 214 * The inherited class doesn't need to check work profile if 215 * android:forWork="true" is set in preference xml. 216 */ 217 @AvailabilityStatus getAvailabilityStatus()218 public abstract int getAvailabilityStatus(); 219 220 @Override getPreferenceKey()221 public String getPreferenceKey() { 222 return mPreferenceKey; 223 } 224 225 @Override getSliceUri()226 public Uri getSliceUri() { 227 return new Uri.Builder() 228 .scheme(ContentResolver.SCHEME_CONTENT) 229 // Default to non-platform authority. Platform Slices will override authority 230 // accordingly. 231 .authority(SettingsSliceProvider.SLICE_AUTHORITY) 232 // Default to action based slices. Intent based slices will override accordingly. 233 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) 234 .appendPath(getPreferenceKey()) 235 .build(); 236 } 237 238 /** 239 * @return {@code true} when the controller can be changed on the device. 240 * 241 * <p> 242 * Will return true for {@link #AVAILABLE} and {@link #DISABLED_DEPENDENT_SETTING}. 243 * <p> 244 * When the availability status returned by {@link #getAvailabilityStatus()} is 245 * {@link #DISABLED_DEPENDENT_SETTING}, then the setting will be disabled by default in the 246 * DashboardFragment, and it is up to the {@link BasePreferenceController} to enable the 247 * preference at the right time. 248 * <p> 249 * This function also check if work profile is existed when android:forWork="true" is set for 250 * the controller in preference xml. 251 * TODO (mfritze) Build a dependency mechanism to allow a controller to easily define the 252 * dependent setting. 253 */ 254 @Override isAvailable()255 public final boolean isAvailable() { 256 if (mIsForWork && mWorkProfileUser == null) { 257 return false; 258 } 259 260 final int availabilityStatus = getAvailabilityStatus(); 261 return (availabilityStatus == AVAILABLE 262 || availabilityStatus == AVAILABLE_UNSEARCHABLE 263 || availabilityStatus == DISABLED_DEPENDENT_SETTING); 264 } 265 266 /** 267 * @return {@code false} if the setting is not applicable to the device. This covers both 268 * settings which were only introduced in future versions of android, or settings that have 269 * hardware dependencies. 270 * </p> 271 * Note that a return value of {@code true} does not mean that the setting is available. 272 */ isSupported()273 public final boolean isSupported() { 274 return getAvailabilityStatus() != UNSUPPORTED_ON_DEVICE; 275 } 276 277 /** 278 * Displays preference in this controller. 279 */ 280 @Override displayPreference(PreferenceScreen screen)281 public void displayPreference(PreferenceScreen screen) { 282 super.displayPreference(screen); 283 if (getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) { 284 // Disable preference if it depends on another setting. 285 final Preference preference = screen.findPreference(getPreferenceKey()); 286 if (preference != null) { 287 preference.setEnabled(false); 288 } 289 } 290 } 291 292 /** 293 * @return the UI type supported by the controller. 294 */ 295 @SliceData.SliceType getSliceType()296 public int getSliceType() { 297 return SliceData.SliceType.INTENT; 298 } 299 300 /** 301 * Updates non-indexable keys for search provider. 302 * 303 * Called by SearchIndexProvider#getNonIndexableKeys 304 */ updateNonIndexableKeys(List<String> keys)305 public void updateNonIndexableKeys(List<String> keys) { 306 final boolean shouldSuppressFromSearch = !isAvailable() 307 || getAvailabilityStatus() == AVAILABLE_UNSEARCHABLE; 308 if (shouldSuppressFromSearch) { 309 final String key = getPreferenceKey(); 310 if (TextUtils.isEmpty(key)) { 311 Log.w(TAG, "Skipping updateNonIndexableKeys due to empty key " + toString()); 312 return; 313 } 314 if (keys.contains(key)) { 315 Log.w(TAG, "Skipping updateNonIndexableKeys, key already in list. " + toString()); 316 return; 317 } 318 keys.add(key); 319 } 320 } 321 322 /** 323 * Indicates this controller is only for work profile user 324 */ setForWork(boolean forWork)325 void setForWork(boolean forWork) { 326 mIsForWork = forWork; 327 if (mIsForWork) { 328 mWorkProfileUser = Utils.getManagedProfile(UserManager.get(mContext)); 329 } 330 } 331 332 /** 333 * Launches the specified fragment for the work profile user if the associated 334 * {@link Preference} is clicked. Otherwise just forward it to the super class. 335 * 336 * @param preference the preference being clicked. 337 * @return {@code true} if handled. 338 */ 339 @Override handlePreferenceTreeClick(Preference preference)340 public boolean handlePreferenceTreeClick(Preference preference) { 341 if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { 342 return super.handlePreferenceTreeClick(preference); 343 } 344 if (!mIsForWork || mWorkProfileUser == null) { 345 return super.handlePreferenceTreeClick(preference); 346 } 347 final Bundle extra = preference.getExtras(); 348 extra.putInt(EXTRA_USER_ID, mWorkProfileUser.getIdentifier()); 349 new SubSettingLauncher(preference.getContext()) 350 .setDestination(preference.getFragment()) 351 .setSourceMetricsCategory(preference.getExtras().getInt(CATEGORY, 352 SettingsEnums.PAGE_UNKNOWN)) 353 .setArguments(preference.getExtras()) 354 .setUserHandle(mWorkProfileUser) 355 .launch(); 356 return true; 357 } 358 359 /** 360 * Updates raw data for search provider. 361 * 362 * Called by SearchIndexProvider#getRawDataToIndex 363 */ updateRawDataToIndex(List<SearchIndexableRaw> rawData)364 public void updateRawDataToIndex(List<SearchIndexableRaw> rawData) { 365 } 366 367 /** 368 * Updates dynamic raw data for search provider. 369 * 370 * Called by SearchIndexProvider#getDynamicRawDataToIndex 371 */ updateDynamicRawDataToIndex(List<SearchIndexableRaw> rawData)372 public void updateDynamicRawDataToIndex(List<SearchIndexableRaw> rawData) { 373 } 374 375 /** 376 * Set {@link UiBlockListener} 377 * 378 * @param uiBlockListener listener to set 379 */ setUiBlockListener(UiBlockListener uiBlockListener)380 public void setUiBlockListener(UiBlockListener uiBlockListener) { 381 mUiBlockListener = uiBlockListener; 382 } 383 setUiBlockerFinished(boolean isFinished)384 public void setUiBlockerFinished(boolean isFinished) { 385 mUiBlockerFinished = isFinished; 386 } 387 getSavedPrefVisibility()388 public boolean getSavedPrefVisibility() { 389 return mPrefVisibility; 390 } 391 392 /** 393 * Listener to invoke when background job is finished 394 */ 395 public interface UiBlockListener { 396 /** 397 * To notify client that UI related background work is finished. 398 * (i.e. Slice is fully loaded.) 399 * 400 * @param controller Controller that contains background work 401 */ onBlockerWorkFinished(BasePreferenceController controller)402 void onBlockerWorkFinished(BasePreferenceController controller); 403 } 404 405 /** 406 * Used for {@link BasePreferenceController} to decide whether it is ui blocker. 407 * If it is, entire UI will be invisible for a certain period until controller 408 * invokes {@link UiBlockListener} 409 * 410 * This won't block UI thread however has similar side effect. Please use it if you 411 * want to avoid janky animation(i.e. new preference is added in the middle of page). 412 * 413 * This must be used in {@link BasePreferenceController} 414 */ 415 public interface UiBlocker { 416 } 417 418 /** 419 * Set the metrics category of the parent fragment. 420 * 421 * Called by DashboardFragment#onAttach 422 */ setMetricsCategory(int metricsCategory)423 public void setMetricsCategory(int metricsCategory) { 424 mMetricsCategory = metricsCategory; 425 } 426 427 /** 428 * @return the metrics category of the parent fragment. 429 */ getMetricsCategory()430 protected int getMetricsCategory() { 431 return mMetricsCategory; 432 } 433 434 /** 435 * @return Non-{@code null} {@link UserHandle} when a work profile is enabled. 436 * Otherwise {@code null}. 437 */ 438 @Nullable getWorkProfileUser()439 protected UserHandle getWorkProfileUser() { 440 return mWorkProfileUser; 441 } 442 443 /** 444 * Used for {@link BasePreferenceController} that implements {@link UiBlocker} to control the 445 * preference visibility. 446 */ updatePreferenceVisibilityDelegate(Preference preference, boolean isVisible)447 protected void updatePreferenceVisibilityDelegate(Preference preference, boolean isVisible) { 448 if (mUiBlockerFinished) { 449 preference.setVisible(isVisible); 450 return; 451 } 452 453 savePrefVisibility(isVisible); 454 455 // Preferences that should be invisible have a high priority to be updated since the 456 // whole UI should be blocked/invisible. While those that should be visible will be 457 // updated once the blocker work is finished. That's done in DashboardFragment. 458 if (!isVisible) { 459 preference.setVisible(false); 460 } 461 } 462 savePrefVisibility(boolean isVisible)463 private void savePrefVisibility(boolean isVisible) { 464 mPrefVisibility = isVisible; 465 } 466 } 467