1/*
2 * Copyright (C) 2015 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 */
16package com.android.settingslib.drawer;
17
18import android.app.ActivityManager;
19import android.content.Context;
20import android.content.Intent;
21import android.content.pm.ActivityInfo;
22import android.content.pm.ApplicationInfo;
23import android.content.pm.PackageManager;
24import android.content.pm.ResolveInfo;
25import android.content.res.Resources;
26import android.graphics.drawable.Icon;
27import android.os.Bundle;
28import android.os.UserHandle;
29import android.os.UserManager;
30import android.text.TextUtils;
31import android.util.Log;
32import android.util.Pair;
33
34import java.util.ArrayList;
35import java.util.Collections;
36import java.util.Comparator;
37import java.util.HashMap;
38import java.util.List;
39import java.util.Map;
40
41public class TileUtils {
42
43    private static final boolean DEBUG = false;
44    private static final boolean DEBUG_TIMING = false;
45
46    private static final String LOG_TAG = "TileUtils";
47
48    /**
49     * Settings will search for system activities of this action and add them as a top level
50     * settings tile using the following parameters.
51     *
52     * <p>A category must be specified in the meta-data for the activity named
53     * {@link #EXTRA_CATEGORY_KEY}
54     *
55     * <p>The title may be defined by meta-data named {@link #META_DATA_PREFERENCE_TITLE}
56     * otherwise the label for the activity will be used.
57     *
58     * <p>The icon may be defined by meta-data named {@link #META_DATA_PREFERENCE_ICON}
59     * otherwise the icon for the activity will be used.
60     *
61     * <p>A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY}
62     */
63    private static final String EXTRA_SETTINGS_ACTION =
64            "com.android.settings.action.EXTRA_SETTINGS";
65
66    /**
67     * Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities.
68     */
69    private static final String SETTINGS_ACTION =
70            "com.android.settings.action.SETTINGS";
71
72    private static final String OPERATOR_SETTINGS =
73            "com.android.settings.OPERATOR_APPLICATION_SETTING";
74
75    private static final String OPERATOR_DEFAULT_CATEGORY =
76            "com.android.settings.category.wireless";
77
78    private static final String MANUFACTURER_SETTINGS =
79            "com.android.settings.MANUFACTURER_APPLICATION_SETTING";
80
81    private static final String MANUFACTURER_DEFAULT_CATEGORY =
82            "com.android.settings.category.device";
83
84    /**
85     * The key used to get the category from metadata of activities of action
86     * {@link #EXTRA_SETTINGS_ACTION}
87     * The value must be one of:
88     * <li>com.android.settings.category.wireless</li>
89     * <li>com.android.settings.category.device</li>
90     * <li>com.android.settings.category.personal</li>
91     * <li>com.android.settings.category.system</li>
92     */
93    private static final String EXTRA_CATEGORY_KEY = "com.android.settings.category";
94
95    /**
96     * Name of the meta-data item that should be set in the AndroidManifest.xml
97     * to specify the icon that should be displayed for the preference.
98     */
99    public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";
100
101    /**
102     * Name of the meta-data item that should be set in the AndroidManifest.xml
103     * to specify the title that should be displayed for the preference.
104     */
105    public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
106
107    /**
108     * Name of the meta-data item that should be set in the AndroidManifest.xml
109     * to specify the summary text that should be displayed for the preference.
110     */
111    public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
112
113    private static final String SETTING_PKG = "com.android.settings";
114
115    public static List<DashboardCategory> getCategories(Context context,
116            HashMap<Pair<String, String>, Tile> cache) {
117        final long startTime = System.currentTimeMillis();
118        ArrayList<Tile> tiles = new ArrayList<>();
119        UserManager userManager = UserManager.get(context);
120        for (UserHandle user : userManager.getUserProfiles()) {
121            // TODO: Needs much optimization, too many PM queries going on here.
122            if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
123                // Only add Settings for this user.
124                getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
125                getTilesForAction(context, user, OPERATOR_SETTINGS, cache,
126                        OPERATOR_DEFAULT_CATEGORY, tiles, false);
127                getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
128                        MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
129            }
130            getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
131        }
132        HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
133        for (Tile tile : tiles) {
134            DashboardCategory category = categoryMap.get(tile.category);
135            if (category == null) {
136                category = createCategory(context, tile.category);
137                if (category == null) {
138                    Log.w(LOG_TAG, "Couldn't find category " + tile.category);
139                    continue;
140                }
141                categoryMap.put(category.key, category);
142            }
143            category.addTile(tile);
144        }
145        ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
146        for (DashboardCategory category : categories) {
147            Collections.sort(category.tiles, TILE_COMPARATOR);
148        }
149        Collections.sort(categories, CATEGORY_COMPARATOR);
150        if (DEBUG_TIMING) Log.d(LOG_TAG, "getCategories took "
151                + (System.currentTimeMillis() - startTime) + " ms");
152        return categories;
153    }
154
155    private static DashboardCategory createCategory(Context context, String categoryKey) {
156        DashboardCategory category = new DashboardCategory();
157        category.key = categoryKey;
158        PackageManager pm = context.getPackageManager();
159        List<ResolveInfo> results = pm.queryIntentActivities(new Intent(categoryKey), 0);
160        if (results.size() == 0) {
161            return null;
162        }
163        for (ResolveInfo resolved : results) {
164            if (!resolved.system) {
165                // Do not allow any app to add to settings, only system ones.
166                continue;
167            }
168            category.title = resolved.activityInfo.loadLabel(pm);
169            category.priority = SETTING_PKG.equals(
170                    resolved.activityInfo.applicationInfo.packageName) ? resolved.priority : 0;
171            if (DEBUG) Log.d(LOG_TAG, "Adding category " + category.title);
172        }
173
174        return category;
175    }
176
177    private static void getTilesForAction(Context context,
178            UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
179            String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings) {
180        Intent intent = new Intent(action);
181        if (requireSettings) {
182            intent.setPackage(SETTING_PKG);
183        }
184        getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles,
185                requireSettings, true);
186    }
187
188    public static void getTilesForIntent(Context context, UserHandle user, Intent intent,
189            Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles,
190            boolean usePriority, boolean checkCategory) {
191        PackageManager pm = context.getPackageManager();
192        List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
193                PackageManager.GET_META_DATA, user.getIdentifier());
194        for (ResolveInfo resolved : results) {
195            if (!resolved.system) {
196                // Do not allow any app to add to settings, only system ones.
197                continue;
198            }
199            ActivityInfo activityInfo = resolved.activityInfo;
200            Bundle metaData = activityInfo.metaData;
201            String categoryKey = defaultCategory;
202            if (checkCategory && ((metaData == null) || !metaData.containsKey(EXTRA_CATEGORY_KEY))
203                    && categoryKey == null) {
204                Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent "
205                        + intent + " missing metadata "
206                        + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
207                continue;
208            } else {
209                categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
210            }
211            Pair<String, String> key = new Pair<String, String>(activityInfo.packageName,
212                    activityInfo.name);
213            Tile tile = addedCache.get(key);
214            if (tile == null) {
215                tile = new Tile();
216                tile.intent = new Intent().setClassName(
217                        activityInfo.packageName, activityInfo.name);
218                tile.category = categoryKey;
219                tile.priority = usePriority ? resolved.priority : 0;
220                tile.metaData = activityInfo.metaData;
221                updateTileData(context, tile, activityInfo, activityInfo.applicationInfo,
222                        pm);
223                if (DEBUG) Log.d(LOG_TAG, "Adding tile " + tile.title);
224
225                addedCache.put(key, tile);
226            }
227            if (!tile.userHandle.contains(user)) {
228                tile.userHandle.add(user);
229            }
230            if (!outTiles.contains(tile)) {
231                outTiles.add(tile);
232            }
233        }
234    }
235
236    private static DashboardCategory getCategory(List<DashboardCategory> target,
237            String categoryKey) {
238        for (DashboardCategory category : target) {
239            if (categoryKey.equals(category.key)) {
240                return category;
241            }
242        }
243        return null;
244    }
245
246    private static boolean updateTileData(Context context, Tile tile,
247            ActivityInfo activityInfo, ApplicationInfo applicationInfo, PackageManager pm) {
248        if (applicationInfo.isSystemApp()) {
249            int icon = 0;
250            CharSequence title = null;
251            String summary = null;
252
253            // Get the activity's meta-data
254            try {
255                Resources res = pm.getResourcesForApplication(
256                        applicationInfo.packageName);
257                Bundle metaData = activityInfo.metaData;
258
259                if (res != null && metaData != null) {
260                    if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) {
261                        icon = metaData.getInt(META_DATA_PREFERENCE_ICON);
262                    }
263                    if (metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
264                        if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
265                            title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
266                        } else {
267                            title = metaData.getString(META_DATA_PREFERENCE_TITLE);
268                        }
269                    }
270                    if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
271                        if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
272                            summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
273                        } else {
274                            summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY);
275                        }
276                    }
277                }
278            } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
279                if (DEBUG) Log.d(LOG_TAG, "Couldn't find info", e);
280            }
281
282            // Set the preference title to the activity's label if no
283            // meta-data is found
284            if (TextUtils.isEmpty(title)) {
285                title = activityInfo.loadLabel(pm).toString();
286            }
287            if (icon == 0) {
288                icon = activityInfo.icon;
289            }
290
291            // Set icon, title and summary for the preference
292            tile.icon = Icon.createWithResource(activityInfo.packageName, icon);
293            tile.title = title;
294            tile.summary = summary;
295            // Replace the intent with this specific activity
296            tile.intent = new Intent().setClassName(activityInfo.packageName,
297                    activityInfo.name);
298
299            return true;
300        }
301
302        return false;
303    }
304
305    private static final Comparator<Tile> TILE_COMPARATOR =
306            new Comparator<Tile>() {
307        @Override
308        public int compare(Tile lhs, Tile rhs) {
309            return rhs.priority - lhs.priority;
310        }
311    };
312
313    private static final Comparator<DashboardCategory> CATEGORY_COMPARATOR =
314            new Comparator<DashboardCategory>() {
315        @Override
316        public int compare(DashboardCategory lhs, DashboardCategory rhs) {
317            return rhs.priority - lhs.priority;
318        }
319    };
320}
321