ackage com.android.settingslib.core;importandroid.app.admin.DevicePolicyManager;importandroid.content.Context;importandroid.os.Build;importandroid.text.TextUtils;importandroid.util.Log;importandroidx.annotation.EmptySuper;importandroidx.annotation.NonNull;importandroidx.annotation.RequiresApi;importandroidx.core.os.BuildCompat;importandroidx.lifecycle.LifecycleOwner;importandroidx.preference.Preference;importandroidx.preference.PreferenceGroup;importandroidx.preference.PreferenceScreen;/**
* A controller that manages event for preference.
*/publicabstractclassAbstractPreferenceController{privatestaticfinalString TAG ="AbstractPrefController";protectedfinalContext mContext;privatefinalDevicePolicyManager mDevicePolicyManager;publicAbstractPreferenceController(Context context){
mContext = context;
mDevicePolicyManager =(DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);}/**
* Displays preference in this controller.
*/publicvoiddisplayPreference(PreferenceScreen screen){finalString prefKey =getPreferenceKey();if(TextUtils.isEmpty(prefKey)){Log.w(TAG,"Skipping displayPreference because key is empty:"+getClass().getName());return;}if(isAvailable()){setVisible(screen, prefKey,true/* visible */);if(thisinstanceofPreference.OnPreferenceChangeListener){finalPreference preference = screen.findPreference(prefKey);if(preference !=null){
preference.setOnPreferenceChangeListener((Preference.OnPreferenceChangeListener)this);}}}else{setVisible(screen, prefKey,false/* visible */);}}/**
* Called on view created.
*/@EmptySuperpublicvoidonViewCreated(@NonNullLifecycleOwner viewLifecycleOwner){}/**
* Updates the current status of preference (summary, switch state, etc)
*/publicvoidupdateState(Preference preference){refreshSummary(preference);}/**
* Refresh preference summary with getSummary()
*/protectedvoidrefreshSummary(Preference preference){if(preference ==null){return;}finalCharSequence summary =getSummary();if(summary ==null){// Default getSummary returns null. If subclass didn't override this, there is nothing// we need to do.return;}
preference.setSummary(summary);}/**
* Returns true if preference is available (should be displayed)
*/publicabstractbooleanisAvailable();/**
* Handles preference tree click
*
* @param preference the preference being clicked
* @return true if click is handled
*/publicbooleanhandlePreferenceTreeClick(Preference preference){returnfalse;}/**
* Returns the key for this preference.
*/publicabstractStringgetPreferenceKey();/**
* Show/hide a preference.
*/protectedfinalvoidsetVisible(PreferenceGroup group,String key,boolean isVisible){finalPreference pref = group.findPreference(key);if(pref !=null){
pref.setVisible(isVisible);}}/**
* @return a {@link CharSequence} for the summary of the preference.
*/publicCharSequencegetSummary(){returnnull;}@RequiresApi(Build.VERSION_CODES.TIRAMISU)protectedvoidreplaceEnterpriseStringTitle(PreferenceScreen screen,String preferenceKey,String overrideKey,int resource){if(!BuildCompat.isAtLeastT()|| mDevicePolicyManager ==null){return;}Preference preference = screen.findPreference(preferenceKey);if(preference ==null){Log.d(TAG,"Could not find enterprise preference "+ preferenceKey);return;}
preference.setTitle(
mDevicePolicyManager.getResources().getString(overrideKey,()-> mContext.getString(resource)));}@RequiresApi(Build.VERSION_CODES.TIRAMISU)protectedvoidreplaceEnterpriseStringSummary(PreferenceScreen screen,String preferenceKey,String overrideKey,int resource){if(!BuildCompat.isAtLeastT()|| mDevicePolicyManager ==null){return;}Preference preference = screen.findPreference(preferenceKey);if(preference ==null){Log.d(TAG,"Could not find enterprise preference "+ preferenceKey);return;}
preference.setSummary(
mDevicePolicyManager.getResources().getString(overrideKey,()-> mContext.getString(resource)));}}
PreferenceControllerListHelper
packagecom.android.settings.core;importstaticcom.android.settings.core.PreferenceXmlParserUtils.METADATA_CONTROLLER;importstaticcom.android.settings.core.PreferenceXmlParserUtils.METADATA_FOR_WORK;importstaticcom.android.settings.core.PreferenceXmlParserUtils.METADATA_KEY;importandroid.annotation.XmlRes;importandroid.content.Context;importandroid.os.Bundle;importandroid.text.TextUtils;importandroid.util.Log;importandroidx.annotation.NonNull;importandroidx.preference.PreferenceManager;importandroidx.preference.PreferenceScreen;importcom.android.settings.core.PreferenceXmlParserUtils.MetadataFlag;importcom.android.settingslib.core.AbstractPreferenceController;importorg.xmlpull.v1.XmlPullParserException;importjava.io.IOException;importjava.util.ArrayList;importjava.util.List;importjava.util.Set;importjava.util.TreeSet;/**
* Helper to load {@link BasePreferenceController} lists from Xml.
*/publicclassPreferenceControllerListHelper{privatestaticfinalString TAG ="PrefCtrlListHelper";/**
* Instantiates a list of controller based on xml definition.
*/@NonNullpublicstaticList<BasePreferenceController>getPreferenceControllersFromXml(Context context,@XmlResint xmlResId){finalList<BasePreferenceController> controllers =newArrayList<>();List<Bundle> preferenceMetadata;try{
preferenceMetadata =PreferenceXmlParserUtils.extractMetadata(context, xmlResId,MetadataFlag.FLAG_NEED_KEY |MetadataFlag.FLAG_NEED_PREF_CONTROLLER
|MetadataFlag.FLAG_INCLUDE_PREF_SCREEN |MetadataFlag.FLAG_FOR_WORK);}catch(IOException|XmlPullParserException e){Log.e(TAG,"Failed to parse preference xml for getting controllers", e);return controllers;}for(Bundle metadata : preferenceMetadata){finalString controllerName = metadata.getString(METADATA_CONTROLLER);if(TextUtils.isEmpty(controllerName)){continue;}BasePreferenceController controller;try{
controller =BasePreferenceController.createInstance(context, controllerName);}catch(IllegalStateException e){Log.d(TAG,"Could not find Context-only controller for pref: "+ controllerName);finalString key = metadata.getString(METADATA_KEY);finalboolean isWorkProfile = metadata.getBoolean(METADATA_FOR_WORK,false);if(TextUtils.isEmpty(key)){Log.w(TAG,"Controller requires key but it's not defined in xml: "+ controllerName);continue;}try{
controller =BasePreferenceController.createInstance(context, controllerName,
key, isWorkProfile);}catch(IllegalStateException e2){Log.w(TAG,"Cannot instantiate controller from reflection: "+ controllerName);continue;}}
controllers.add(controller);}return controllers;}/**
* Checks if the given PreferenceScreen will be empty due to all preferences being unavailable.
*
* @param xmlResId resource id of the PreferenceScreen to check
* @return {@code true} if none of the preferences in the given screen will appear
*/publicstaticbooleanareAllPreferencesUnavailable(@NonNullContext context,@NonNullPreferenceManager preferenceManager,@XmlResint xmlResId){PreferenceScreen screen = preferenceManager.inflateFromResource(context, xmlResId,/* rootPreferences= */null);List<BasePreferenceController> preferenceControllers =getPreferenceControllersFromXml(context, xmlResId);if(screen.getPreferenceCount()!= preferenceControllers.size()){// There are some preferences without controllers, which will show regardless.returnfalse;}return preferenceControllers.stream().noneMatch(BasePreferenceController::isAvailable);}/**
* Return a sub list of {@link AbstractPreferenceController} to only contain controller that
* doesn't exist in filter.
*
* @param filter The filter. This list will be unchanged.
* @param input This list will be filtered into a sublist and element is kept
* IFF the controller key is not used by anything from {@param filter}.
*/@NonNullpublicstaticList<BasePreferenceController>filterControllers(@NonNullList<BasePreferenceController> input,List<AbstractPreferenceController> filter){if(input ==null|| filter ==null){return input;}finalSet<String> keys =newTreeSet<>();finalList<BasePreferenceController> filteredList =newArrayList<>();for(AbstractPreferenceController controller : filter){finalString key = controller.getPreferenceKey();if(key !=null){
keys.add(key);}}for(BasePreferenceController controller : input){if(keys.contains(controller.getPreferenceKey())){Log.w(TAG, controller.getPreferenceKey()+" already has a controller");continue;}
filteredList.add(controller);}return filteredList;}}
BasePreferenceController
importstaticandroid.content.Intent.EXTRA_USER_ID;importstaticcom.android.settings.dashboard.DashboardFragment.CATEGORY;importandroid.annotation.IntDef;importandroid.app.settings.SettingsEnums;importandroid.content.ContentResolver;importandroid.content.Context;importandroid.net.Uri;importandroid.os.Bundle;importandroid.os.UserHandle;importandroid.os.UserManager;importandroid.provider.SettingsSlicesContract;importandroid.text.TextUtils;importandroid.util.Log;importandroidx.annotation.Nullable;importandroidx.preference.Preference;importandroidx.preference.PreferenceScreen;importcom.android.settings.Utils;importcom.android.settings.slices.SettingsSliceProvider;importcom.android.settings.slices.SliceData;importcom.android.settings.slices.Sliceable;importcom.android.settingslib.core.AbstractPreferenceController;importcom.android.settingslib.search.SearchIndexableRaw;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.reflect.Constructor;importjava.lang.reflect.InvocationTargetException;importjava.util.List;/**
* Abstract class to consolidate utility between preference controllers and act as an interface
* for Slices. The abstract classes that inherit from this class will act as the direct interfaces
* for each type when plugging into Slices.
*/publicabstractclassBasePreferenceControllerextendsAbstractPreferenceControllerimplementsSliceable{privatestaticfinalString TAG ="SettingsPrefController";/**
* Denotes the availability of the Setting.
* <p>
* Used both explicitly and by the convenience methods {@link #isAvailable()} and
* {@link #isSupported()}.
*/@Retention(RetentionPolicy.SOURCE)@IntDef({AVAILABLE, AVAILABLE_UNSEARCHABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_USER,
DISABLED_DEPENDENT_SETTING, CONDITIONALLY_UNAVAILABLE})public@interfaceAvailabilityStatus{}/**
* The setting is available, and searchable to all search clients.
*/publicstaticfinalint AVAILABLE =0;/**
* The setting is available, but is not searchable to any search client.
*/publicstaticfinalint AVAILABLE_UNSEARCHABLE =1;/**
* A generic catch for settings which are currently unavailable, but may become available in
* the future. You should use {@link #DISABLED_FOR_USER} or {@link #DISABLED_DEPENDENT_SETTING}
* if they describe the condition more accurately.
*/publicstaticfinalint CONDITIONALLY_UNAVAILABLE =2;/**
* The setting is not, and will not supported by this device.
* <p>
* There is no guarantee that the setting page exists, and any links to the Setting should take
* you to the home page of Settings.
*/publicstaticfinalint UNSUPPORTED_ON_DEVICE =3;/**
* The setting cannot be changed by the current user.
* <p>
* Links to the Setting should take you to the page of the Setting, even if it cannot be
* changed.
*/publicstaticfinalint DISABLED_FOR_USER =4;/**
* The setting has a dependency in the Settings App which is currently blocking access.
* <p>
* It must be possible for the Setting to be enabled by changing the configuration of the device
* settings. That is, a setting that cannot be changed because of the state of another setting.
* This should not be used for a setting that would be hidden from the UI entirely.
* <p>
* Correct use: Intensity of night display should be {@link #DISABLED_DEPENDENT_SETTING} when
* night display is off.
* Incorrect use: Mobile Data is {@link #DISABLED_DEPENDENT_SETTING} when there is no
* data-enabled sim.
* <p>
* Links to the Setting should take you to the page of the Setting, even if it cannot be
* changed.
*/publicstaticfinalint DISABLED_DEPENDENT_SETTING =5;protectedfinalString mPreferenceKey;protectedUiBlockListener mUiBlockListener;protectedboolean mUiBlockerFinished;privateboolean mIsForWork;@NullableprivateUserHandle mWorkProfileUser;privateint mMetricsCategory;privateboolean mPrefVisibility;/**
* Instantiate a controller as specified controller type and user-defined key.
* <p/>
* This is done through reflection. Do not use this method unless you know what you are doing.
*/publicstaticBasePreferenceControllercreateInstance(Context context,String controllerName,String key){try{finalClass<?> clazz =Class.forName(controllerName);finalConstructor<?> preferenceConstructor =
clazz.getConstructor(Context.class,String.class);finalObject[] params =newObject[]{context, key};return(BasePreferenceController) preferenceConstructor.newInstance(params);}catch(ClassNotFoundException|NoSuchMethodException|InstantiationException|IllegalArgumentException|InvocationTargetException|IllegalAccessException e){thrownewIllegalStateException("Invalid preference controller: "+ controllerName, e);}}/**
* Instantiate a controller as specified controller type.
* <p/>
* This is done through reflection. Do not use this method unless you know what you are doing.
*/publicstaticBasePreferenceControllercreateInstance(Context context,String controllerName){try{finalClass<?> clazz =Class.forName(controllerName);finalConstructor<?> preferenceConstructor = clazz.getConstructor(Context.class);finalObject[] params =newObject[]{context};return(BasePreferenceController) preferenceConstructor.newInstance(params);}catch(ClassNotFoundException|NoSuchMethodException|InstantiationException|IllegalArgumentException|InvocationTargetException|IllegalAccessException e){thrownewIllegalStateException("Invalid preference controller: "+ controllerName, e);}}/**
* Instantiate a controller as specified controller type and work profile
* <p/>
* This is done through reflection. Do not use this method unless you know what you are doing.
*
* @param context application context
* @param controllerName class name of the {@link BasePreferenceController}
* @param key attribute android:key of the {@link Preference}
* @param isWorkProfile is this controller only for work profile user?
*/publicstaticBasePreferenceControllercreateInstance(Context context,String controllerName,String key,boolean isWorkProfile){try{finalClass<?> clazz =Class.forName(controllerName);finalConstructor<?> preferenceConstructor =
clazz.getConstructor(Context.class,String.class);finalObject[] params =newObject[]{context, key};finalBasePreferenceController controller =(BasePreferenceController) preferenceConstructor.newInstance(params);
controller.setForWork(isWorkProfile);return controller;}catch(ClassNotFoundException|NoSuchMethodException|InstantiationException|IllegalArgumentException|InvocationTargetException|IllegalAccessException e){thrownewIllegalStateException("Invalid preference controller: "+ controllerName, e);}}publicBasePreferenceController(Context context,String preferenceKey){super(context);
mPreferenceKey = preferenceKey;
mPrefVisibility =true;if(TextUtils.isEmpty(mPreferenceKey)){thrownewIllegalArgumentException("Preference key must be set");}}/**
* @return {@link AvailabilityStatus} for the Setting. This status is used to determine if the
* Setting should be shown or disabled in Settings. Further, it can be used to produce
* appropriate error / warning Slice in the case of unavailability.
* </p>
* The status is used for the convenience methods: {@link #isAvailable()},
* {@link #isSupported()}
* </p>
* The inherited class doesn't need to check work profile if
* android:forWork="true" is set in preference xml.
*/@AvailabilityStatuspublicabstractintgetAvailabilityStatus();@OverridepublicStringgetPreferenceKey(){return mPreferenceKey;}@OverridepublicUrigetSliceUri(){returnnewUri.Builder().scheme(ContentResolver.SCHEME_CONTENT)// Default to non-platform authority. Platform Slices will override authority// accordingly..authority(SettingsSliceProvider.SLICE_AUTHORITY)// Default to action based slices. Intent based slices will override accordingly..appendPath(SettingsSlicesContract.PATH_SETTING_ACTION).appendPath(getPreferenceKey()).build();}/**
* @return {@code true} when the controller can be changed on the device.
*
* <p>
* Will return true for {@link #AVAILABLE} and {@link #DISABLED_DEPENDENT_SETTING}.
* <p>
* When the availability status returned by {@link #getAvailabilityStatus()} is
* {@link #DISABLED_DEPENDENT_SETTING}, then the setting will be disabled by default in the
* DashboardFragment, and it is up to the {@link BasePreferenceController} to enable the
* preference at the right time.
* <p>
* This function also check if work profile is existed when android:forWork="true" is set for
* the controller in preference xml.
* TODO (mfritze) Build a dependency mechanism to allow a controller to easily define the
* dependent setting.
*/@OverridepublicfinalbooleanisAvailable(){if(mIsForWork && mWorkProfileUser ==null){returnfalse;}finalint availabilityStatus =getAvailabilityStatus();return(availabilityStatus == AVAILABLE
|| availabilityStatus == AVAILABLE_UNSEARCHABLE
|| availabilityStatus == DISABLED_DEPENDENT_SETTING);}/**
* @return {@code false} if the setting is not applicable to the device. This covers both
* settings which were only introduced in future versions of android, or settings that have
* hardware dependencies.
* </p>
* Note that a return value of {@code true} does not mean that the setting is available.
*/publicfinalbooleanisSupported(){returngetAvailabilityStatus()!= UNSUPPORTED_ON_DEVICE;}/**
* Displays preference in this controller.
*/@OverridepublicvoiddisplayPreference(PreferenceScreen screen){super.displayPreference(screen);if(getAvailabilityStatus()== DISABLED_DEPENDENT_SETTING){// Disable preference if it depends on another setting.finalPreference preference = screen.findPreference(getPreferenceKey());if(preference !=null){
preference.setEnabled(false);}}}/**
* @return the UI type supported by the controller.
*/@SliceData.SliceTypepublicintgetSliceType(){returnSliceData.SliceType.INTENT;}/**
* Updates non-indexable keys for search provider.
*
* Called by SearchIndexProvider#getNonIndexableKeys
*/publicvoidupdateNonIndexableKeys(List<String> keys){finalboolean shouldSuppressFromSearch =!isAvailable()||getAvailabilityStatus()== AVAILABLE_UNSEARCHABLE;if(shouldSuppressFromSearch){finalString key =getPreferenceKey();if(TextUtils.isEmpty(key)){Log.w(TAG,"Skipping updateNonIndexableKeys due to empty key "+toString());return;}if(keys.contains(key)){Log.w(TAG,"Skipping updateNonIndexableKeys, key already in list. "+toString());return;}
keys.add(key);}}/**
* Indicates this controller is only for work profile user
*/voidsetForWork(boolean forWork){
mIsForWork = forWork;if(mIsForWork){
mWorkProfileUser =Utils.getManagedProfile(UserManager.get(mContext));}}/**
* Launches the specified fragment for the work profile user if the associated
* {@link Preference} is clicked. Otherwise just forward it to the super class.
*
* @param preference the preference being clicked.
* @return {@code true} if handled.
*/@OverridepublicbooleanhandlePreferenceTreeClick(Preference preference){if(!TextUtils.equals(preference.getKey(),getPreferenceKey())){returnsuper.handlePreferenceTreeClick(preference);}if(!mIsForWork || mWorkProfileUser ==null){returnsuper.handlePreferenceTreeClick(preference);}finalBundle extra = preference.getExtras();
extra.putInt(EXTRA_USER_ID, mWorkProfileUser.getIdentifier());newSubSettingLauncher(preference.getContext()).setDestination(preference.getFragment()).setSourceMetricsCategory(preference.getExtras().getInt(CATEGORY,SettingsEnums.PAGE_UNKNOWN)).setArguments(preference.getExtras()).setUserHandle(mWorkProfileUser).launch();returntrue;}/**
* Updates raw data for search provider.
*
* Called by SearchIndexProvider#getRawDataToIndex
*/publicvoidupdateRawDataToIndex(List<SearchIndexableRaw> rawData){}/**
* Updates dynamic raw data for search provider.
*
* Called by SearchIndexProvider#getDynamicRawDataToIndex
*/publicvoidupdateDynamicRawDataToIndex(List<SearchIndexableRaw> rawData){}/**
* Set {@link UiBlockListener}
*
* @param uiBlockListener listener to set
*/publicvoidsetUiBlockListener(UiBlockListener uiBlockListener){
mUiBlockListener = uiBlockListener;}publicvoidsetUiBlockerFinished(boolean isFinished){
mUiBlockerFinished = isFinished;}publicbooleangetSavedPrefVisibility(){return mPrefVisibility;}/**
* Listener to invoke when background job is finished
*/publicinterfaceUiBlockListener{/**
* To notify client that UI related background work is finished.
* (i.e. Slice is fully loaded.)
*
* @param controller Controller that contains background work
*/voidonBlockerWorkFinished(BasePreferenceController controller);}/**
* Used for {@link BasePreferenceController} to decide whether it is ui blocker.
* If it is, entire UI will be invisible for a certain period until controller
* invokes {@link UiBlockListener}
*
* This won't block UI thread however has similar side effect. Please use it if you
* want to avoid janky animation(i.e. new preference is added in the middle of page).
*
* This must be used in {@link BasePreferenceController}
*/publicinterfaceUiBlocker{}/**
* Set the metrics category of the parent fragment.
*
* Called by DashboardFragment#onAttach
*/publicvoidsetMetricsCategory(int metricsCategory){
mMetricsCategory = metricsCategory;}/**
* @return the metrics category of the parent fragment.
*/protectedintgetMetricsCategory(){return mMetricsCategory;}/**
* @return Non-{@code null} {@link UserHandle} when a work profile is enabled.
* Otherwise {@code null}.
*/@NullableprotectedUserHandlegetWorkProfileUser(){return mWorkProfileUser;}/**
* Used for {@link BasePreferenceController} that implements {@link UiBlocker} to control the
* preference visibility.
*/protectedvoidupdatePreferenceVisibilityDelegate(Preference preference,boolean isVisible){if(mUiBlockerFinished){
preference.setVisible(isVisible);return;}savePrefVisibility(isVisible);// Preferences that should be invisible have a high priority to be updated since the// whole UI should be blocked/invisible. While those that should be visible will be// updated once the blocker work is finished. That's done in DashboardFragment.if(!isVisible){
preference.setVisible(false);}}privatevoidsavePrefVisibility(boolean isVisible){
mPrefVisibility = isVisible;}}