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.Context; 31 import android.content.Intent; 32 import android.content.pm.ActivityInfo; 33 import android.content.pm.PackageManager; 34 import android.content.pm.ResolveInfo; 35 import android.content.res.Resources; 36 import android.graphics.drawable.Drawable; 37 import android.os.Bundle; 38 import android.text.TextUtils; 39 40 import androidx.annotation.VisibleForTesting; 41 import androidx.preference.Preference; 42 43 import com.android.car.settings.R; 44 import com.android.car.ui.preference.CarUiPreference; 45 import com.android.settingslib.drawer.TileUtils; 46 47 import java.util.LinkedHashMap; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Set; 51 import java.util.stream.Collectors; 52 53 /** 54 * Loads Activity with TileUtils.EXTRA_SETTINGS_ACTION. 55 */ 56 // TODO: investigate using SettingsLib Tiles. 57 public class ExtraSettingsLoader { 58 static final String META_DATA_PREFERENCE_IS_TOP_LEVEL = "injectedTopLevelPreference"; 59 private static final Logger LOG = new Logger(ExtraSettingsLoader.class); 60 private static final String META_DATA_PREFERENCE_CATEGORY = "com.android.settings.category"; 61 private final Context mContext; 62 private final Set<String> mTopLevelCategories; 63 private final boolean mIsTopLevelSummariesEnabled; 64 private Map<Preference, Bundle> mPreferenceBundleMap; 65 private PackageManager mPm; 66 ExtraSettingsLoader(Context context)67 public ExtraSettingsLoader(Context context) { 68 mContext = context; 69 mPm = context.getPackageManager(); 70 mPreferenceBundleMap = new LinkedHashMap<>(); 71 mTopLevelCategories = Set.of(mContext.getResources().getStringArray( 72 R.array.config_top_level_injection_categories)); 73 mIsTopLevelSummariesEnabled = mContext.getResources().getBoolean( 74 R.bool.config_top_level_injection_enable_summaries); 75 } 76 77 @VisibleForTesting setPackageManager(PackageManager pm)78 void setPackageManager(PackageManager pm) { 79 mPm = pm; 80 } 81 82 /** 83 * Returns a map of {@link Preference} and {@link Bundle} representing settings injected from 84 * system apps and their metadata. The given intent must specify the action to use for 85 * resolving activities and a category with the key "com.android.settings.category" and one of 86 * the values in {@link com.android.settingslib.drawer.CategoryKey}. 87 * 88 * {@link com.android.settingslib.drawer.TileUtils#EXTRA_SETTINGS_ACTION} is automatically added 89 * for backwards compatibility. Please make sure to use 90 * {@link com.android.settingslib.drawer.TileUtils#IA_SETTINGS_ACTION} instead. 91 * 92 * @param intent intent specifying the extra settings category to load 93 */ loadPreferences(Intent intent)94 public Map<Preference, Bundle> loadPreferences(Intent intent) { 95 intent.setAction(TileUtils.IA_SETTINGS_ACTION); 96 List<ResolveInfo> results = mPm.queryIntentActivitiesAsUser(intent, 97 PackageManager.GET_META_DATA, ActivityManager.getCurrentUser()); 98 99 intent.setAction(TileUtils.EXTRA_SETTINGS_ACTION); 100 List<ResolveInfo> extra_settings_results = mPm.queryIntentActivitiesAsUser(intent, 101 PackageManager.GET_META_DATA, ActivityManager.getCurrentUser()); 102 for (ResolveInfo extra_settings_resolveInfo : extra_settings_results) { 103 if (!results.contains(extra_settings_resolveInfo)) { 104 results.add(extra_settings_resolveInfo); 105 } 106 } 107 108 String extraCategory = intent.getStringExtra(META_DATA_PREFERENCE_CATEGORY); 109 110 // Filter to only include valid results and then sort the results 111 // Filter criteria: must be a system application and must have metaData 112 // Sort criteria: sort results based on [order, package within order] 113 results = results.stream() 114 .filter(r -> r.system && r.activityInfo != null && r.activityInfo.metaData != null) 115 .sorted((r1, r2) -> { 116 // First sort by order 117 int orderCompare = r2.activityInfo.metaData.getInt(META_DATA_KEY_ORDER) 118 - r1.activityInfo.metaData.getInt(META_DATA_KEY_ORDER); 119 if (orderCompare != 0) { 120 return orderCompare; 121 } 122 123 // Then sort by package name 124 String package1 = r1.activityInfo.packageName; 125 String package2 = r2.activityInfo.packageName; 126 return CASE_INSENSITIVE_ORDER.compare(package1, package2); 127 }) 128 .collect(Collectors.toList()); 129 130 for (ResolveInfo resolved : results) { 131 String key = null; 132 String title = null; 133 String summary = null; 134 String category = null; 135 ActivityInfo activityInfo = resolved.activityInfo; 136 Bundle metaData = activityInfo.metaData; 137 try { 138 Resources res = mPm.getResourcesForApplication(activityInfo.packageName); 139 if (metaData.containsKey(META_DATA_PREFERENCE_KEYHINT)) { 140 key = extractMetaDataString(metaData, META_DATA_PREFERENCE_KEYHINT, res); 141 } 142 if (!metaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) { 143 title = extractMetaDataString(metaData, META_DATA_PREFERENCE_TITLE, res); 144 if (TextUtils.isEmpty(title)) { 145 LOG.d("no title."); 146 title = activityInfo.loadLabel(mPm).toString(); 147 } 148 } 149 if (!metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { 150 summary = extractMetaDataString(metaData, META_DATA_PREFERENCE_SUMMARY, res); 151 if (TextUtils.isEmpty(summary)) { 152 LOG.d("no description."); 153 } 154 } 155 category = extractMetaDataString(metaData, META_DATA_PREFERENCE_CATEGORY, res); 156 if (TextUtils.isEmpty(category)) { 157 LOG.d("no category."); 158 } 159 } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { 160 LOG.d("Couldn't find info", e); 161 } 162 Intent extraSettingIntent = 163 new Intent().setClassName(activityInfo.packageName, activityInfo.name); 164 if (category == null) { 165 // If category is not specified or not supported, default to device. 166 category = CATEGORY_DEVICE; 167 } 168 boolean isTopLevel = mTopLevelCategories.contains(category); 169 metaData.putBoolean(META_DATA_PREFERENCE_IS_TOP_LEVEL, isTopLevel); 170 Drawable icon = ExtraSettingsUtil.createIcon(mContext, metaData, 171 activityInfo.packageName); 172 173 if (!TextUtils.equals(extraCategory, category)) { 174 continue; 175 } 176 CarUiPreference preference; 177 if (isTopLevel) { 178 preference = new TopLevelPreference(mContext); 179 if (!mIsTopLevelSummariesEnabled) { 180 // remove summary data 181 summary = null; 182 metaData.remove(META_DATA_PREFERENCE_SUMMARY_URI); 183 } 184 } else { 185 preference = new CarUiPreference(mContext); 186 } 187 preference.setTitle(title); 188 preference.setSummary(summary); 189 if (key != null) { 190 preference.setKey(key); 191 } 192 if (icon != null) { 193 preference.setIcon(icon); 194 } 195 preference.setIntent(extraSettingIntent); 196 mPreferenceBundleMap.put(preference, metaData); 197 } 198 return mPreferenceBundleMap; 199 } 200 201 /** 202 * Extracts the value in the metadata specified by the key. 203 * If it is resource, resolve the string and return. Otherwise, return the string itself. 204 */ extractMetaDataString(Bundle metaData, String key, Resources res)205 private String extractMetaDataString(Bundle metaData, String key, Resources res) { 206 if (metaData.containsKey(key)) { 207 if (metaData.get(key) instanceof Integer) { 208 return res.getString(metaData.getInt(key)); 209 } 210 return metaData.getString(key); 211 } 212 return null; 213 } 214 } 215