Android Settings解析

首语

Android设置应用是Android系统中一个非常重要的系统应用,它允许用户调整和设置系统的各种参数和功能(系统设置/自定义设置/控制应用权限/开发者选项/系统信息等),使用户获得更好的使用体验。同时它一般也是Android系统开发者了解深入的第一个系统级应用,也是用户使用最频繁的系统应用。

源码目录

AOSP源码路径为packages/apps/Settings 。src/com/android/settings目录下包含Settings的主要源码。libs目录下的contextualcards.aar包含实现上下文卡片功能的代码和资源,它可以将相关的内容组织在一起,以卡片的形式展现给用户。res目录下包含各种静态资源。Android.bp文件中可以看到模块名为"Settings"。

设计指南

上图是Settings里一个普通的页面,从这个页面可以看出它将许多设置放在一起,设置列表是多个控件的组合。

它有如下优点:

  • 提供一个很好的概述。用户应该能够浏览设置屏幕并了解所有单独的设置及其值。
  • 直观的设置项目。常用设置放在屏幕顶部。限制一个屏幕上的设置数量。将一些设置移动到单独的屏幕来创建直观的菜单。
  • 使用明确的标题和状态。标题简短而有意义。在标题下方,显示状态以突出设置的值,显示具体细节。

关于Settings设计的详细规则及细节,可以参考官网:设计指南

Preference

在Android 常用组件里,存在一个Preference组件,它提供了一个方便的用户界面,用于管理和显示应用程序的各种设置选项,让用户可以轻松浏览和更改应用程序的设置。Preference还通过SharedPreference实现保存读取数据,以其key作为SharedPreference的键,实现持久化数据。Settings中大多数菜单都是通过Preference去实现,且使用的是androidx包的Preference,因此首先了解下Preference的使用。

Preference组件和其它页面组件使用类似,区别在于XML 资源必须放置于 res/xml/ 目录,Preference的根标签必须为PreferenceScreen。举例如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
    xmlns:andoird="http://schemas.android.com/apk/res-auto">
	<PreferenceCategory
        app:key="prefer_category">
   		<Preference
       		andoird:key="feedback"
       		andoird:title="Send feedback"
        	andoird:summary="Report technical issues or suggest new features"/>
	</PreferenceCategory>
</PreferenceScreen>

PreferenceCategory是对Preferences进行分组的标签,显示类别标题,并在视觉上进行分隔Preference。如设计指南中的Settings Display页面的截图中Brightness/Lock display类别标签。

以下是Preference相关属性的介绍:

attr description
andoird:allowDividerAbove 在菜单上显示一条分割线
andoird:allowDividerBelow 在菜单下显示一条分割线
android:defaultValue 默认值。
android:dependency 设置此元素附属于另一个元素,依赖的可用则当前元素也可用(enable),反之。
andoird:enableCopying 启用长按复制
android:enabled 设置是否可用。
android:fragment 指定跳转fragment。
android:icon 指定左侧的图标。
andoird:iconSpaceReserved 为图标预留位置,菜单向右偏移,默认false
andoird:isPreferenceVisible 菜单是否显示
android:key 选项的名称,也是用来存储时唯一的key。
android:layout 给当前元素指定一个自定义布局。
android:order 偏好的顺序。如果不指定,默认的顺序将字母。
android:persistent 是否将其值持久化。
android:selectable 设置是否可以选择操作。
android:shouldDisableView 当enabled设置为false变暗,同时此属性设置为false时disable但不变暗。
andoird:singleLineTitle 菜单title限制为一行,默认为true
android:summary 摘要,配置的简要说明,显示在标题下面。
android:title 选项的标题,当没有设置summary时自动垂直居中显示。
android:widgetLayout 控件可调小部件的布局。是为一个优先选择的布局,比如一个复选框选择要指定一个自定义布局(注意:包括的只是复选框)在这里。

Setting中扩展的attr如下:

xml 复制代码
    <declare-styleable name="Preference">
        <!-- 搜索关键词 -->
        <attr name="keywords" format="string" />
        <!-- 是否可搜索,默认为true -->
        <attr name="searchable" format="boolean" />
        <!-- Preference controller类 -->
        <attr name="controller" format="string" />
        <!-- 自定义字幕 -->
        <attr name="unavailableSliceSubtitle" format="string" />
        <!-- Preference针对work profile,默认为false -->
        <attr name="forWork" format="boolean" />
        <!-- 用于在双窗格上突出显示菜单首选项的标识符 -->
        <attr name="highlightableMenuKey" format="string" />
    </declare-styleable>

查看Preference的源码可知,还有一些自定义Preference实现的组件,如CheckBoxPreference/DropDownPreference/EditTextPreference/ListPreference/SwitchPreference等,它是针对不同Android控件(checkbox/dropdown/edittext等)实现的自定义Preference,如需使用只需要在xml引用即可

然后创建一个fragment,继承于PreferenceFragmentCompat。onCreatePreferences方法在PreferenceFragmentCompat的onCreate方法调用,用于创建Prerefence。通过setPreferencesFromResource引用定义的Preference xml资源。这样通过Preference实现的一个简单的菜单就显示在屏幕上了。

java 复制代码
public class SettingsFragment extends PreferenceFragmentCompat {

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        setPreferencesFromResource(R.xml.root_preferences, rootKey);
    }
    //preference点击事件,通过key区分
    @Override
    public boolean onPreferenceTreeClick(@NonNull Preference preference) {
        return super.onPreferenceTreeClick(preference);
    }    
}

