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