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