看下Preference的点击事件实现,如果对应的fragment实现了OnPreferenceStartFragmentCallback,重写了onPreferenceStartFragment方法,那么Preference的跳转实现就在onPreferenceStartFragment方法里,并返回处理结果,如果没有实现OnPreferenceStartFragmentCallback,则去获取xml中设置的android:fragment或者setFragment设置的fragment跳转。

java 复制代码
public abstract class PreferenceFragmentCompat extends Fragment implements
        PreferenceManager.OnPreferenceTreeClickListener,
        PreferenceManager.OnDisplayPreferenceDialogListener,
        PreferenceManager.OnNavigateToScreenListener,
        DialogPreference.TargetFragment {
	@Override
    public boolean onPreferenceTreeClick(@NonNull Preference preference) {
        if (preference.getFragment() != null) {
            boolean handled = false;
            if (getCallbackFragment() instanceof OnPreferenceStartFragmentCallback) {
                handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment())
                        .onPreferenceStartFragment(this, preference);
            }
            Fragment callbackFragment = this;
            while (!handled && callbackFragment != null) {
                if (callbackFragment instanceof OnPreferenceStartFragmentCallback) {
                    handled = ((OnPreferenceStartFragmentCallback) callbackFragment)
                            .onPreferenceStartFragment(this, preference);
                }
                callbackFragment = callbackFragment.getParentFragment();
            }
            if (!handled && getContext() instanceof OnPreferenceStartFragmentCallback) {
                handled = ((OnPreferenceStartFragmentCallback) getContext())
                        .onPreferenceStartFragment(this, preference);
            }
            if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback) {
                handled = ((OnPreferenceStartFragmentCallback) getActivity())
                        .onPreferenceStartFragment(this, preference);
            }
            if (!handled) {   
                final FragmentManager fragmentManager = getParentFragmentManager();
                final Bundle args = preference.getExtras();
                final Fragment fragment = fragmentManager.getFragmentFactory().instantiate(
                        requireActivity().getClassLoader(), preference.getFragment());
                fragment.setArguments(args);
                fragment.setTargetFragment(this, 0);
                fragmentManager.beginTransaction()
                        .replace(((View) requireView().getParent()).getId(), fragment)
                        .addToBackStack(null)
                        .commit();
            }
            return true;
        }
        return false;
    }            
}

androidx包中Preference只有针对Fragment的实现,没有针对Activity的实现。还有针对Dialog实现的PreferenceDialogFragmentCompat。在dialog里引用preference。

页面加载分析

本文以Android 13 Settings源码为例进行分析。

首页加载流程

在AndroidManifest.xml中可以看到,启动activity为Settings,Settings中包含大量的静态类继承于SettingsActivity。

xml 复制代码
 <activity-alias android:name="Settings"
                android:label="@string/settings_label_launcher"
                android:taskAffinity="com.android.settings.root"
                android:launchMode="singleTask"
                android:exported="true"
                android:targetActivity=".homepage.SettingsHomepageActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
        </activity-alias>

查看SettingsActivity的onCreate方法,首先读取fragment class和HighlightMenuKey,设置布局为settings_main_prefs.xml,通过intent传递的数据显示不同的布局。切换fragment或根据之前保存状态显示页面。settings_main_prefs.xml中包含顶部的switch bar,底部的button(Back/Skip/Next),和中间的framelayout显示Fragment,switch bar和button默认隐藏。

java 复制代码
public class SettingsActivity extends SettingsBaseActivity
        implements PreferenceManager.OnPreferenceTreeClickListener,
        PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
        ButtonBarHandler, FragmentManager.OnBackStackChangedListener {
    @Override
    protected void onCreate(Bundle savedState) {
        // Should happen before any call to getIntent()
        getMetaData();
        final Intent intent = getIntent();

        if (shouldShowTwoPaneDeepLink(intent) && tryStartTwoPaneDeepLink(intent)) {
            finish();
            super.onCreate(savedState);
            return;
        }

        super.onCreate(savedState);
        Log.d(LOG_TAG, "Starting onCreate");
        createUiFromIntent(savedState, intent);
    }
    private void getMetaData() {
        try {
            ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
                    PackageManager.GET_META_DATA);
            if (ai == null || ai.metaData == null) return;
            //读取设置的fragment
            mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
            mHighlightMenuKey = ai.metaData.getString(META_DATA_KEY_HIGHLIGHT_MENU_KEY);
        } catch (NameNotFoundException nnfe) {
            // No recovery
            Log.d(LOG_TAG, "Cannot get Metadata for: " + getComponentName().toString());
        }
    }
    protected void createUiFromIntent(Bundle savedState, Intent intent) {
        ...
        setContentView(R.layout.settings_main_prefs);
        ...
        if (savedState != null) {
            // We are restarting from a previous saved state; used that to initialize, instead
            // of starting fresh.
            setTitleFromIntent(intent);

            ArrayList<DashboardCategory> categories =
                    savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);
            if (categories != null) {
                mCategories.clear();
                mCategories.addAll(categories);
                setTitleFromBackStack();
            }
        } else {
            //加载fragment
            launchSettingFragment(initialFragmentName, intent);
        }
    }
}

