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