原文链接:https://blog.csdn.net/wangwei6227/article/details/130935976
Android设备中,很多应用需要增加在Settings中增加菜单,作为应用的入口。此时可以仿照google GMS包的应用,采用动态加载的方式。这种方法不需要修改Settings中代码,修改应用本身的AndroidManifest.xml文件就行,实现解耦并自动适配
1、使用方法
1.1、示例:
在AndroidMainfest.xml中增加如下配置
<!--最后的效果是Settings-Display中会增加一个菜单,点击该菜单进入MainActivity-->
<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!--Settings会检测该action来查找要插入的菜单-->
<intent-filter android:priority="6">
<action android:name="com.android.settings.action.EXTRA_SETTINGS" />
</intent-filter>
<!--确定在哪个界面添加,这是表示在Settings-display中增加,
具体界面的值可以在CategoryKey.java中查看-->
<meta-data
android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.display" />
<!--菜单标题-->
<meta-data
android:name="com.android.settings.title"
android:value="@string/app_name"/>
<!--菜单summary-->
<meta-data
android:name="com.android.settings.summary"
android:resource="@string/summary_text" />
<!--菜单icon-->
<meta-data
android:name="com.android.settings.icon"
android:resource="@drawable/icon_setings" />
<!--菜单显示位置-->
<meta-data
android:name="com.android.settings.order"
android:value="-100" />
</activity>
1.2、权限
动态插入菜单有一个条件,需要插入菜单的应用必须为system应用,若要所有的应用都能插入菜单,需要修改Settings源码中这段逻辑
frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
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();
//获取meta data
final List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
PackageManager.GET_META_DATA, user.getIdentifier());
for (ResolveInfo resolved : results) {
if (!resolved.system) { //判断是否为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);
}
}
2、原理
查看Settings源码,分析流程
2.1、数据更新
SettingsActivity.java。Settings中的界面主要为Settings和SubSettings,都需要继承SettingsActivity。故数据更新在该类onResume中
SettingsActivity-onResume
SettingsActivity:
@Override
protected void onResume() {
...
updateTilesList(); //1
}
private void updateTilesList() {
// Generally the items that are will be changing from these updates will
// not be in the top list of tiles, so run it in the background and the
// SettingsBaseActivity will pick up on the updates automatically.
AsyncTask.execute(() -> doUpdateTilesList()); //2
}
private void doUpdateTilesList() {
// Final step, refresh categories.
if (somethingChanged) {
Log.d(LOG_TAG, "Enabled state changed for some tiles, reloading all categories "
+ changedList.toString());
mCategoryMixin.updateCategories(); //3
} else {
Log.d(LOG_TAG, "No enabled state changed, skipping updateCategory call");
}
}
CategoryMixin:
/**
* Updates dashboard categories.
*/
public void updateCategories() {
updateCategories(false /* fromBroadcast */); //4
}
private void updateCategories(boolean fromBroadcast) {
// Only allow at most 2 tasks existing at the same time since when the first one is
// executing, there may be new data from the second update request.
// Ignore the third update request because the second task is still waiting for the first
// task to complete in a serial thread, which will get the latest data.
if (mCategoriesUpdateTaskCount < 2) {
new CategoriesUpdateTask().execute(fromBroadcast); //5
}
}
CategoryMixin$CategoriesUpdateTask:
@Override
protected Set<String> doInBackground(Boolean... params) {
mPreviousTileMap = mCategoryManager.getTileByComponentMap();
mCategoryManager.reloadAllCategories(mContext); //6
mCategoryManager.updateCategoryFromDenylist(sTileDenylist);
return getChangedCategories(params[0]);
}
CategoryManager:
public synchronized void reloadAllCategories(Context context) {
final boolean forceClearCache = mInterestingConfigChanges.applyNewConfig(
context.getResources());
mCategories = null;
tryInitCategories(context, forceClearCache); //7 forceClearCache用来保存Tile
}
private synchronized void tryInitCategories(Context context, boolean forceClearCache) {
...
mCategories = TileUtils.getCategories(context, mTileByComponentCache); //8
...
}
TileUtils:
/**
* Build a list of DashboardCategory.
*/
public static List<DashboardCategory> getCategories(Context context,
Map<Pair<String, String>, Tile> cache) {
...
loadTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true); //9
...
}
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) {
intent.setPackage(SETTING_PKG);
}
//10
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); //11
}
}
private static void loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData,
ComponentInfo componentInfo) {} //12
2.2、插入Preference
DashboardFragment:
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
checkUiBlocker(mControllers);
refreshAllPreferences(getLogTag());//1
...
}
private void refreshAllPreferences(final String tag) {
refreshDashboardTiles(tag); //2
}
/**
* Refresh preference items backed by DashboardCategory.
*/
private void refreshDashboardTiles(final String tag) {
final PreferenceScreen screen = getPreferenceScreen(); //获取根布局PreferenceScreen
//3 获取Category
final DashboardCategory category =
mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
...
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());
screen.addPreference(pref); //4 添加Preference,即插入菜单
registerDynamicDataObservers(observers);
mDashboardTilePrefKeys.put(key, observers);
}
3、增加搜索
继承SearchIndexablesProvider(framework/./base/core/java/android/provider/SearchIndexablesProvider.java)
可在设置中增加搜索
<provider
android:name=".search.SettingsSearchIndexablesProvider"
android:authorities="com.android.settings"
android:multiprocess="false"
android:grantUriPermissions="true"
android:permission="android.permission.READ_SEARCH_INDEXABLES"
android:exported="true">
<intent-filter>
<action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER" />
</intent-filter>
</provider>
分析SearchIndexablesProvider源码,主要重写以下几个方法:
queryXmlResources 返回xml(android.provider.SearchIndexablesContract.XmlResource)资源类型的菜单信息
queryRawData 返回RawData(android.provider.SearchIndexablesContract.RawData)菜单元数据
queryNonIndexableKeys 返回不加入搜索的菜单key信息
queryDynamicRawData 返回动态菜单元数据
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
try {
switch (mMatcher.match(uri)) {
case MATCH_RES_CODE:
return queryXmlResources(null);
case MATCH_RAW_CODE:
return queryRawData(null);
case MATCH_NON_INDEXABLE_KEYS_CODE:
return queryNonIndexableKeys(null);
case MATCH_SITE_MAP_PAIRS_CODE:
return querySiteMapPairs();
case MATCH_SLICE_URI_PAIRS_CODE:
return querySliceUriPairs();
case MATCH_DYNAMIC_RAW_CODE:
return queryDynamicRawData(null);
default:
throw new UnsupportedOperationException("Unknown Uri " + uri);
}
} catch (UnsupportedOperationException e) {
throw e;
} catch (Exception e) {
Log.e(TAG, "Provider querying exception:", e);
return null;
}
}