AndroidManifest中并没有传递Settings对应的fragment数据,而是指定targetActivity为SettingsHomepageActivity,查看SettingsHomepageActivity的onCreate方法,布局为settings_homepage_container.xml,然后初始化搜索栏。设备不是低内存的情况下加载Suggestion菜单,设置fragment为TopLevelSettings。

java 复制代码
@Override
protected void onCreate(Bundle savedInstanceState) {
        ...
        setContentView(R.layout.settings_homepage_container);
    	...
        initSearchBarView();
        ...
        if (!getSystemService(ActivityManager.class).isLowRamDevice()) {
            initAvatarView();
            final boolean scrollNeeded = mIsEmbeddingActivityEnabled
                    && !TextUtils.equals(getString(DEFAULT_HIGHLIGHT_MENU_KEY), highlightMenuKey);
            showSuggestionFragment(scrollNeeded);
            if (FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) {
                showFragment(() -> new ContextualCardsFragment(), R.id.contextual_cards_content);
                ((FrameLayout) findViewById(R.id.main_content))
                        .getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
            }
        }
        mMainFragment = showFragment(() -> {
            final TopLevelSettings fragment = new TopLevelSettings();
            fragment.getArguments().putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY,
                    highlightMenuKey);
            return fragment;
        }, R.id.main_content);
        ...
}
private void initSearchBarView() {
        final Toolbar toolbar = findViewById(R.id.search_action_bar);
        FeatureFactory.getFactory(this).getSearchFeatureProvider()
                .initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE);

        if (mIsEmbeddingActivityEnabled) {
            final Toolbar toolbarTwoPaneVersion = findViewById(R.id.search_action_bar_two_pane);
            //初始化搜索实现
            FeatureFactory.getFactory(this).getSearchFeatureProvider()
                    .initSearchToolbar(this /* activity */, toolbarTwoPaneVersion,
                            SettingsEnums.SETTINGS_HOMEPAGE);
        }
    }

在搜索的实现类SearchFeatureProviderImpl中可以看到构造的搜索跳转intent如下,可以发现Settings的搜索核心实现在另外一个app内,包名为com.android.settings.intelligence。在后面会对搜索及这个模块进行深入分析,继续分析Settings页面加载。

java 复制代码
    @Override
    public Intent buildSearchIntent(Context context, int pageId) {
        return new Intent(Settings.ACTION_APP_SEARCH_SETTINGS)
                .setPackage(getSettingsIntelligencePkgName(context))
                .putExtra(Intent.EXTRA_REFERRER, buildReferrer(context, pageId));
    }

    default String getSettingsIntelligencePkgName(Context context) {
        return context.getString(R.string.config_settingsintelligence_package_name);
    }

    <!-- Settings intelligence package name -->
    <string name="config_settingsintelligence_package_name" translatable="false">
        com.android.settings.intelligence
    </string>

首先看下TopLevelSettings的继承关系,TopLevelSettings继承于DashboardFragment,DashboardFragment是静态和动态Settings 菜单的基类,Settings中大多数菜单对应的fragment继承于DashboardFragment,它继承于SettingsPreferenceFragment,SettingsPreferenceFragment是Settings fragment的基类,它继承于InstrumentedPreferenceFragment,它记录fragment显示状态,继承于ObservablePreferenceFragment,ObservablePreferenceFragment是在SettingsLib里定义的,模块路径:frameworks/base/packages/SettingsLib,在后面会对这个模块深入分析。ObservablePreferenceFragment继承于PreferenceFragmentCompat。

java 复制代码
public class TopLevelSettings extends DashboardFragment implements SplitLayoutListener,
        PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
            public TopLevelSettings() {
        final Bundle args = new Bundle();
        // Disable the search icon because this page uses a full search view in actionbar.
        args.putBoolean(NEED_SEARCH_ICON_IN_ACTION_BAR, false);
        setArguments(args);
    }
    //设置preference对应xml资源
    @Override
    protected int getPreferenceScreenResId() {
        return R.xml.top_level_settings;
    }
     @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        HighlightableMenu.fromXml(context, getPreferenceScreenResId());
        use(SupportPreferenceController.class).setActivity(getActivity());
    }
}

首先查看onAttach方法,调用DashboardFragment的use方法获取SupportPreferenceController实例。SupportPreferenceController存储在mPreferenceControllers中,通过addPreferenceController方法将PreferenceController添加到mPreferenceControllers中,在DashboardFragment的onAttach方法中会调用addPreferenceController,通过createPreferenceControllers方法将代码设置的controller添加到mControllers集合,然后读取xml中设置的controller添加到mControllers集合。

然后看下加载Preference的onCreatePreferences方法,首先通过getPreferenceScreenResId获取对应的xml资源,TopLevelSettings对应的是top_level_settings.xml,xml中定义了Settings首页菜单,最终通过addPreferencesFromResource方法显示Preference。在TopLevelSettings的onCreatePreferences方法还对图标颜色进行了处理。Preference点击事件调用Preferencecontroller的handlePreferenceTreeClick方法。

