/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.settings.common; import static com.android.settingslib.drawer.CategoryKey.CATEGORY_DEVICE; import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI; import static java.lang.String.CASE_INSENSITIVE_ORDER; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.TextUtils; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import com.android.car.settings.R; import com.android.car.ui.preference.CarUiPreference; import com.android.settingslib.drawer.TileUtils; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * Loads Activity with TileUtils.EXTRA_SETTINGS_ACTION. */ // TODO: investigate using SettingsLib Tiles. public class ExtraSettingsLoader { static final String META_DATA_PREFERENCE_IS_TOP_LEVEL = "injectedTopLevelPreference"; private static final Logger LOG = new Logger(ExtraSettingsLoader.class); private static final String META_DATA_PREFERENCE_CATEGORY = "com.android.settings.category"; private final Context mContext; private final Set mTopLevelCategories; private final boolean mIsTopLevelSummariesEnabled; private Map mPreferenceBundleMap; private PackageManager mPm; public ExtraSettingsLoader(Context context) { mContext = context; mPm = context.getPackageManager(); mPreferenceBundleMap = new LinkedHashMap<>(); mTopLevelCategories = Set.of(mContext.getResources().getStringArray( R.array.config_top_level_injection_categories)); mIsTopLevelSummariesEnabled = mContext.getResources().getBoolean( R.bool.config_top_level_injection_enable_summaries); } @VisibleForTesting void setPackageManager(PackageManager pm) { mPm = pm; } /** * Returns a map of {@link Preference} and {@link Bundle} representing settings injected from * system apps and their metadata. The given intent must specify the action to use for * resolving activities and a category with the key "com.android.settings.category" and one of * the values in {@link com.android.settingslib.drawer.CategoryKey}. * * {@link com.android.settingslib.drawer.TileUtils#EXTRA_SETTINGS_ACTION} is automatically added * for backwards compatibility. Please make sure to use * {@link com.android.settingslib.drawer.TileUtils#IA_SETTINGS_ACTION} instead. * * @param intent intent specifying the extra settings category to load */ public Map loadPreferences(Intent intent) { intent.setAction(TileUtils.IA_SETTINGS_ACTION); List results = mPm.queryIntentActivitiesAsUser(intent, PackageManager.GET_META_DATA, ActivityManager.getCurrentUser()); intent.setAction(TileUtils.EXTRA_SETTINGS_ACTION); List extra_settings_results = mPm.queryIntentActivitiesAsUser(intent, PackageManager.GET_META_DATA, ActivityManager.getCurrentUser()); for (ResolveInfo extra_settings_resolveInfo : extra_settings_results) { if (!results.contains(extra_settings_resolveInfo)) { results.add(extra_settings_resolveInfo); } } String extraCategory = intent.getStringExtra(META_DATA_PREFERENCE_CATEGORY); // Filter to only include valid results and then sort the results // Filter criteria: must be a system application and must have metaData // Sort criteria: sort results based on [order, package within order] results = results.stream() .filter(r -> r.system && r.activityInfo != null && r.activityInfo.metaData != null) .sorted((r1, r2) -> { // First sort by order int orderCompare = r2.activityInfo.metaData.getInt(META_DATA_KEY_ORDER) - r1.activityInfo.metaData.getInt(META_DATA_KEY_ORDER); if (orderCompare != 0) { return orderCompare; } // Then sort by package name String package1 = r1.activityInfo.packageName; String package2 = r2.activityInfo.packageName; return CASE_INSENSITIVE_ORDER.compare(package1, package2); }) .collect(Collectors.toList()); for (ResolveInfo resolved : results) { String key = null; String title = null; String summary = null; String category = null; ActivityInfo activityInfo = resolved.activityInfo; Bundle metaData = activityInfo.metaData; try { Resources res = mPm.getResourcesForApplication(activityInfo.packageName); if (metaData.containsKey(META_DATA_PREFERENCE_KEYHINT)) { key = extractMetaDataString(metaData, META_DATA_PREFERENCE_KEYHINT, res); } if (!metaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) { title = extractMetaDataString(metaData, META_DATA_PREFERENCE_TITLE, res); if (TextUtils.isEmpty(title)) { LOG.d("no title."); title = activityInfo.loadLabel(mPm).toString(); } } if (!metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { summary = extractMetaDataString(metaData, META_DATA_PREFERENCE_SUMMARY, res); if (TextUtils.isEmpty(summary)) { LOG.d("no description."); } } category = extractMetaDataString(metaData, META_DATA_PREFERENCE_CATEGORY, res); if (TextUtils.isEmpty(category)) { LOG.d("no category."); } } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { LOG.d("Couldn't find info", e); } Intent extraSettingIntent = new Intent().setClassName(activityInfo.packageName, activityInfo.name); if (category == null) { // If category is not specified or not supported, default to device. category = CATEGORY_DEVICE; } boolean isTopLevel = mTopLevelCategories.contains(category); metaData.putBoolean(META_DATA_PREFERENCE_IS_TOP_LEVEL, isTopLevel); Drawable icon = ExtraSettingsUtil.createIcon(mContext, metaData, activityInfo.packageName); if (!TextUtils.equals(extraCategory, category)) { continue; } CarUiPreference preference; if (isTopLevel) { preference = new TopLevelPreference(mContext); if (!mIsTopLevelSummariesEnabled) { // remove summary data summary = null; metaData.remove(META_DATA_PREFERENCE_SUMMARY_URI); } } else { preference = new CarUiPreference(mContext); } preference.setTitle(title); preference.setSummary(summary); if (key != null) { preference.setKey(key); } if (icon != null) { preference.setIcon(icon); } preference.setIntent(extraSettingIntent); mPreferenceBundleMap.put(preference, metaData); } return mPreferenceBundleMap; } /** * Extracts the value in the metadata specified by the key. * If it is resource, resolve the string and return. Otherwise, return the string itself. */ private String extractMetaDataString(Bundle metaData, String key, Resources res) { if (metaData.containsKey(key)) { if (metaData.get(key) instanceof Integer) { return res.getString(metaData.getInt(key)); } return metaData.getString(key); } return null; } }