1 /* 2 * Copyright (C) 2008 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.launcher3; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.SharedPreferences; 23 import android.content.pm.ActivityInfo; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapFactory; 28 import android.text.TextUtils; 29 import android.util.Base64; 30 import android.util.Log; 31 32 import com.android.launcher3.compat.LauncherActivityInfoCompat; 33 import com.android.launcher3.compat.LauncherAppsCompat; 34 import com.android.launcher3.compat.UserHandleCompat; 35 import com.android.launcher3.compat.UserManagerCompat; 36 import com.android.launcher3.util.PackageManagerHelper; 37 import com.android.launcher3.util.Thunk; 38 39 import org.json.JSONException; 40 import org.json.JSONObject; 41 import org.json.JSONStringer; 42 import org.json.JSONTokener; 43 44 import java.net.URISyntaxException; 45 import java.util.ArrayList; 46 import java.util.HashSet; 47 import java.util.Iterator; 48 import java.util.Set; 49 50 public class InstallShortcutReceiver extends BroadcastReceiver { 51 private static final String TAG = "InstallShortcutReceiver"; 52 private static final boolean DBG = false; 53 54 private static final String ACTION_INSTALL_SHORTCUT = 55 "com.android.launcher.action.INSTALL_SHORTCUT"; 56 57 private static final String LAUNCH_INTENT_KEY = "intent.launch"; 58 private static final String NAME_KEY = "name"; 59 private static final String ICON_KEY = "icon"; 60 private static final String ICON_RESOURCE_NAME_KEY = "iconResource"; 61 private static final String ICON_RESOURCE_PACKAGE_NAME_KEY = "iconResourcePackage"; 62 63 private static final String APP_SHORTCUT_TYPE_KEY = "isAppShortcut"; 64 private static final String USER_HANDLE_KEY = "userHandle"; 65 66 // The set of shortcuts that are pending install 67 private static final String APPS_PENDING_INSTALL = "apps_to_install"; 68 69 public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450; 70 public static final int NEW_SHORTCUT_STAGGER_DELAY = 85; 71 72 private static final Object sLock = new Object(); 73 addToInstallQueue( SharedPreferences sharedPrefs, PendingInstallShortcutInfo info)74 private static void addToInstallQueue( 75 SharedPreferences sharedPrefs, PendingInstallShortcutInfo info) { 76 synchronized(sLock) { 77 String encoded = info.encodeToString(); 78 if (encoded != null) { 79 Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null); 80 if (strings == null) { 81 strings = new HashSet<String>(1); 82 } else { 83 strings = new HashSet<String>(strings); 84 } 85 strings.add(encoded); 86 sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, strings).apply(); 87 } 88 } 89 } 90 removeFromInstallQueue(Context context, HashSet<String> packageNames, UserHandleCompat user)91 public static void removeFromInstallQueue(Context context, HashSet<String> packageNames, 92 UserHandleCompat user) { 93 if (packageNames.isEmpty()) { 94 return; 95 } 96 SharedPreferences sp = Utilities.getPrefs(context); 97 synchronized(sLock) { 98 Set<String> strings = sp.getStringSet(APPS_PENDING_INSTALL, null); 99 if (DBG) { 100 Log.d(TAG, "APPS_PENDING_INSTALL: " + strings 101 + ", removing packages: " + packageNames); 102 } 103 if (strings != null) { 104 Set<String> newStrings = new HashSet<String>(strings); 105 Iterator<String> newStringsIter = newStrings.iterator(); 106 while (newStringsIter.hasNext()) { 107 String encoded = newStringsIter.next(); 108 PendingInstallShortcutInfo info = decode(encoded, context); 109 if (info == null || (packageNames.contains(info.getTargetPackage()) 110 && user.equals(info.user))) { 111 newStringsIter.remove(); 112 } 113 } 114 sp.edit().putStringSet(APPS_PENDING_INSTALL, newStrings).apply(); 115 } 116 } 117 } 118 getAndClearInstallQueue( SharedPreferences sharedPrefs, Context context)119 private static ArrayList<PendingInstallShortcutInfo> getAndClearInstallQueue( 120 SharedPreferences sharedPrefs, Context context) { 121 synchronized(sLock) { 122 Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null); 123 if (DBG) Log.d(TAG, "Getting and clearing APPS_PENDING_INSTALL: " + strings); 124 if (strings == null) { 125 return new ArrayList<PendingInstallShortcutInfo>(); 126 } 127 ArrayList<PendingInstallShortcutInfo> infos = 128 new ArrayList<PendingInstallShortcutInfo>(); 129 for (String encoded : strings) { 130 PendingInstallShortcutInfo info = decode(encoded, context); 131 if (info != null) { 132 infos.add(info); 133 } 134 } 135 sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, new HashSet<String>()).apply(); 136 return infos; 137 } 138 } 139 140 // Determines whether to defer installing shortcuts immediately until 141 // processAllPendingInstalls() is called. 142 private static boolean mUseInstallQueue = false; 143 onReceive(Context context, Intent data)144 public void onReceive(Context context, Intent data) { 145 if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) { 146 return; 147 } 148 PendingInstallShortcutInfo info = createPendingInfo(context, data); 149 if (info != null) { 150 if (!info.isLauncherActivity()) { 151 // Since its a custom shortcut, verify that it is safe to launch. 152 if (!PackageManagerHelper.hasPermissionForActivity( 153 context, info.launchIntent, null)) { 154 // Target cannot be launched, or requires some special permission to launch 155 Log.e(TAG, "Ignoring malicious intent " + info.launchIntent.toUri(0)); 156 return; 157 } 158 } 159 queuePendingShortcutInfo(info, context); 160 } 161 } 162 163 /** 164 * @return true is the extra is either null or is of type {@param type} 165 */ isValidExtraType(Intent intent, String key, Class type)166 private static boolean isValidExtraType(Intent intent, String key, Class type) { 167 Object extra = intent.getParcelableExtra(key); 168 return extra == null || type.isInstance(extra); 169 } 170 171 /** 172 * Verifies the intent and creates a {@link PendingInstallShortcutInfo} 173 */ createPendingInfo(Context context, Intent data)174 private static PendingInstallShortcutInfo createPendingInfo(Context context, Intent data) { 175 if (!isValidExtraType(data, Intent.EXTRA_SHORTCUT_INTENT, Intent.class) || 176 !(isValidExtraType(data, Intent.EXTRA_SHORTCUT_ICON_RESOURCE, 177 Intent.ShortcutIconResource.class)) || 178 !(isValidExtraType(data, Intent.EXTRA_SHORTCUT_ICON, Bitmap.class))) { 179 180 if (DBG) Log.e(TAG, "Invalid install shortcut intent"); 181 return null; 182 } 183 184 PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); 185 if (info.launchIntent == null || info.label == null) { 186 if (DBG) Log.e(TAG, "Invalid install shortcut intent"); 187 return null; 188 } 189 190 return convertToLauncherActivityIfPossible(info); 191 } 192 fromShortcutIntent(Context context, Intent data)193 public static ShortcutInfo fromShortcutIntent(Context context, Intent data) { 194 PendingInstallShortcutInfo info = createPendingInfo(context, data); 195 return info == null ? null : info.getShortcutInfo(); 196 } 197 queuePendingShortcutInfo(PendingInstallShortcutInfo info, Context context)198 private static void queuePendingShortcutInfo(PendingInstallShortcutInfo info, Context context) { 199 // Queue the item up for adding if launcher has not loaded properly yet 200 LauncherAppState app = LauncherAppState.getInstance(); 201 boolean launcherNotLoaded = app.getModel().getCallback() == null; 202 203 addToInstallQueue(Utilities.getPrefs(context), info); 204 if (!mUseInstallQueue && !launcherNotLoaded) { 205 flushInstallQueue(context); 206 } 207 } 208 enableInstallQueue()209 static void enableInstallQueue() { 210 mUseInstallQueue = true; 211 } disableAndFlushInstallQueue(Context context)212 static void disableAndFlushInstallQueue(Context context) { 213 mUseInstallQueue = false; 214 flushInstallQueue(context); 215 } flushInstallQueue(Context context)216 static void flushInstallQueue(Context context) { 217 SharedPreferences sp = Utilities.getPrefs(context); 218 ArrayList<PendingInstallShortcutInfo> installQueue = getAndClearInstallQueue(sp, context); 219 if (!installQueue.isEmpty()) { 220 Iterator<PendingInstallShortcutInfo> iter = installQueue.iterator(); 221 ArrayList<ItemInfo> addShortcuts = new ArrayList<ItemInfo>(); 222 while (iter.hasNext()) { 223 final PendingInstallShortcutInfo pendingInfo = iter.next(); 224 225 // If the intent specifies a package, make sure the package exists 226 String packageName = pendingInfo.getTargetPackage(); 227 if (!TextUtils.isEmpty(packageName)) { 228 UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle(); 229 if (!LauncherModel.isValidPackage(context, packageName, myUserHandle)) { 230 if (DBG) Log.d(TAG, "Ignoring shortcut for absent package: " 231 + pendingInfo.launchIntent); 232 continue; 233 } 234 } 235 236 // Generate a shortcut info to add into the model 237 addShortcuts.add(pendingInfo.getShortcutInfo()); 238 } 239 240 // Add the new apps to the model and bind them 241 if (!addShortcuts.isEmpty()) { 242 LauncherAppState app = LauncherAppState.getInstance(); 243 app.getModel().addAndBindAddedWorkspaceItems(context, addShortcuts); 244 } 245 } 246 } 247 248 /** 249 * Ensures that we have a valid, non-null name. If the provided name is null, we will return 250 * the application name instead. 251 */ ensureValidName(Context context, Intent intent, CharSequence name)252 @Thunk static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) { 253 if (name == null) { 254 try { 255 PackageManager pm = context.getPackageManager(); 256 ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0); 257 name = info.loadLabel(pm); 258 } catch (PackageManager.NameNotFoundException nnfe) { 259 return ""; 260 } 261 } 262 return name; 263 } 264 265 private static class PendingInstallShortcutInfo { 266 267 final LauncherActivityInfoCompat activityInfo; 268 269 final Intent data; 270 final Context mContext; 271 final Intent launchIntent; 272 final String label; 273 final UserHandleCompat user; 274 275 /** 276 * Initializes a PendingInstallShortcutInfo received from a different app. 277 */ PendingInstallShortcutInfo(Intent data, Context context)278 public PendingInstallShortcutInfo(Intent data, Context context) { 279 this.data = data; 280 mContext = context; 281 282 launchIntent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); 283 label = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); 284 user = UserHandleCompat.myUserHandle(); 285 activityInfo = null; 286 } 287 288 /** 289 * Initializes a PendingInstallShortcutInfo to represent a launcher target. 290 */ PendingInstallShortcutInfo(LauncherActivityInfoCompat info, Context context)291 public PendingInstallShortcutInfo(LauncherActivityInfoCompat info, Context context) { 292 this.data = null; 293 mContext = context; 294 activityInfo = info; 295 user = info.getUser(); 296 297 launchIntent = AppInfo.makeLaunchIntent(context, info, user); 298 label = info.getLabel().toString(); 299 } 300 encodeToString()301 public String encodeToString() { 302 if (activityInfo != null) { 303 try { 304 // If it a launcher target, we only need component name, and user to 305 // recreate this. 306 return new JSONStringer() 307 .object() 308 .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0)) 309 .key(APP_SHORTCUT_TYPE_KEY).value(true) 310 .key(USER_HANDLE_KEY).value(UserManagerCompat.getInstance(mContext) 311 .getSerialNumberForUser(user)) 312 .endObject().toString(); 313 } catch (JSONException e) { 314 Log.d(TAG, "Exception when adding shortcut: " + e); 315 return null; 316 } 317 } 318 319 if (launchIntent.getAction() == null) { 320 launchIntent.setAction(Intent.ACTION_VIEW); 321 } else if (launchIntent.getAction().equals(Intent.ACTION_MAIN) && 322 launchIntent.getCategories() != null && 323 launchIntent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { 324 launchIntent.addFlags( 325 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 326 } 327 328 // This name is only used for comparisons and notifications, so fall back to activity 329 // name if not supplied 330 String name = ensureValidName(mContext, launchIntent, label).toString(); 331 Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON); 332 Intent.ShortcutIconResource iconResource = 333 data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE); 334 335 // Only encode the parameters which are supported by the API. 336 try { 337 JSONStringer json = new JSONStringer() 338 .object() 339 .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0)) 340 .key(NAME_KEY).value(name); 341 if (icon != null) { 342 byte[] iconByteArray = Utilities.flattenBitmap(icon); 343 json = json.key(ICON_KEY).value( 344 Base64.encodeToString( 345 iconByteArray, 0, iconByteArray.length, Base64.DEFAULT)); 346 } 347 if (iconResource != null) { 348 json = json.key(ICON_RESOURCE_NAME_KEY).value(iconResource.resourceName); 349 json = json.key(ICON_RESOURCE_PACKAGE_NAME_KEY) 350 .value(iconResource.packageName); 351 } 352 return json.endObject().toString(); 353 } catch (JSONException e) { 354 Log.d(TAG, "Exception when adding shortcut: " + e); 355 } 356 return null; 357 } 358 getShortcutInfo()359 public ShortcutInfo getShortcutInfo() { 360 if (activityInfo != null) { 361 return ShortcutInfo.fromActivityInfo(activityInfo, mContext); 362 } else { 363 return LauncherAppState.getInstance().getModel().infoFromShortcutIntent(mContext, data); 364 } 365 } 366 getTargetPackage()367 public String getTargetPackage() { 368 String packageName = launchIntent.getPackage(); 369 if (packageName == null) { 370 packageName = launchIntent.getComponent() == null ? null : 371 launchIntent.getComponent().getPackageName(); 372 } 373 return packageName; 374 } 375 isLauncherActivity()376 public boolean isLauncherActivity() { 377 return activityInfo != null; 378 } 379 } 380 decode(String encoded, Context context)381 private static PendingInstallShortcutInfo decode(String encoded, Context context) { 382 try { 383 JSONObject object = (JSONObject) new JSONTokener(encoded).nextValue(); 384 Intent launcherIntent = Intent.parseUri(object.getString(LAUNCH_INTENT_KEY), 0); 385 386 if (object.optBoolean(APP_SHORTCUT_TYPE_KEY)) { 387 // The is an internal launcher target shortcut. 388 UserHandleCompat user = UserManagerCompat.getInstance(context) 389 .getUserForSerialNumber(object.getLong(USER_HANDLE_KEY)); 390 if (user == null) { 391 return null; 392 } 393 394 LauncherActivityInfoCompat info = LauncherAppsCompat.getInstance(context) 395 .resolveActivity(launcherIntent, user); 396 return info == null ? null : new PendingInstallShortcutInfo(info, context); 397 } 398 399 Intent data = new Intent(); 400 data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launcherIntent); 401 data.putExtra(Intent.EXTRA_SHORTCUT_NAME, object.getString(NAME_KEY)); 402 403 String iconBase64 = object.optString(ICON_KEY); 404 String iconResourceName = object.optString(ICON_RESOURCE_NAME_KEY); 405 String iconResourcePackageName = object.optString(ICON_RESOURCE_PACKAGE_NAME_KEY); 406 if (iconBase64 != null && !iconBase64.isEmpty()) { 407 byte[] iconArray = Base64.decode(iconBase64, Base64.DEFAULT); 408 Bitmap b = BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length); 409 data.putExtra(Intent.EXTRA_SHORTCUT_ICON, b); 410 } else if (iconResourceName != null && !iconResourceName.isEmpty()) { 411 Intent.ShortcutIconResource iconResource = 412 new Intent.ShortcutIconResource(); 413 iconResource.resourceName = iconResourceName; 414 iconResource.packageName = iconResourcePackageName; 415 data.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); 416 } 417 418 return new PendingInstallShortcutInfo(data, context); 419 } catch (JSONException | URISyntaxException e) { 420 Log.d(TAG, "Exception reading shortcut to add: " + e); 421 } 422 return null; 423 } 424 425 /** 426 * Tries to create a new PendingInstallShortcutInfo which represents the same target, 427 * but is an app target and not a shortcut. 428 * @return the newly created info or the original one. 429 */ convertToLauncherActivityIfPossible( PendingInstallShortcutInfo original)430 private static PendingInstallShortcutInfo convertToLauncherActivityIfPossible( 431 PendingInstallShortcutInfo original) { 432 if (original.isLauncherActivity()) { 433 // Already an activity target 434 return original; 435 } 436 if (!Utilities.isLauncherAppTarget(original.launchIntent) 437 || !original.user.equals(UserHandleCompat.myUserHandle())) { 438 // We can only convert shortcuts which point to a main activity in the current user. 439 return original; 440 } 441 442 PackageManager pm = original.mContext.getPackageManager(); 443 ResolveInfo info = pm.resolveActivity(original.launchIntent, 0); 444 445 if (info == null) { 446 return original; 447 } 448 449 // Ignore any conflicts in the label name, as that can change based on locale. 450 LauncherActivityInfoCompat launcherInfo = LauncherActivityInfoCompat 451 .fromResolveInfo(info, original.mContext); 452 return new PendingInstallShortcutInfo(launcherInfo, original.mContext); 453 } 454 } 455