java 复制代码
public abstract class DashboardFragment extends SettingsPreferenceFragment
        implements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,
        BasePreferenceController.UiBlockListener {
        @Override
    	public void onAttach(Context context) {
        	super.onAttach(context);
            ...
             // Load preference controllers from code
        	final List<AbstractPreferenceController> controllersFromCode =
                createPreferenceControllers(context);
        	// Load preference controllers from xml definition
        	final List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper
                .getPreferenceControllersFromXml(context, 			             getPreferenceScreenResId());
        // Filter xml-based controllers in case a similar controller is created from code already.
        final List<BasePreferenceController> uniqueControllerFromXml =
                PreferenceControllerListHelper.filterControllers(
                        controllersFromXml, controllersFromCode);

        // Add unique controllers to list.
        if (controllersFromCode != null) {
            mControllers.addAll(controllersFromCode);
        }
        mControllers.addAll(uniqueControllerFromXml);
            for (AbstractPreferenceController controller : mControllers) {
            	addPreferenceController(controller);
        	}	
        }
        //获取对应Preference controller实例
        protected <T extends AbstractPreferenceController> T use(Class<T> clazz) {
        List<AbstractPreferenceController> controllerList = mPreferenceControllers.get(clazz);
        if (controllerList != null) {
            if (controllerList.size() > 1) {
                Log.w(TAG, "Multiple controllers of Class " + clazz.getSimpleName()
                        + " found, returning first one.");
            }
            return (T) controllerList.get(0);
        }

        return null;
    }
    protected void addPreferenceController(AbstractPreferenceController controller) {
        if (mPreferenceControllers.get(controller.getClass()) == null) {
            mPreferenceControllers.put(controller.getClass(), new ArrayList<>());
        }
        mPreferenceControllers.get(controller.getClass()).add(controller);
    }
    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        checkUiBlocker(mControllers);
        refreshAllPreferences(getLogTag());
     	...
    }
    private void refreshAllPreferences(final String tag) {
        ...
         // Add resource based tiles.
        displayResourceTiles();
        ...
    }
    private void displayResourceTiles() {
        final int resId = getPreferenceScreenResId();
        if (resId <= 0) {
            return;
        }
        addPreferencesFromResource(resId);
        final PreferenceScreen screen = getPreferenceScreen();
        screen.setOnExpandButtonClickListener(this);
        displayResourceTilesToScreen(screen);
    }
    @Override
    public boolean onPreferenceTreeClick(Preference preference) {
        final Collection<List<AbstractPreferenceController>> controllers =
                mPreferenceControllers.values();
        for (List<AbstractPreferenceController> controllerList : controllers) {
            for (AbstractPreferenceController controller : controllerList) {
                if (controller.handlePreferenceTreeClick(preference)) {
                    // log here since calling super.onPreferenceTreeClick will be skipped
                    writePreferenceClickMetric(preference);
                    return true;
                }
            }
        }
        return super.onPreferenceTreeClick(preference);
    }
}

接下来我们以SupportPreferenceController为例,分析下PreferenceController。它继承于BasePreferenceController,BasePreferenceController继承于AbstractPreferenceController。

java 复制代码
public class SupportPreferenceController extends BasePreferenceController {
    //指定显示状态
    @Override
    public int getAvailabilityStatus() {
        return mSupportFeatureProvider == null ? UNSUPPORTED_ON_DEVICE : AVAILABLE;
    }
	//点击事件
    @Override
    public boolean handlePreferenceTreeClick(Preference preference) {
        if (preference == null || mActivity == null ||
                !TextUtils.equals(preference.getKey(), getPreferenceKey())) {
            return false;
        }
        mSupportFeatureProvider.startSupport(mActivity);
        return true;

    }
}

AbstractPreferenceController是一个抽象类,主要方法如下:

java 复制代码
public abstract class AbstractPreferenceController {
    //preference是否有效
    public abstract boolean isAvailable();
    //preference点击事件
    public boolean handlePreferenceTreeClick(Preference preference) {
        return false;
    }
    //显示preference
    public void displayPreference(PreferenceScreen screen) {
        final String 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 (this instanceof Preference.OnPreferenceChangeListener) {
                final Preference preference = screen.findPreference(prefKey);
                preference.setOnPreferenceChangeListener(
                        (Preference.OnPreferenceChangeListener) this);
            }
        } else {
            setVisible(screen, prefKey, false /* visible */);
        }
    }
    //更新preference状态(summary)
    public void updateState(Preference preference) {
        refreshSummary(preference);
    }
    //preference key
    public abstract String getPreferenceKey();
}

BasePreferenceController对AbstractPreferenceController进行了简单封装,对Preference状态进行处理,共有6中状态,其次对Preference搜索支持也进行了处理。

java 复制代码
public abstract class BasePreferenceController extends AbstractPreferenceController implements Sliceable {
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({AVAILABLE, AVAILABLE_UNSEARCHABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_USER,
            DISABLED_DEPENDENT_SETTING, CONDITIONALLY_UNAVAILABLE})
    public @interface AvailabilityStatus {
    }
    //preference 有效
    public static final int AVAILABLE = 0;
    //preference 有效不能搜索
    public static final int AVAILABLE_UNSEARCHABLE = 1;
    //当前不可用,将来可能可用
    public static final int CONDITIONALLY_UNAVAILABLE = 2;
    //设备不支持
    public static final int UNSUPPORTED_ON_DEVICE = 3;
    //当前用户不支持
    public static final int DISABLED_FOR_USER = 4;
    //preference置灰,无法更改,依赖其它设置
    public static final int DISABLED_DEPENDENT_SETTING = 5;
    //指定Preference状态
    @AvailabilityStatus
    public abstract int getAvailabilityStatus();
    //preference 有效的实现
    @Override
    public final boolean isAvailable() {
        if (mIsForWork && mWorkProfileUser == null) {
            return false;
        }

        final int availabilityStatus = getAvailabilityStatus();
        return (availabilityStatus == AVAILABLE
                || availabilityStatus == AVAILABLE_UNSEARCHABLE
                || availabilityStatus == DISABLED_DEPENDENT_SETTING);
    }
    //针对DISABLED_DEPENDENT_SETTING状态进行置灰
    @Override
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);
        if (getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
            // Disable preference if it depends on another setting.
            final Preference preference = screen.findPreference(getPreferenceKey());
            if (preference != null) {
                preference.setEnabled(false);
            }
        }
    }
}

