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