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