• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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