从上面可以看出来,Preferencecontroller它是Preference的控制器,控制Preference的显示,点击事件,搜索。Settings中大多数Preference的控制器都继承于BasePreferenceController。以上就是首页菜单加载的流程。

那SupportPreferenceContoller是那个菜单的控制器呢,它是首页Tips & support菜单的控制器。top_level_settings.xml有定义这个preference。Settings中大多数是以这样的实现构成的,xml定义Preference和引用Preferencecontroller,Preferencecontroller去实现对应菜单的逻辑。

二级页面加载流程

首先查看首页菜单的点击事件,它是获取了Preference controller的handlePreferenceTreeClick方法处理点击事件。

java 复制代码
public abstract class DashboardFragment extends SettingsPreferenceFragment
        implements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,
        BasePreferenceController.UiBlockListener {
            @Override
    public boolean onPreferenceTreeClick(Preference preference) {
        final Collection<List<AbstractPreferenceController>> controllers =
                mPreferenceControllers.values();
        for (List<AbstractPreferenceController> controllerList : controllers) {
            for (AbstractPreferenceController controller : controllerList) {
                if (controller.handlePreferenceTreeClick(preference)) {
                    // log here since calling super.onPreferenceTreeClick will be skipped
                    writePreferenceClickMetric(preference);
                    return true;
                }
            }
        }
        return super.onPreferenceTreeClick(preference);
    }
}

Preference controller的点击基础实现如下:

java 复制代码
public abstract class BasePreferenceController extends AbstractPreferenceController implements  Sliceable {
    @Override
    public boolean handlePreferenceTreeClick(Preference preference) {
        if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) {
            return super.handlePreferenceTreeClick(preference);
        }
        if (!mIsForWork || mWorkProfileUser == null) {
            return super.handlePreferenceTreeClick(preference);
        }
        final Bundle extra = preference.getExtras();
        extra.putInt(EXTRA_USER_ID, mWorkProfileUser.getIdentifier());
        new SubSettingLauncher(preference.getContext())
                .setDestination(preference.getFragment())
                .setSourceMetricsCategory(preference.getExtras().getInt(CATEGORY,
                        SettingsEnums.PAGE_UNKNOWN))
                .setArguments(preference.getExtras())
                .setUserHandle(mWorkProfileUser)
                .launch();
        return true;
    }
}

如果Preference controller不处理,则通过onPreferenceStartFragment方法,TopLevelSettings实现了OnPreferenceStartFragmentCallback,

java 复制代码
public class TopLevelSettings extends DashboardFragment implements SplitLayoutListener,
        PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
       @Override
    public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
        new SubSettingLauncher(getActivity())
                .setDestination(pref.getFragment())
                .setArguments(pref.getExtras())
                .setSourceMetricsCategory(caller instanceof Instrumentable
                        ? ((Instrumentable) caller).getMetricsCategory()
                        : Instrumentable.METRICS_CATEGORY_UNKNOWN)
                .setTitleRes(-1)
                .setIsSecondLayerPage(true)
                .launch();
        return true;
    }
}

可以发现,跳转二级页面的实现都是通过SubSettingLauncher来传递参数并且跳转目标fragment。toIntent方法构造调整intent,可以看到跳转的类是SubSettings,launcher方法进行跳转。

java 复制代码
public class SubSettingLauncher {
    public void launch() {
       	...
        final Intent intent = toIntent();

        boolean launchAsUser = mLaunchRequest.mUserHandle != null
                && mLaunchRequest.mUserHandle.getIdentifier() != UserHandle.myUserId();
        boolean launchForResult = mLaunchRequest.mResultListener != null;
        if (launchAsUser && launchForResult) {
            launchForResultAsUser(intent, mLaunchRequest.mUserHandle,
                    mLaunchRequest.mResultListener, mLaunchRequest.mRequestCode);
        } else if (launchAsUser && !launchForResult) {
            launchAsUser(intent, mLaunchRequest.mUserHandle);
        } else if (!launchAsUser && launchForResult) {
            launchForResult(mLaunchRequest.mResultListener, intent, mLaunchRequest.mRequestCode);
        } else {
            launch(intent);
        }
    }
    public Intent toIntent() {
        final Intent intent = new Intent(Intent.ACTION_MAIN);
        copyExtras(intent);
        intent.setClass(mContext, SubSettings.class);
        if (TextUtils.isEmpty(mLaunchRequest.mDestinationName)) {
            throw new IllegalArgumentException("Destination fragment must be set");
        }
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, mLaunchRequest.mDestinationName);

