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.settings.shortcut;
18 
19 import android.app.Activity;
20 import android.app.settings.SettingsEnums;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.ActivityInfo;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ResolveInfo;
27 import android.content.pm.ShortcutInfo;
28 import android.content.pm.ShortcutManager;
29 import android.graphics.Bitmap;
30 import android.graphics.Canvas;
31 import android.graphics.drawable.Drawable;
32 import android.graphics.drawable.Icon;
33 import android.graphics.drawable.LayerDrawable;
34 import android.net.ConnectivityManager;
35 import android.util.Log;
36 import android.view.ContextThemeWrapper;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.widget.ImageView;
40 
41 import com.android.settings.R;
42 import com.android.settings.Settings.TetherSettingsActivity;
43 import com.android.settings.core.BasePreferenceController;
44 import com.android.settings.overlay.FeatureFactory;
45 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
46 
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.Comparator;
50 import java.util.List;
51 
52 import androidx.annotation.VisibleForTesting;
53 import androidx.preference.Preference;
54 import androidx.preference.PreferenceCategory;
55 import androidx.preference.PreferenceGroup;
56 
57 /**
58  * {@link BasePreferenceController} that populates a list of widgets that Settings app support.
59  */
60 public class CreateShortcutPreferenceController extends BasePreferenceController {
61 
62     private static final String TAG = "CreateShortcutPrefCtrl";
63 
64     static final String SHORTCUT_ID_PREFIX = "component-shortcut-";
65     static final Intent SHORTCUT_PROBE = new Intent(Intent.ACTION_MAIN)
66             .addCategory("com.android.settings.SHORTCUT")
67             .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
68 
69     private final ShortcutManager mShortcutManager;
70     private final PackageManager mPackageManager;
71     private final ConnectivityManager mConnectivityManager;
72     private final MetricsFeatureProvider mMetricsFeatureProvider;
73     private Activity mHost;
74 
CreateShortcutPreferenceController(Context context, String preferenceKey)75     public CreateShortcutPreferenceController(Context context, String preferenceKey) {
76         super(context, preferenceKey);
77         mConnectivityManager =
78                 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
79         mShortcutManager = context.getSystemService(ShortcutManager.class);
80         mPackageManager = context.getPackageManager();
81         mMetricsFeatureProvider = FeatureFactory.getFactory(context)
82                 .getMetricsFeatureProvider();
83     }
84 
setActivity(Activity host)85     public void setActivity(Activity host) {
86         mHost = host;
87     }
88 
89     @Override
getAvailabilityStatus()90     public int getAvailabilityStatus() {
91         return AVAILABLE_UNSEARCHABLE;
92     }
93 
94     @Override
updateState(Preference preference)95     public void updateState(Preference preference) {
96         if (!(preference instanceof PreferenceGroup)) {
97             return;
98         }
99         final PreferenceGroup group = (PreferenceGroup) preference;
100         group.removeAll();
101         final List<ResolveInfo> shortcuts = queryShortcuts();
102         final Context uiContext = preference.getContext();
103         if (shortcuts.isEmpty()) {
104             return;
105         }
106         PreferenceCategory category = new PreferenceCategory(uiContext);
107         group.addPreference(category);
108         int bucket = 0;
109         for (ResolveInfo info : shortcuts) {
110             // Priority is not consecutive (aka, jumped), add a divider between prefs.
111             final int currentBucket = info.priority / 10;
112             boolean needDivider = currentBucket != bucket;
113             bucket = currentBucket;
114             if (needDivider) {
115                 // add a new Category
116                 category = new PreferenceCategory(uiContext);
117                 group.addPreference(category);
118             }
119 
120             final Preference pref = new Preference(uiContext);
121             pref.setTitle(info.loadLabel(mPackageManager));
122             pref.setKey(info.activityInfo.getComponentName().flattenToString());
123             pref.setOnPreferenceClickListener(clickTarget -> {
124                 if (mHost == null) {
125                     return false;
126                 }
127                 final Intent shortcutIntent = createResultIntent(
128                         buildShortcutIntent(info),
129                         info, clickTarget.getTitle());
130                 mHost.setResult(Activity.RESULT_OK, shortcutIntent);
131                 logCreateShortcut(info);
132                 mHost.finish();
133                 return true;
134             });
135             category.addPreference(pref);
136         }
137     }
138 
139     /**
140      * Create {@link Intent} that will be consumed by ShortcutManager, which later generates a
141      * launcher widget using this intent.
142      */
143     @VisibleForTesting
createResultIntent(Intent shortcutIntent, ResolveInfo resolveInfo, CharSequence label)144     Intent createResultIntent(Intent shortcutIntent, ResolveInfo resolveInfo,
145             CharSequence label) {
146         ShortcutInfo info = createShortcutInfo(mContext, shortcutIntent, resolveInfo, label);
147         Intent intent = mShortcutManager.createShortcutResultIntent(info);
148         if (intent == null) {
149             intent = new Intent();
150         }
151         intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
152                 Intent.ShortcutIconResource.fromContext(mContext, R.mipmap.ic_launcher_settings))
153                 .putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent)
154                 .putExtra(Intent.EXTRA_SHORTCUT_NAME, label);
155 
156         final ActivityInfo activityInfo = resolveInfo.activityInfo;
157         if (activityInfo.icon != 0) {
158             intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon(
159                     mContext,
160                     activityInfo.applicationInfo,
161                     activityInfo.icon,
162                     R.layout.shortcut_badge,
163                     mContext.getResources().getDimensionPixelSize(R.dimen.shortcut_size)));
164         }
165         return intent;
166     }
167 
168     /**
169      * Finds all shortcut supported by Settings.
170      */
171     @VisibleForTesting
queryShortcuts()172     List<ResolveInfo> queryShortcuts() {
173         final List<ResolveInfo> shortcuts = new ArrayList<>();
174         final List<ResolveInfo> activities = mPackageManager.queryIntentActivities(SHORTCUT_PROBE,
175                 PackageManager.GET_META_DATA);
176 
177         if (activities == null) {
178             return null;
179         }
180         for (ResolveInfo info : activities) {
181             if (info.activityInfo.name.endsWith(TetherSettingsActivity.class.getSimpleName())) {
182                 if (!mConnectivityManager.isTetheringSupported()) {
183                     continue;
184                 }
185             }
186             if (!info.activityInfo.applicationInfo.isSystemApp()) {
187                 Log.d(TAG, "Skipping non-system app: " + info.activityInfo);
188                 continue;
189             }
190             shortcuts.add(info);
191         }
192         Collections.sort(shortcuts, SHORTCUT_COMPARATOR);
193         return shortcuts;
194     }
195 
logCreateShortcut(ResolveInfo info)196     private void logCreateShortcut(ResolveInfo info) {
197         if (info == null || info.activityInfo == null) {
198             return;
199         }
200         mMetricsFeatureProvider.action(
201                 mContext, SettingsEnums.ACTION_SETTINGS_CREATE_SHORTCUT,
202                 info.activityInfo.name);
203     }
204 
buildShortcutIntent(ResolveInfo info)205     private static Intent buildShortcutIntent(ResolveInfo info) {
206         return new Intent(SHORTCUT_PROBE)
207                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP)
208                 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
209     }
210 
createShortcutInfo(Context context, Intent shortcutIntent, ResolveInfo resolveInfo, CharSequence label)211     private static ShortcutInfo createShortcutInfo(Context context, Intent shortcutIntent,
212             ResolveInfo resolveInfo, CharSequence label) {
213         final ActivityInfo activityInfo = resolveInfo.activityInfo;
214 
215         final Icon maskableIcon;
216         if (activityInfo.icon != 0 && activityInfo.applicationInfo != null) {
217             maskableIcon = Icon.createWithAdaptiveBitmap(createIcon(
218                     context,
219                     activityInfo.applicationInfo, activityInfo.icon,
220                     R.layout.shortcut_badge_maskable,
221                     context.getResources().getDimensionPixelSize(R.dimen.shortcut_size_maskable)));
222         } else {
223             maskableIcon = Icon.createWithResource(context, R.drawable.ic_launcher_settings);
224         }
225         final String shortcutId = SHORTCUT_ID_PREFIX +
226                 shortcutIntent.getComponent().flattenToShortString();
227         return new ShortcutInfo.Builder(context, shortcutId)
228                 .setShortLabel(label)
229                 .setIntent(shortcutIntent)
230                 .setIcon(maskableIcon)
231                 .build();
232     }
233 
createIcon(Context context, ApplicationInfo app, int resource, int layoutRes, int size)234     private static Bitmap createIcon(Context context, ApplicationInfo app, int resource,
235             int layoutRes, int size) {
236         final Context themedContext = new ContextThemeWrapper(context,
237                 android.R.style.Theme_Material);
238         final View view = LayoutInflater.from(themedContext).inflate(layoutRes, null);
239         final int spec = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY);
240         view.measure(spec, spec);
241         final Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(),
242                 Bitmap.Config.ARGB_8888);
243         final Canvas canvas = new Canvas(bitmap);
244 
245         Drawable iconDrawable;
246         try {
247             iconDrawable = context.getPackageManager().getResourcesForApplication(app)
248                     .getDrawable(resource);
249             if (iconDrawable instanceof LayerDrawable) {
250                 iconDrawable = ((LayerDrawable) iconDrawable).getDrawable(1);
251             }
252             ((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(iconDrawable);
253         } catch (PackageManager.NameNotFoundException e) {
254             Log.w(TAG, "Cannot load icon from app " + app + ", returning a default icon");
255             Icon icon = Icon.createWithResource(context, R.drawable.ic_launcher_settings);
256             ((ImageView) view.findViewById(android.R.id.icon)).setImageIcon(icon);
257         }
258 
259         view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
260         view.draw(canvas);
261         return bitmap;
262     }
263 
updateRestoredShortcuts(Context context)264     public static void updateRestoredShortcuts(Context context) {
265         ShortcutManager sm = context.getSystemService(ShortcutManager.class);
266         List<ShortcutInfo> updatedShortcuts = new ArrayList<>();
267         for (ShortcutInfo si : sm.getPinnedShortcuts()) {
268             if (si.getId().startsWith(SHORTCUT_ID_PREFIX)) {
269                 ResolveInfo ri = context.getPackageManager().resolveActivity(si.getIntent(), 0);
270 
271                 if (ri != null) {
272                     updatedShortcuts.add(createShortcutInfo(context, buildShortcutIntent(ri), ri,
273                             si.getShortLabel()));
274                 }
275             }
276         }
277         if (!updatedShortcuts.isEmpty()) {
278             sm.updateShortcuts(updatedShortcuts);
279         }
280     }
281 
282     private static final Comparator<ResolveInfo> SHORTCUT_COMPARATOR =
283             (i1, i2) -> i1.priority - i2.priority;
284 }
285