Android 11 Settings源码入门

本篇主要探讨Android 11中系统设置的简单实现,主要包括一下几点:

  • 系统设置首页
  • 系统设置其他界面
  • 数据控制

一,系统设置首页

Android系统设置的主界面是com.android.settings.Settings, 但是它只是一个activity-alias, 指向的是.homepage.SettingsHomepageActivity


    
        
        
        
    
    

需要注意的是通过命令(adb shell "dumpsys window | grep mCurrentFocus")查看手机顶层activity时,打印出的不是targetActivity,而是这个activity-alias.

.homepage.SettingsHomepageActivity中的逻辑并不复杂,直接加载了TopLevelSettings这个Fragment

showFragment(new TopLevelSettings(), R.id.main_content);

TopLevelSettings通过AndroidXPreference来展示设置项列表,设置项列表的内容通过静态配置+动态添加的方式获取。

1,静态配置

所谓静态配置就是通过xml来配置。
如果你还不了解Preference,可以移步:https://www.jianshu.com/p/348eb0928af7 简单了解一下

TopLevelSettings继承自抽象类DashboardFragment, 实现抽象方法getPreferenceScreenResId()并返回preference的配置文件即可完成静态配置。

@Override
protected int getPreferenceScreenResId() {
    return R.xml.top_level_settings;
}

top_level_settings中配置了页面需要展示的配置项:



    

    
        
    ...
    

其中:

  • key:该配置项的主键
  • title:配置项的标题
  • summary:概要标题下面的文字
  • icon:前面的图标
  • order:用来做排序的,值越小则排行越靠前
  • fragment:点击该item要跳转的界面
  • controller:该item的控制器,控制它的内容展示,是否可用,也可以控制它的点击事件
2,动态添加

动态获取是根据特殊的action标记,通过packageManger查询系统中安装的符合对应action的应用,将其动态添加到列表中。

例如:网络流量监控,存储空间管理,默认应用等配置项都是动态添加的。

具体实现可以参看文章:Android 11 Settings动态加载之快霸是如何被加载的

二,系统设置其他界面

系统设置中除了.homepage.SettingsHomepageActivity,其他大部分的Activity都定义在Settings中, 并且继承自SettingsActivity, 但其中并没有实现任何逻辑。因此,这些Activity的逻辑都是在SettingsActivity中实现。

/**
 * Top-level Settings activity
 */
public class Settings extends SettingsActivity {

    /*
    * Settings subclasses for launching independently.
    */
    public static class AssistGestureSettingsActivity extends SettingsActivity { /* empty */}
    public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }
    public static class CreateShortcutActivity extends SettingsActivity { /* empty */ }
    public static class FaceSettingsActivity extends SettingsActivity { /* empty */ }
    public static class FingerprintSettingsActivity extends SettingsActivity { /* empty */ }
    ...
}

这些Activity中并没有实现任何逻辑,那它是怎么加载到自己应有的布局的呢?

在父类SettingsActivityonCreate()中:

@Override
protected void onCreate(Bundle savedState) {
    
    ...
    // Should happen before any call to getIntent()
    // 第一步
    getMetaData();
    // 第二步
    final Intent intent = getIntent();
    if (intent.hasExtra(EXTRA_UI_OPTIONS)) {
        getWindow().setUiOptions(intent.getIntExtra(EXTRA_UI_OPTIONS, 0));
    }

    // Getting Intent properties can only be done after the super.onCreate(...)
    final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
    ...
    // 第三步
    launchSettingFragment(initialFragmentName, intent);
    
    ...
}

跟着上面的三个步骤:

第一步

首先通过getMetaData()获取该Activity在manifest中配置的fragment, 并赋值给mFragmentClass

public static final String META_DATA_KEY_FRAGMENT_CLASS = "com.android.settings.FRAGMENT_CLASS";
            
private void getMetaData() {
    try {
        ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
                PackageManager.GET_META_DATA);
        if (ai == null || ai.metaData == null) return;
        mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
    } catch (NameNotFoundException nnfe) {
        // No recovery
        Log.d(LOG_TAG, "Cannot get Metadata for: " + getComponentName().toString());
    }
}

那么manifest中是怎么配置的呢?如下:


    
        
        
        
    
    

由此可知WifiInfoActivity这个Acitivity对应的fragment是:com.android.settings.wifi.WifiInfo

第二步

通过getIntent()构造包含EXTRA_SHOW_FRAGMENT的intent

public Intent getIntent() {
    Intent superIntent = super.getIntent();
    String startingFragment = getStartingFragmentClass(superIntent);
    // This is called from super.onCreate, isMultiPane() is not yet reliable
    // Do not use onIsHidingHeaders either, which relies itself on this method
    if (startingFragment != null) {
        Intent modIntent = new Intent(superIntent);
        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment);
        Bundle args = superIntent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
        if (args != null) {
            args = new Bundle(args);
        } else {
            args = new Bundle();
        }
        args.putParcelable("intent", superIntent);
        modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
        return modIntent;
    }
    return superIntent;
}

/**
 * Checks if the component name in the intent is different from the Settings class and
 * returns the class name to load as a fragment.
 */