        if (mLaunchRequest.mSourceMetricsCategory < 0) {
            throw new IllegalArgumentException("Source metrics category must be set");
        }
        intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
                mLaunchRequest.mSourceMetricsCategory);

        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, mLaunchRequest.mArguments);
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RES_PACKAGE_NAME,
                mLaunchRequest.mTitleResPackageName);
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID,
                mLaunchRequest.mTitleResId);
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, mLaunchRequest.mTitle);
        intent.addFlags(mLaunchRequest.mFlags);
        intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
                mLaunchRequest.mTransitionType);
        intent.putExtra(SettingsActivity.EXTRA_IS_SECOND_LAYER_PAGE,
                mLaunchRequest.mIsSecondLayerPage);

        return intent;
    }

    @VisibleForTesting
    void launch(Intent intent) {
        mContext.startActivity(intent);
    }
}

SubSettings继承于SettingsActivity,说明首页菜单除了自定义实现页面跳转逻辑的之外,其它都是跳转到SubSettings这个Activity,这里有一个小技巧,正常情况下我们抓取当前页面的Activity可以通过以下命令:

shell 复制代码
adb shell dumpsys window | grep mCurrentFocus

但是我们不清楚这个页面对应的fragment,通过上面命令都是SubSettings。在跳转时可以通过以下命令获取fragment:

shell 复制代码
adb logcat -s "SubSettings"

这样就打印出了具体的启动fragment。

java 复制代码
public class SubSettings extends SettingsActivity {

    @Override
    public boolean onNavigateUp() {
        finish();
        return true;
    }

    @Override
    protected boolean isValidFragment(String fragmentName) {
        //打印页面
        Log.d("SubSettings", "Launching fragment " + fragmentName);
        return true;
    }
}

剩下的相关逻辑都和TopLevelSettings类似,这里不继续展开分析了。

动态插入菜单

在Settings里的一些菜单,我们会发现一些菜单在xml和代码中并未添加,但实际上显示在页面上,这是为什么呢?原来是Settings支持动态插入菜单。实现逻辑如下:

在创建Preference的时候,refreshAllPreferences方法刷新Preference,包括来自xml的静态Preference和动态Preference。动态Preference添加实现在refreshDashboardTiles方法中。

java 复制代码
public abstract class DashboardFragment extends SettingsPreferenceFragment
        implements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,
        BasePreferenceController.UiBlockListener {
    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        checkUiBlocker(mControllers);
        refreshAllPreferences(getLogTag());
        ...
    }
    private void refreshAllPreferences(final String tag) {
        ...
        // Add resource based tiles.
        displayResourceTiles();
		//动态Preference
        refreshDashboardTiles(tag);
    }
    private void refreshDashboardTiles(final String tag) {
        final PreferenceScreen screen = getPreferenceScreen();

        final DashboardCategory category =
                mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
      	...
        final List<Tile> tiles = category.getTiles();
        // Create a list to track which tiles are to be removed.
        final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys);

        // Install dashboard tiles and collect pending observers.
        final boolean forceRoundedIcons = shouldForceRoundedIcon();
        final List<DynamicDataObserver> pendingObservers = new ArrayList<>();
        for (Tile tile : tiles) {
            final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
            ...
            final List<DynamicDataObserver> observers;
            if (mDashboardTilePrefKeys.containsKey(key)) {
                // Have the key already, will rebind.
                final Preference preference = screen.findPreference(key);
                observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
                        getActivity(), this, forceRoundedIcons, preference, tile, key,
                        mPlaceholderPreferenceController.getOrder());
            } else {
                // Don't have this key, add it.
                final Preference pref = createPreference(tile);
                observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
                        getActivity(), this, forceRoundedIcons, pref, tile, key,
                        mPlaceholderPreferenceController.getOrder());
                //添加Preference
                screen.addPreference(pref);
                registerDynamicDataObservers(observers);
                mDashboardTilePrefKeys.put(key, observers);
            }
            if (observers != null) {
                pendingObservers.addAll(observers);
            }
            remove.remove(key);
            ...
   //类别key             
   public String getCategoryKey() {
        return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());
    }
}

首先获取了类别 key,PARENT_TO_CATEGORY_KEY_MAP中实现了页面和key的对应。通过页面class name来确定页面对应的key。

java 复制代码
public class DashboardFragmentRegistry {
     static {
        PARENT_TO_CATEGORY_KEY_MAP = new ArrayMap<>();
        PARENT_TO_CATEGORY_KEY_MAP.put(TopLevelSettings.class.getName(),
                CategoryKey.CATEGORY_HOMEPAGE);
        PARENT_TO_CATEGORY_KEY_MAP.put(
                NetworkDashboardFragment.class.getName(), CategoryKey.CATEGORY_NETWORK);
        PARENT_TO_CATEGORY_KEY_MAP.put(ConnectedDeviceDashboardFragment.class.getName(),
                CategoryKey.CATEGORY_CONNECT);
        PARENT_TO_CATEGORY_KEY_MAP.put(AdvancedConnectedDeviceDashboardFragment.class.getName(),
        ...
     }
}

页面key的定义在CategoryKey类中。这样通过key就清楚当前页面是否动态加载那些菜单。

java 复制代码
public final class CategoryKey {
     // Activities in this category shows up in Settings homepage.
    public static final String CATEGORY_HOMEPAGE = "com.android.settings.category.ia.homepage";

