• 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.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