private String getStartingFragmentClass(Intent intent) {
    // 存在mFragmentClass则直接返回
    if (mFragmentClass != null) return mFragmentClass;

    String intentClass = intent.getComponent().getClassName();
    if (intentClass.equals(getClass().getName())) return null;

    if ("com.android.settings.RunningServices".equals(intentClass)
            || "com.android.settings.applications.StorageUse".equals(intentClass)) {
        // Old names of manage apps.
        intentClass = ManageApplications.class.getName();
    }

    return intentClass;
}

这里包含了mFragmentClass为空的情况,暂时先不管。

第三步

通过launchSettingFragment()启动对应Fragment,这里的initialFragmentName参数就是第二步Intent中包含的EXTRA_SHOW_FRAGMENT参数,mFragmentClass不为空的情况下传入的就是mFragmentClass

void launchSettingFragment(String initialFragmentName, Intent intent) {
    if (initialFragmentName != null) {
        setTitleFromIntent(intent);

        Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
        switchToFragment(initialFragmentName, initialArguments, true,
                mInitialTitleResId, mInitialTitle);
    } else {
        // Show search icon as up affordance if we are displaying the main Dashboard
        mInitialTitleResId = R.string.dashboard_title;
        switchToFragment(TopLevelSettings.class.getName(), null /* args */, false,
                mInitialTitleResId, mInitialTitle);
    }
}

switchToFragment()中将fragment添加到activity中。

/**
 * Switch to a specific Fragment with taking care of validation, Title and BackStack
 */
private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate,
        int titleResId, CharSequence title) {
    Log.d(LOG_TAG, "Switching to fragment " + fragmentName);
    if (validate && !isValidFragment(fragmentName)) {
        throw new IllegalArgumentException("Invalid fragment for this activity: "
                + fragmentName);
    }
    // 反射创建fragment
    Fragment f = Utils.getTargetFragment(this, fragmentName, args);
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.main_content, f);
    if (titleResId > 0) {
        transaction.setBreadCrumbTitle(titleResId);
    } else if (title != null) {
        transaction.setBreadCrumbTitle(title);
    }
    // 提交事务
    transaction.commitAllowingStateLoss();
    getSupportFragmentManager().executePendingTransactions();
    Log.d(LOG_TAG, "Executed frag manager pendingTransactions");
    return f;
}

三,数据控制

在首页-电池设置项中可以显示实时电量。那么它是如果实现的呢?

首先看下它是如何配置的:


配置项中配置了TopLevelBatteryPreferenceController控制器,它继承自AbstractPreferenceController,这个抽象类用于对所有菜单项进行统一管理(例如展示或隐藏,监听点击事件等)。

TopLevelBatteryPreferenceController代码如下:

public class TopLevelBatteryPreferenceController extends BasePreferenceController implements
        LifecycleObserver, OnStart, OnStop {

    // 电量改变广播
    private final BatteryBroadcastReceiver mBatteryBroadcastReceiver;
    // 当前配置项
    private Preference mPreference;
    // 电量信息
    private BatteryInfo mBatteryInfo;

    public TopLevelBatteryPreferenceController(Context context, String preferenceKey) {
        super(context, preferenceKey);
        mBatteryBroadcastReceiver = new BatteryBroadcastReceiver(mContext);
        mBatteryBroadcastReceiver.setBatteryChangedListener(type -> {
            BatteryInfo.getBatteryInfo(mContext, info -> {
                mBatteryInfo = info;
                updateState(mPreference);
            }, true /* shortString */);
        });
    }

    // 控制该项是否可用
    @Override
    public int getAvailabilityStatus() {
        return mContext.getResources().getBoolean(R.bool.config_show_top_level_battery)
                ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
    }

    @Override
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);
        // 获取当前的配置项
        mPreference = screen.findPreference(getPreferenceKey());
    }

    @Override
    public void onStart() {
        // 注册广播
        mBatteryBroadcastReceiver.register();
    }

    @Override
    public void onStop() {
        // 取消注册广播
        mBatteryBroadcastReceiver.unRegister();
    }

    @Override
    public CharSequence getSummary() {
        // 返回电量概览
        return getDashboardLabel(mContext, mBatteryInfo);
    }

    // 获取电量信息
    static CharSequence getDashboardLabel(Context context, BatteryInfo info) {
        if (info == null || context == null) {
            return null;
        }
        CharSequence label;
        if (!info.discharging && info.chargeLabel != null) {
            label = info.chargeLabel;
        } else if (info.remainingLabel == null) {
            label = info.batteryPercentString;
        } else {
            label = context.getString(R.string.power_remaining_settings_home_page,
                    info.batteryPercentString,
                    info.remainingLabel);
        }
        return label;
    }
}

代码比较简单:

  1. 在构造方法中初始化电量改变广播
  2. onStart()onStop()中注册和取消注册广播
  3. 一旦收到电量改变广播,则把电量信息保存在mBatteryInfo
  4. 然后执行updateState(),该方法会调用getSummary()把信息设置给当前配置项
  5. getSummary()中将mBatteryInfo保存的电量信息解析出来
小结:

菜单项的展示、隐藏、监听点击事件等都是通过继承自AbstractPreferenceController的控制器完成,这个控制器有时候实在xml中配置,有些时候则是在fragment中动态添加。

免责声明:最近做了一段时间Android 11系统设置应用相关的开发,将相关代码简单梳理总结了一下,讲解不当的地方还请大佬们指出

参考:https://blog.csdn.net/qq_34149526/article/details/83239567

你可能感兴趣的:(Android 11 Settings源码入门)