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