    // Top level category.
    public static final String CATEGORY_NETWORK = "com.android.settings.category.ia.wireless";
    public static final String CATEGORY_CONNECT = "com.android.settings.category.ia.connect";
    public static final String CATEGORY_DEVICE = "com.android.settings.category.ia.device";
    public static final String CATEGORY_APPS = "com.android.settings.category.ia.apps";
    ...
}

getTilesForCategory方法的实现在DashboardFeatureProviderImpl类中,它是通过CategoryManager类的getTilesByCategory方法实现。mCategories是获取所有动态菜单的集合。

java 复制代码
public class CategoryManager {
    public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) {
        tryInitCategories(context);

        return mCategoryByKeyMap.get(categoryKey);
    }
    private synchronized void tryInitCategories(Context context) {
        // Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange
        // happens.
        tryInitCategories(context, false /* forceClearCache */);
    }

    private synchronized void tryInitCategories(Context context, boolean forceClearCache) {
        if (mCategories == null) {
            final boolean firstLoading = mCategoryByKeyMap.isEmpty();
            if (forceClearCache) {
                mTileByComponentCache.clear();
            }
            mCategoryByKeyMap.clear();
            //获取categories list
            mCategories = TileUtils.getCategories(context, mTileByComponentCache);
            for (DashboardCategory category : mCategories) {
                mCategoryByKeyMap.put(category.key, category);
            }
            backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap);
            sortCategories(context, mCategoryByKeyMap);
            filterDuplicateTiles(mCategoryByKeyMap);
            if (firstLoading) {
                logTiles(context);

                final DashboardCategory homepageCategory = mCategoryByKeyMap.get(
                        CategoryKey.CATEGORY_HOMEPAGE);
                if (homepageCategory == null) {
                    return;
                }
                for (Tile tile : homepageCategory.getTiles()) {
                    final String key = tile.getKey(context);
                    if (TextUtils.isEmpty(key)) {
                        Log.w(TAG, "Key hint missing for homepage tile: " + tile.getTitle(context));
                        continue;
                    }
                    HighlightableMenu.addMenuKey(key);
                }
            }
        }
    }
}

从loadActivityTiles方法里可以看出,在Settings里动态插入菜单只能是系统应用。

源码路径:frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java

java 复制代码
public class TileUtils {
    public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS";
    public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS";
     private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS";

    public static List<DashboardCategory> getCategories(Context context,
            Map<Pair<String, String>, Tile> cache) {
        final long startTime = System.currentTimeMillis();
        final boolean setup =
                Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0;
        final ArrayList<Tile> tiles = new ArrayList<>();
        final UserManager userManager = (UserManager) context.getSystemService(
                Context.USER_SERVICE);
        for (UserHandle user : userManager.getUserProfiles()) {
            // TODO: Needs much optimization, too many PM queries going on here.
            if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
                loadTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
                loadTilesForAction(context, user, OPERATOR_SETTINGS, cache,
                        OPERATOR_DEFAULT_CATEGORY, tiles, false);
                loadTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
                        MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
            }
            if (setup) {
                loadTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
                loadTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);
            }
        }
        ...
        return categories;
    }
    static void loadTilesForAction(Context context,
            UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
            String defaultCategory, List<Tile> outTiles, boolean requireSettings) {
        final Intent intent = new Intent(action);
        if (requireSettings) {
	        // 只允许settings通过SETTINGS_ACTION添加
            intent.setPackage(SETTING_PKG);
        }
        loadActivityTiles(context, user, addedCache, defaultCategory, outTiles, intent);
        loadProviderTiles(context, user, addedCache, defaultCategory, outTiles, intent);
    }
    
    private static void loadActivityTiles(Context context,
            UserHandle user, Map<Pair<String, String>, Tile> addedCache,
            String defaultCategory, List<Tile> outTiles, Intent intent) {
        final PackageManager pm = context.getPackageManager();
        final List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
                PackageManager.GET_META_DATA, user.getIdentifier());
        for (ResolveInfo resolved : results) {
            //系统应用
            if (!resolved.system) {
                // Do not allow any app to add to settings, only system ones.
                continue;
            }
            final ActivityInfo activityInfo = resolved.activityInfo;
            final Bundle metaData = activityInfo.metaData;
            loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData, activityInfo);
        }
    }
    private static void loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache,
            String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData,
            ComponentInfo componentInfo) {
        // Skip loading tile if the component is tagged primary_profile_only but not running on
        // the current user.
        if (user.getIdentifier() != ActivityManager.getCurrentUser()
                && Tile.isPrimaryProfileOnly(componentInfo.metaData)) {
            Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "
                    + intent + " is primary profile only, skip loading tile for uid "
                    + user.getIdentifier());
            return;
        }

        String categoryKey = defaultCategory;
        // Load category
        categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
        final boolean isProvider = componentInfo instanceof ProviderInfo;
        final Pair<String, String> key = isProvider
                ? new Pair<>(((ProviderInfo) componentInfo).authority,
                        metaData.getString(META_DATA_PREFERENCE_KEYHINT))
                : new Pair<>(componentInfo.packageName, componentInfo.name);
        Tile tile = addedCache.get(key);
        if (tile == null) {
            tile = isProvider
                    ? new ProviderTile((ProviderInfo) componentInfo, categoryKey, metaData)
                    : new ActivityTile((ActivityInfo) componentInfo, categoryKey);
            addedCache.put(key, tile);
        } else {
            tile.setMetaData(metaData);
        }
        if (!tile.userHandle.contains(user)) {
            tile.userHandle.add(user);
        }
        if (!outTiles.contains(tile)) {
            outTiles.add(tile);
        }
    }
}

