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