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 17package com.android.launcher3; 18 19import android.content.BroadcastReceiver; 20import android.content.Context; 21import android.content.Intent; 22import android.content.SharedPreferences; 23import android.content.pm.ActivityInfo; 24import android.content.pm.PackageManager; 25import android.content.pm.ResolveInfo; 26import android.graphics.Bitmap; 27import android.graphics.BitmapFactory; 28import android.text.TextUtils; 29import android.util.Base64; 30import android.util.Log; 31 32import com.android.launcher3.compat.LauncherActivityInfoCompat; 33import com.android.launcher3.compat.LauncherAppsCompat; 34import com.android.launcher3.compat.UserHandleCompat; 35import com.android.launcher3.compat.UserManagerCompat; 36import com.android.launcher3.util.Thunk; 37 38import org.json.JSONException; 39import org.json.JSONObject; 40import org.json.JSONStringer; 41import org.json.JSONTokener; 42 43import java.net.URISyntaxException; 44import java.util.ArrayList; 45import java.util.HashSet; 46import java.util.Iterator; 47import java.util.Set; 48 49public 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).commit(); 86 } 87 } 88 } 89 90 public static void removeFromInstallQueue(Context context, ArrayList<String> packageNames, 91 UserHandleCompat user) { 92 if (packageNames.isEmpty()) { 93 return; 94 } 95 String spKey = LauncherAppState.getSharedPreferencesKey(); 96 SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); 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).commit(); 115 } 116 } 117 } 118 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>()).commit(); 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 144 public void onReceive(Context context, Intent data) { 145 if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) { 146 return; 147 } 148 149 PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); 150 if (info.launchIntent == null || info.label == null) { 151 if (DBG) Log.e(TAG, "Invalid install shortcut intent"); 152 return; 153 } 154 155 info = convertToLauncherActivityIfPossible(info); 156 queuePendingShortcutInfo(info, context); 157 } 158 159 public static ShortcutInfo fromShortcutIntent(Context context, Intent data) { 160 PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); 161 if (info.launchIntent == null || info.label == null) { 162 if (DBG) Log.e(TAG, "Invalid install shortcut intent"); 163 return null; 164 } 165 info = convertToLauncherActivityIfPossible(info); 166 return info.getShortcutInfo(); 167 } 168 169 static void queueInstallShortcut(LauncherActivityInfoCompat info, Context context) { 170 queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, context), context); 171 } 172 173 private static void queuePendingShortcutInfo(PendingInstallShortcutInfo info, Context context) { 174 // Queue the item up for adding if launcher has not loaded properly yet 175 LauncherAppState.setApplicationContext(context.getApplicationContext()); 176 LauncherAppState app = LauncherAppState.getInstance(); 177 boolean launcherNotLoaded = app.getModel().getCallback() == null; 178 179 String spKey = LauncherAppState.getSharedPreferencesKey(); 180 SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); 181 addToInstallQueue(sp, info); 182 if (!mUseInstallQueue && !launcherNotLoaded) { 183 flushInstallQueue(context); 184 } 185 } 186 187 static void enableInstallQueue() { 188 mUseInstallQueue = true; 189 } 190 static void disableAndFlushInstallQueue(Context context) { 191 mUseInstallQueue = false; 192 flushInstallQueue(context); 193 } 194 static void flushInstallQueue(Context context) { 195 String spKey = LauncherAppState.getSharedPreferencesKey(); 196 SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); 197 ArrayList<PendingInstallShortcutInfo> installQueue = getAndClearInstallQueue(sp, context); 198 if (!installQueue.isEmpty()) { 199 Iterator<PendingInstallShortcutInfo> iter = installQueue.iterator(); 200 ArrayList<ItemInfo> addShortcuts = new ArrayList<ItemInfo>(); 201 while (iter.hasNext()) { 202 final PendingInstallShortcutInfo pendingInfo = iter.next(); 203 final Intent intent = pendingInfo.launchIntent; 204 205 // If the intent specifies a package, make sure the package exists 206 String packageName = pendingInfo.getTargetPackage(); 207 if (!TextUtils.isEmpty(packageName)) { 208 UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle(); 209 if (!LauncherModel.isValidPackage(context, packageName, myUserHandle)) { 210 if (DBG) Log.d(TAG, "Ignoring shortcut for absent package:" + intent); 211 continue; 212 } 213 } 214 215 // Generate a shortcut info to add into the model 216 addShortcuts.add(pendingInfo.getShortcutInfo()); 217 } 218 219 // Add the new apps to the model and bind them 220 if (!addShortcuts.isEmpty()) { 221 LauncherAppState app = LauncherAppState.getInstance(); 222 app.getModel().addAndBindAddedWorkspaceItems(context, addShortcuts); 223 } 224 } 225 } 226 227 /** 228 * Ensures that we have a valid, non-null name. If the provided name is null, we will return 229 * the application name instead. 230 */ 231 @Thunk static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) { 232 if (name == null) { 233 try { 234 PackageManager pm = context.getPackageManager(); 235 ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0); 236 name = info.loadLabel(pm); 237 } catch (PackageManager.NameNotFoundException nnfe) { 238 return ""; 239 } 240 } 241 return name; 242 } 243 244 private static class PendingInstallShortcutInfo { 245 246 final LauncherActivityInfoCompat activityInfo; 247 248 final Intent data; 249 final Context mContext; 250 final Intent launchIntent; 251 final String label; 252 final UserHandleCompat user; 253 254 /** 255 * Initializes a PendingInstallShortcutInfo received from a different app. 256 */ 257 public PendingInstallShortcutInfo(Intent data, Context context) { 258 this.data = data; 259 mContext = context; 260 261 launchIntent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); 262 label = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); 263 user = UserHandleCompat.myUserHandle(); 264 activityInfo = null; 265 } 266 267 /** 268 * Initializes a PendingInstallShortcutInfo to represent a launcher target. 269 */ 270 public PendingInstallShortcutInfo(LauncherActivityInfoCompat info, Context context) { 271 this.data = null; 272 mContext = context; 273 activityInfo = info; 274 user = info.getUser(); 275 276 launchIntent = AppInfo.makeLaunchIntent(context, info, user); 277 label = info.getLabel().toString(); 278 } 279 280 public String encodeToString() { 281 if (activityInfo != null) { 282 try { 283 // If it a launcher target, we only need component name, and user to 284 // recreate this. 285 return new JSONStringer() 286 .object() 287 .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0)) 288 .key(APP_SHORTCUT_TYPE_KEY).value(true) 289 .key(USER_HANDLE_KEY).value(UserManagerCompat.getInstance(mContext) 290 .getSerialNumberForUser(user)) 291 .endObject().toString(); 292 } catch (JSONException e) { 293 Log.d(TAG, "Exception when adding shortcut: " + e); 294 return null; 295 } 296 } 297 298 if (launchIntent.getAction() == null) { 299 launchIntent.setAction(Intent.ACTION_VIEW); 300 } else if (launchIntent.getAction().equals(Intent.ACTION_MAIN) && 301 launchIntent.getCategories() != null && 302 launchIntent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { 303 launchIntent.addFlags( 304 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 305 } 306 307 // This name is only used for comparisons and notifications, so fall back to activity 308 // name if not supplied 309 String name = ensureValidName(mContext, launchIntent, label).toString(); 310 Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON); 311 Intent.ShortcutIconResource iconResource = 312 data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE); 313 314 // Only encode the parameters which are supported by the API. 315 try { 316 JSONStringer json = new JSONStringer() 317 .object() 318 .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0)) 319 .key(NAME_KEY).value(name); 320 if (icon != null) { 321 byte[] iconByteArray = Utilities.flattenBitmap(icon); 322 json = json.key(ICON_KEY).value( 323 Base64.encodeToString( 324 iconByteArray, 0, iconByteArray.length, Base64.DEFAULT)); 325 } 326 if (iconResource != null) { 327 json = json.key(ICON_RESOURCE_NAME_KEY).value(iconResource.resourceName); 328 json = json.key(ICON_RESOURCE_PACKAGE_NAME_KEY) 329 .value(iconResource.packageName); 330 } 331 return json.endObject().toString(); 332 } catch (JSONException e) { 333 Log.d(TAG, "Exception when adding shortcut: " + e); 334 } 335 return null; 336 } 337 338 public ShortcutInfo getShortcutInfo() { 339 if (activityInfo != null) { 340 return ShortcutInfo.fromActivityInfo(activityInfo, mContext); 341 } else { 342 return LauncherAppState.getInstance().getModel().infoFromShortcutIntent(mContext, data); 343 } 344 } 345 346 public String getTargetPackage() { 347 String packageName = launchIntent.getPackage(); 348 if (packageName == null) { 349 packageName = launchIntent.getComponent() == null ? null : 350 launchIntent.getComponent().getPackageName(); 351 } 352 return packageName; 353 } 354 355 public boolean isLuncherActivity() { 356 return activityInfo != null; 357 } 358 } 359 360 private static PendingInstallShortcutInfo decode(String encoded, Context context) { 361 try { 362 JSONObject object = (JSONObject) new JSONTokener(encoded).nextValue(); 363 Intent launcherIntent = Intent.parseUri(object.getString(LAUNCH_INTENT_KEY), 0); 364 365 if (object.optBoolean(APP_SHORTCUT_TYPE_KEY)) { 366 // The is an internal launcher target shortcut. 367 UserHandleCompat user = UserManagerCompat.getInstance(context) 368 .getUserForSerialNumber(object.getLong(USER_HANDLE_KEY)); 369 if (user == null) { 370 return null; 371 } 372 373 LauncherActivityInfoCompat info = LauncherAppsCompat.getInstance(context) 374 .resolveActivity(launcherIntent, user); 375 return info == null ? null : new PendingInstallShortcutInfo(info, context); 376 } 377 378 Intent data = new Intent(); 379 data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launcherIntent); 380 data.putExtra(Intent.EXTRA_SHORTCUT_NAME, object.getString(NAME_KEY)); 381 382 String iconBase64 = object.optString(ICON_KEY); 383 String iconResourceName = object.optString(ICON_RESOURCE_NAME_KEY); 384 String iconResourcePackageName = object.optString(ICON_RESOURCE_PACKAGE_NAME_KEY); 385 if (iconBase64 != null && !iconBase64.isEmpty()) { 386 byte[] iconArray = Base64.decode(iconBase64, Base64.DEFAULT); 387 Bitmap b = BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length); 388 data.putExtra(Intent.EXTRA_SHORTCUT_ICON, b); 389 } else if (iconResourceName != null && !iconResourceName.isEmpty()) { 390 Intent.ShortcutIconResource iconResource = 391 new Intent.ShortcutIconResource(); 392 iconResource.resourceName = iconResourceName; 393 iconResource.packageName = iconResourcePackageName; 394 data.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); 395 } 396 397 return new PendingInstallShortcutInfo(data, context); 398 } catch (JSONException e) { 399 Log.d(TAG, "Exception reading shortcut to add: " + e); 400 } catch (URISyntaxException e) { 401 Log.d(TAG, "Exception reading shortcut to add: " + e); 402 } 403 return null; 404 } 405 406 /** 407 * Tries to create a new PendingInstallShortcutInfo which represents the same target, 408 * but is an app target and not a shortcut. 409 * @return the newly created info or the original one. 410 */ 411 private static PendingInstallShortcutInfo convertToLauncherActivityIfPossible( 412 PendingInstallShortcutInfo original) { 413 if (original.isLuncherActivity()) { 414 // Already an activity target 415 return original; 416 } 417 if (!Utilities.isLauncherAppTarget(original.launchIntent) 418 || !original.user.equals(UserHandleCompat.myUserHandle())) { 419 // We can only convert shortcuts which point to a main activity in the current user. 420 return original; 421 } 422 423 PackageManager pm = original.mContext.getPackageManager(); 424 ResolveInfo info = pm.resolveActivity(original.launchIntent, 0); 425 426 if (info == null) { 427 return original; 428 } 429 430 // Ignore any conflicts in the label name, as that can change based on locale. 431 LauncherActivityInfoCompat launcherInfo = LauncherActivityInfoCompat 432 .fromResolveInfo(info, original.mContext); 433 return new PendingInstallShortcutInfo(launcherInfo, original.mContext); 434 } 435} 436