1 /*
2  * Copyright (C) 2019 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.developeroptions.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.car.developeroptions.R;
47 import com.android.car.developeroptions.Settings.TetherSettingsActivity;
48 import com.android.car.developeroptions.core.BasePreferenceController;
49 import com.android.car.developeroptions.overlay.FeatureFactory;
50 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
51 
52 import java.util.ArrayList;
53 import java.util.Collections;
54 import java.util.Comparator;
55 import java.util.List;
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.car.developeroptions.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         final ActivityInfo activityInfo = resolveInfo.activityInfo;
147 
148         final Icon maskableIcon;
149         if (activityInfo.icon != 0 && activityInfo.applicationInfo != null) {
150             maskableIcon = Icon.createWithAdaptiveBitmap(createIcon(
151                     activityInfo.applicationInfo, activityInfo.icon,
152                     R.layout.shortcut_badge_maskable,
153                     mContext.getResources().getDimensionPixelSize(R.dimen.shortcut_size_maskable)));
154         } else {
155             maskableIcon = Icon.createWithResource(mContext, R.drawable.ic_launcher_settings);
156         }
157         final String shortcutId = SHORTCUT_ID_PREFIX +
158                 shortcutIntent.getComponent().flattenToShortString();
159         ShortcutInfo info = new ShortcutInfo.Builder(mContext, shortcutId)
160                 .setShortLabel(label)
161                 .setIntent(shortcutIntent)
162                 .setIcon(maskableIcon)
163                 .build();
164         Intent intent = mShortcutManager.createShortcutResultIntent(info);
165         if (intent == null) {
166             intent = new Intent();
167         }
168         intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
169                 Intent.ShortcutIconResource.fromContext(mContext, R.mipmap.ic_launcher_settings))
170                 .putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent)
171                 .putExtra(Intent.EXTRA_SHORTCUT_NAME, label);
172 
173         if (activityInfo.icon != 0) {
174             intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon(
175                     activityInfo.applicationInfo,
176                     activityInfo.icon,
177                     R.layout.shortcut_badge,
178                     mContext.getResources().getDimensionPixelSize(R.dimen.shortcut_size)));
179         }
180         return intent;
181     }
182 
183     /**
184      * Finds all shortcut supported by Settings.
185      */
186     @VisibleForTesting
queryShortcuts()187     List<ResolveInfo> queryShortcuts() {
188         final List<ResolveInfo> shortcuts = new ArrayList<>();
189         final List<ResolveInfo> activities = mPackageManager.queryIntentActivities(SHORTCUT_PROBE,
190                 PackageManager.GET_META_DATA);
191 
192         if (activities == null) {
193             return null;
194         }
195         for (ResolveInfo info : activities) {
196             if (info.activityInfo.name.endsWith(TetherSettingsActivity.class.getSimpleName())) {
197                 if (!mConnectivityManager.isTetheringSupported()) {
198                     continue;
199                 }
200             }
201             if (!info.activityInfo.applicationInfo.isSystemApp()) {
202                 Log.d(TAG, "Skipping non-system app: " + info.activityInfo);
203                 continue;
204             }
205             shortcuts.add(info);
206         }
207         Collections.sort(shortcuts, SHORTCUT_COMPARATOR);
208         return shortcuts;
209     }
210 
logCreateShortcut(ResolveInfo info)211     private void logCreateShortcut(ResolveInfo info) {
212         if (info == null || info.activityInfo == null) {
213             return;
214         }
215         mMetricsFeatureProvider.action(
216                 mContext, SettingsEnums.ACTION_SETTINGS_CREATE_SHORTCUT,
217                 info.activityInfo.name);
218     }
219 
buildShortcutIntent(ResolveInfo info)220     private Intent buildShortcutIntent(ResolveInfo info) {
221         return new Intent(SHORTCUT_PROBE)
222                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP)
223                 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
224     }
225 
createIcon(ApplicationInfo app, int resource, int layoutRes, int size)226     private Bitmap createIcon(ApplicationInfo app, int resource, int layoutRes, int size) {
227         final Context context = new ContextThemeWrapper(mContext, android.R.style.Theme_Material);
228         final View view = LayoutInflater.from(context).inflate(layoutRes, null);
229         final int spec = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY);
230         view.measure(spec, spec);
231         final Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(),
232                 Bitmap.Config.ARGB_8888);
233         final Canvas canvas = new Canvas(bitmap);
234 
235         Drawable iconDrawable;
236         try {
237             iconDrawable = mPackageManager.getResourcesForApplication(app).getDrawable(resource);
238             if (iconDrawable instanceof LayerDrawable) {
239                 iconDrawable = ((LayerDrawable) iconDrawable).getDrawable(1);
240             }
241             ((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(iconDrawable);
242         } catch (PackageManager.NameNotFoundException e) {
243             Log.w(TAG, "Cannot load icon from app " + app + ", returning a default icon");
244             Icon icon = Icon.createWithResource(mContext, R.drawable.ic_launcher_settings);
245             ((ImageView) view.findViewById(android.R.id.icon)).setImageIcon(icon);
246         }
247 
248         view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
249         view.draw(canvas);
250         return bitmap;
251     }
252 
253     private static final Comparator<ResolveInfo> SHORTCUT_COMPARATOR =
254             new Comparator<ResolveInfo>() {
255 
256                 @Override
257                 public int compare(ResolveInfo i1, ResolveInfo i2) {
258                     return i1.priority - i2.priority;
259                 }
260             };
261 }
262