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