• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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