1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.settings.common; 18 19 import static com.android.settingslib.drawer.CategoryKey.CATEGORY_DEVICE; 20 import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER; 21 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; 22 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; 23 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; 24 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; 25 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI; 26 27 import static java.lang.String.CASE_INSENSITIVE_ORDER; 28 29 import android.app.ActivityManager; 30 import android.content.ComponentName; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.pm.ActivityInfo; 34 import android.content.pm.PackageManager; 35 import android.content.pm.ResolveInfo; 36 import android.content.res.Resources; 37 import android.graphics.drawable.Drawable; 38 import android.os.Bundle; 39 import android.text.TextUtils; 40 41 import androidx.annotation.VisibleForTesting; 42 import androidx.preference.Preference; 43 44 import com.android.car.settings.R; 45 import com.android.car.settings.activityembedding.ActivityEmbeddingRulesController; 46 import com.android.car.ui.preference.CarUiPreference; 47 import com.android.settingslib.drawer.TileUtils; 48 49 import java.util.LinkedHashMap; 50 import java.util.List; 51 import java.util.Locale; 52 import java.util.Map; 53 import java.util.Set; 54 import java.util.stream.Collectors; 55 56 /** 57 * Loads Activity with TileUtils.EXTRA_SETTINGS_ACTION. 58 */ 59 // TODO: investigate using SettingsLib Tiles. 60 public class ExtraSettingsLoader { 61 static final String META_DATA_IS_TOP_LEVEL_EXTRA_SETTINGS = "injectedTopLevelPreference"; 62 private static final Logger LOG = new Logger(ExtraSettingsLoader.class); 63 private static final String META_DATA_PREFERENCE_CATEGORY = "com.android.settings.category"; 64 private final Context mContext; 65 private final Set<String> mTopLevelCategories; 66 private final boolean mIsTopLevelSummariesEnabled; 67 private Map<Preference, Bundle> mPreferenceBundleMap; 68 private PackageManager mPm; 69 ExtraSettingsLoader(Context context)70 public ExtraSettingsLoader(Context context) { 71 mContext = context; 72 mPm = context.getPackageManager(); 73 mPreferenceBundleMap = new LinkedHashMap<>(); 74 mTopLevelCategories = Set.of(mContext.getResources().getStringArray( 75 R.array.config_top_level_injection_categories)); 76 mIsTopLevelSummariesEnabled = mContext.getResources().getBoolean( 77 R.bool.config_top_level_injection_enable_summaries); 78 } 79 80 @VisibleForTesting setPackageManager(PackageManager pm)81 void setPackageManager(PackageManager pm) { 82 mPm = pm; 83 } 84 85 /** 86 * Returns a map of {@link Preference} and {@link Bundle} representing settings injected from 87 * system apps and their metadata. The given intent must specify the action to use for 88 * resolving activities and a category with the key "com.android.settings.category" and one of 89 * the values in {@link com.android.settingslib.drawer.CategoryKey}. 90 * 91 * {@link com.android.settingslib.drawer.TileUtils#EXTRA_SETTINGS_ACTION} is automatically added 92 * for backwards compatibility. Please make sure to use 93 * {@link com.android.settingslib.drawer.TileUtils#IA_SETTINGS_ACTION} instead. 94 * 95 * @param intent intent specifying the extra settings category to load 96 */ loadPreferences(Intent intent)97 public Map<Preference, Bundle> loadPreferences(Intent intent) { 98 intent.setAction(TileUtils.IA_SETTINGS_ACTION); 99 List<ResolveInfo> results = mPm.queryIntentActivitiesAsUser(intent, 100 PackageManager.GET_META_DATA, ActivityManager.getCurrentUser()); 101 102 intent.setAction(TileUtils.EXTRA_SETTINGS_ACTION); 103 List<ResolveInfo> extra_settings_results = mPm.queryIntentActivitiesAsUser(intent, 104 PackageManager.GET_META_DATA, ActivityManager.getCurrentUser()); 105 for (ResolveInfo extra_settings_resolveInfo : extra_settings_results) { 106 // queryIntentActivitiesAsUser returns shallow copies so we can't use .equals() 107 addResolveInfoIfUnique(results, extra_settings_resolveInfo); 108 } 109 110 String extraCategory = intent.getStringExtra(META_DATA_PREFERENCE_CATEGORY); 111 112 // Filter to only include valid results and then sort the results 113 // Filter criteria: must be a system application and must have metaData 114 // Sort criteria: sort results based on [order, package within order] 115 results = results.stream() 116 .filter(r -> r.system && r.activityInfo != null && r.activityInfo.metaData != null) 117 .sorted((r1, r2) -> { 118 // First sort by order 119 int orderCompare = r2.activityInfo.metaData.getInt(META_DATA_KEY_ORDER) 120 - r1.activityInfo.metaData.getInt(META_DATA_KEY_ORDER); 121 if (orderCompare != 0) { 122 return orderCompare; 123 } 124 125 // Then sort by package name 126 String package1 = r1.activityInfo.packageName; 127 String package2 = r2.activityInfo.packageName; 128 return CASE_INSENSITIVE_ORDER.compare(package1, package2); 129 }) 130 .collect(Collectors.toList()); 131 132 for (ResolveInfo resolved : results) { 133 String key = null; 134 String title = null; 135 String summary = null; 136 String category = null; 137 ActivityInfo activityInfo = resolved.activityInfo; 138 Bundle metaData = activityInfo.metaData; 139 try { 140 Resources res = mPm.getResourcesForApplication(activityInfo.packageName); 141 if (metaData.containsKey(META_DATA_PREFERENCE_KEYHINT)) { 142 key = extractMetaDataString(metaData, META_DATA_PREFERENCE_KEYHINT, res); 143 } 144 if (!metaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) { 145 title = extractMetaDataString(metaData, META_DATA_PREFERENCE_TITLE, res); 146 if (TextUtils.isEmpty(title)) { 147 LOG.d("no title."); 148 title = activityInfo.loadLabel(mPm).toString(); 149 } 150 } 151 if (!metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { 152 summary = extractMetaDataString(metaData, META_DATA_PREFERENCE_SUMMARY, res); 153 if (TextUtils.isEmpty(summary)) { 154 LOG.d("no description."); 155 } 156 } 157 category = extractMetaDataString(metaData, META_DATA_PREFERENCE_CATEGORY, res); 158 if (TextUtils.isEmpty(category)) { 159 LOG.d("no category."); 160 } 161 } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { 162 LOG.d("Couldn't find info", e); 163 } 164 Intent extraSettingIntent = 165 new Intent().setClassName(activityInfo.packageName, activityInfo.name); 166 if (category == null) { 167 // If category is not specified or not supported, default to device. 168 category = CATEGORY_DEVICE; 169 } 170 boolean isTopLevel = mTopLevelCategories.contains(category); 171 metaData.putBoolean(META_DATA_IS_TOP_LEVEL_EXTRA_SETTINGS, isTopLevel); 172 173 Drawable icon = ExtraSettingsUtil.createIcon(mContext, metaData, 174 activityInfo.packageName); 175 176 if (!TextUtils.equals(extraCategory, category)) { 177 continue; 178 } 179 CarUiPreference preference; 180 if (isTopLevel) { 181 preference = new TopLevelPreference(mContext); 182 if (!mIsTopLevelSummariesEnabled) { 183 // remove summary data 184 summary = null; 185 metaData.remove(META_DATA_PREFERENCE_SUMMARY_URI); 186 } 187 key = key != null ? key : getTopLevelPreferenceKey(title); 188 registerDualPaneSplitRule(extraSettingIntent, activityInfo.launchMode); 189 } else { 190 preference = new CarUiPreference(mContext); 191 } 192 preference.setTitle(title); 193 preference.setSummary(summary); 194 if (key != null) { 195 preference.setKey(key); 196 } 197 if (icon != null) { 198 preference.setIcon(icon); 199 } 200 preference.setIntent(extraSettingIntent); 201 mPreferenceBundleMap.put(preference, metaData); 202 } 203 return mPreferenceBundleMap; 204 } 205 getTopLevelPreferenceKey(CharSequence preferenceTitle)206 private String getTopLevelPreferenceKey(CharSequence preferenceTitle) { 207 return new String("top_level_extra_settings_preference_" + preferenceTitle + "_entry") 208 .replace(" ", "_") 209 .toLowerCase(Locale.ROOT); 210 } 211 212 /** 213 * Extracts the value in the metadata specified by the key. 214 * If it is resource, resolve the string and return. Otherwise, return the string itself. 215 */ extractMetaDataString(Bundle metaData, String key, Resources res)216 private String extractMetaDataString(Bundle metaData, String key, Resources res) { 217 if (metaData.containsKey(key)) { 218 if (metaData.get(key) instanceof Integer) { 219 return res.getString(metaData.getInt(key)); 220 } 221 return metaData.getString(key); 222 } 223 return null; 224 } 225 226 /** Adds new ResolveInfo to list if it is unique. */ addResolveInfoIfUnique(List<ResolveInfo> originalList, ResolveInfo newResolveInfo)227 private void addResolveInfoIfUnique(List<ResolveInfo> originalList, 228 ResolveInfo newResolveInfo) { 229 ComponentName componentName = newResolveInfo.activityInfo.getComponentName(); 230 boolean alreadyContains = originalList.stream().anyMatch(resolveInfo -> 231 componentName.equals(resolveInfo.activityInfo.getComponentName())); 232 233 if (!alreadyContains) { 234 originalList.add(newResolveInfo); 235 } 236 } 237 238 /** 239 * Registers {@link ActivityEmbeddingRulesController#registerHomepageDualPaneSplitRule} for all 240 * top level extra settings preference as these activities should all finish along with the 241 * {@link CarSettingActivities.HomepageActivity} on back press. 242 */ registerDualPaneSplitRule(Intent extraSettingIntent, int launchMode)243 private void registerDualPaneSplitRule(Intent extraSettingIntent, int launchMode) { 244 // Single instance activities may create new tasks if they already exist in the back of 245 // current task stack. Settings clearTop to true will bring those activities behind Settings 246 // rather than to the foreground. 247 boolean clearTop = (launchMode != ActivityInfo.LAUNCH_SINGLE_INSTANCE) 248 && (launchMode != ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK); 249 ActivityEmbeddingRulesController.registerHomepageDualPaneSplitRule(/* context= */ mContext, 250 /* secondaryComponent= */ extraSettingIntent.getComponent(), 251 /* secondaryIntentAction= */ extraSettingIntent.getAction(), 252 /* clearTop= */ clearTop); 253 } 254 } 255