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