然后遍历tiles集合,Tile类里包含Preference的数据(Key/order/intent等等),也可以设置这些字段的key。

最后一个动态菜单就被成功添加到当前页面了。我们以System->Developer options 菜单为例,它是被动态添加到Settings里的菜单。它定义在Settings的AndroidManifest.xml中。

xml 复制代码
<activity
            android:name="Settings$DevelopmentSettingsDashboardActivity"
            android:label="@string/development_settings_title"
            android:icon="@drawable/ic_settings_development"
            android:exported="true"
            android:enabled="false">
            <intent-filter android:priority="1">
                <action android:name="android.settings.APPLICATION_DEVELOPMENT_SETTINGS" />
                <action android:name="com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS" />
                <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter>
                <action android:name="com.android.settings.action.SETTINGS" />
            </intent-filter>
            <meta-data android:name="com.android.settings.order" android:value="-40"/>
            <meta-data android:name="com.android.settings.category"
                       android:value="com.android.settings.category.ia.system" />
            <meta-data android:name="com.android.settings.summary"
                       android:resource="@string/summary_empty"/>
            <meta-data android:name="com.android.settings.icon"
                       android:resource="@drawable/ic_settings_development" />
            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
                       android:value="com.android.settings.development.DevelopmentSettingsDashboardFragment" />
            <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
                       android:value="@string/menu_key_system"/>
            <meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
                       android:value="true" />
        </activity>

首先它设置action为com.android.settings.action.SETTING,从前面TileUtils类分析知道这个action只能Settings里添加时设置。然后设置菜单顺序,菜单category为com.android.settings.category.ia.system,查阅DashboardFragmentRegistry类中PARENT_TO_CATEGORY_KEY_MAP的对应关系,可知对应页面fragment为SystemDashboardFragment,即System页面,接着指定了Summary,icon,fragment等等。这样开发者选项菜单就被插入到了System页面下。

Settings中还存在其它动态插入的选项,例如Google GMS插入的首页菜单Google和Digital Wellbeing & parental controlls。

因为很多应用需要在Settings中增加菜单,作为应用的入口,这种不需要修改Settings代码,而直接修改应用的AndroidManifest.xml文件,实现解耦并自动适配。当然只有系统应用可以动态在Settings插入菜单。

SettingsLib

在分析Settings页面加载分析的时候,发现有部分类来自SettingsLib模块,这个模块是干嘛的呢?

SettingsLib是Android系统中一个专注于为Settings应用提供服务的库。它包含了许多Settings基础功能,并封装了一些操作。

源码路径:frameworks/base/packages/SettingsLib

从bp文件可知,编译后会生成一个SettingsLib的jar包。SettingsLib下根据不同功能,UI基础实现有许多模块,SettingsLib引用这些模块。

SettingsLib模块只有具有系统级别权限如系统应用,framework等才可以调用,第三方应用无法使用。

此时在想,为什么不直接在Settings中直接实现呢?因为将不同功能,UI等的基础实现放在一个公共模块中,可以方便其它与Settings交互的模块或framework使用,进行定制使用,因此,SettingsLib虽专注于为Settings,但它服务于系统,供系统进行Settings相关扩展使用,例如SystemUI模块就在使用SettingsLib相关实现。

相关资料

官方文档:Android"设置"菜单

总结

AndroidSettings具有以下优势:

  • 界面。引入Preference显示菜单设置项。统一的页面风格,页面简单,标题状态清晰。

  • 扩展性。Android Settings页面采用单个Activity(SubSettings),多Fragment,支持其它系统应用在Settings添加菜单,可扩展性强。

    Preference和PreferenceController的配合使用,方便定制新的设置项和页面,厂商定制性高。

  • 使用。添加了搜索,让用户可以轻松快速找到设置项。界面也决定了用户可以轻松修改各种设置项。

相关推荐
Dnelic-2 个月前
【笔记】Android 多用户模式和用户类型
android·framework·aosp·多用户模式·usermanager
ttdevs2 个月前
【Android】ServiceNotFoundException: No service published for: search
android·framework·aosp·selinux·search
千里马学框架2 个月前
安卓xml乱码/加密转换:abx2xml和xml2abx使用及源码介绍
android·xml·车载系统·framework·系统开发·车载开发·系统应用
AFinalStone2 个月前
Android 12系统源码_Settings(一)认识Preference
android·frameworks·preference
Dnelic-3 个月前
【笔记】Android Settings 应用设置菜单的界面代码介绍
android·安卓·settings·应用开发
Sgq丶5 个月前
Android 13 aosp 默认关闭SELinux
android·aosp·selinux
土拨鼠不是老鼠5 个月前
QML 本地存储(Setting,sqlite)
开发语言·前端·javascript·sqlite·settings·qml 本地存储
吴同学是个程序员7 个月前
【Android】源码中的工厂方法模式
android·设计模式·工厂方法模式·aosp
吴同学是个程序员7 个月前
【Android】AOSP 架构
android·安卓·aosp·安卓架构
小先生Zcutie7 个月前
【Android】隐藏settings中的二级菜单
android·settings