1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.L;
4 import static android.os.Build.VERSION_CODES.N;
5 import static android.os.Build.VERSION_CODES.N_MR1;
6 import static android.os.Build.VERSION_CODES.O;
7 import static android.os.Build.VERSION_CODES.P;
8 import static android.os.Build.VERSION_CODES.Q;
9 import static org.robolectric.util.reflector.Reflector.reflector;
10 
11 import android.annotation.NonNull;
12 import android.annotation.Nullable;
13 import android.content.ComponentName;
14 import android.content.IntentSender;
15 import android.content.pm.ApplicationInfo;
16 import android.content.pm.LauncherActivityInfo;
17 import android.content.pm.LauncherApps;
18 import android.content.pm.LauncherApps.ShortcutQuery;
19 import android.content.pm.PackageInstaller.SessionCallback;
20 import android.content.pm.PackageInstaller.SessionInfo;
21 import android.content.pm.PackageManager.NameNotFoundException;
22 import android.content.pm.ShortcutInfo;
23 import android.graphics.Rect;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.os.Looper;
27 import android.os.Process;
28 import android.os.UserHandle;
29 import android.util.Pair;
30 import com.google.common.collect.HashMultimap;
31 import com.google.common.collect.Iterables;
32 import com.google.common.collect.Lists;
33 import com.google.common.collect.Multimap;
34 import java.util.ArrayList;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.concurrent.Executor;
39 import java.util.function.Predicate;
40 import java.util.stream.Collectors;
41 import org.robolectric.annotation.Implementation;
42 import org.robolectric.annotation.Implements;
43 import org.robolectric.annotation.Resetter;
44 import org.robolectric.util.reflector.Accessor;
45 import org.robolectric.util.reflector.ForType;
46 
47 /** Shadow of {@link android.content.pm.LauncherApps}. */
48 @Implements(value = LauncherApps.class)
49 public class ShadowLauncherApps {
50   private static List<ShortcutInfo> shortcuts = new ArrayList<>();
51   private static final Multimap<UserHandle, String> enabledPackages = HashMultimap.create();
52   private static final Multimap<UserHandle, ComponentName> enabledActivities =
53       HashMultimap.create();
54   private static final Multimap<UserHandle, LauncherActivityInfo> shortcutActivityList =
55       HashMultimap.create();
56   private static final Multimap<UserHandle, LauncherActivityInfo> activityList =
57       HashMultimap.create();
58   private static final Map<UserHandle, Map<String, ApplicationInfo>> applicationInfoList =
59       new HashMap<>();
60   private static final Map<UserHandle, Map<String, Bundle>> suspendedPackageLauncherExtras =
61       new HashMap<>();
62 
63   private static final List<Pair<LauncherApps.Callback, Handler>> callbacks = new ArrayList<>();
64   private static boolean hasShortcutHostPermission = false;
65 
66   @Resetter
reset()67   public static void reset() {
68     shortcuts.clear();
69     enabledPackages.clear();
70     enabledActivities.clear();
71     shortcutActivityList.clear();
72     activityList.clear();
73     applicationInfoList.clear();
74     suspendedPackageLauncherExtras.clear();
75     callbacks.clear();
76     hasShortcutHostPermission = false;
77   }
78 
79   /**
80    * Adds a dynamic shortcut to be returned by {@link #getShortcuts(ShortcutQuery, UserHandle)}.
81    *
82    * @param shortcutInfo the shortcut to add.
83    */
addDynamicShortcut(ShortcutInfo shortcutInfo)84   public void addDynamicShortcut(ShortcutInfo shortcutInfo) {
85     shortcuts.add(shortcutInfo);
86     shortcutsChanged(shortcutInfo.getPackage(), Lists.newArrayList(shortcutInfo));
87   }
88 
shortcutsChanged(String packageName, List<ShortcutInfo> shortcuts)89   private void shortcutsChanged(String packageName, List<ShortcutInfo> shortcuts) {
90     for (Pair<LauncherApps.Callback, Handler> callbackPair : callbacks) {
91       callbackPair.second.post(
92           () ->
93               callbackPair.first.onShortcutsChanged(
94                   packageName, shortcuts, Process.myUserHandle()));
95     }
96   }
97 
98   /**
99    * Fires {@link LauncherApps.Callback#onPackageAdded(String, UserHandle)} on all of the registered
100    * callbacks, with the provided packageName.
101    *
102    * @param packageName the package the was added.
103    */
notifyPackageAdded(String packageName)104   public void notifyPackageAdded(String packageName) {
105     for (Pair<LauncherApps.Callback, Handler> callbackPair : callbacks) {
106       callbackPair.second.post(
107           () -> callbackPair.first.onPackageAdded(packageName, Process.myUserHandle()));
108     }
109   }
110 
111   /**
112    * Adds an enabled package to be checked by {@link #isPackageEnabled(String, UserHandle)}.
113    *
114    * @param userHandle the user handle to be added.
115    * @param packageName the package name to be added.
116    */
addEnabledPackage(UserHandle userHandle, String packageName)117   public void addEnabledPackage(UserHandle userHandle, String packageName) {
118     enabledPackages.put(userHandle, packageName);
119   }
120 
121   /**
122    * Sets an activity referenced by ComponentName as enabled, to be checked by {@link
123    * #isActivityEnabled(ComponentName, UserHandle)}.
124    *
125    * @param userHandle the user handle to be set.
126    * @param componentName the component name of the activity to be enabled.
127    */
setActivityEnabled(UserHandle userHandle, ComponentName componentName)128   public void setActivityEnabled(UserHandle userHandle, ComponentName componentName) {
129     enabledActivities.put(userHandle, componentName);
130   }
131 
132   /**
133    * Adds a {@link LauncherActivityInfo} to be retrieved by {@link
134    * #getShortcutConfigActivityList(String, UserHandle)}.
135    *
136    * @param userHandle the user handle to be added.
137    * @param activityInfo the {@link LauncherActivityInfo} to be added.
138    */
addShortcutConfigActivity(UserHandle userHandle, LauncherActivityInfo activityInfo)139   public void addShortcutConfigActivity(UserHandle userHandle, LauncherActivityInfo activityInfo) {
140     shortcutActivityList.put(userHandle, activityInfo);
141   }
142 
143   /**
144    * Adds a {@link LauncherActivityInfo} to be retrieved by {@link #getActivityList(String,
145    * UserHandle)}.
146    *
147    * @param userHandle the user handle to be added.
148    * @param activityInfo the {@link LauncherActivityInfo} to be added.
149    */
addActivity(UserHandle userHandle, LauncherActivityInfo activityInfo)150   public void addActivity(UserHandle userHandle, LauncherActivityInfo activityInfo) {
151     activityList.put(userHandle, activityInfo);
152   }
153 
154   /**
155    * Fires {@link LauncherApps.Callback#onPackageRemoved(String, UserHandle)} on all of the
156    * registered callbacks, with the provided packageName.
157    *
158    * @param packageName the package the was removed.
159    */
notifyPackageRemoved(String packageName)160   public void notifyPackageRemoved(String packageName) {
161     for (Pair<LauncherApps.Callback, Handler> callbackPair : callbacks) {
162       callbackPair.second.post(
163           () -> callbackPair.first.onPackageRemoved(packageName, Process.myUserHandle()));
164     }
165   }
166 
167   /**
168    * Adds a {@link ApplicationInfo} to be retrieved by {@link #getApplicationInfo(String, int,
169    * UserHandle)}.
170    *
171    * @param userHandle the user handle to be added.
172    * @param packageName the package name to be added.
173    * @param applicationInfo the application info to be added.
174    */
addApplicationInfo( UserHandle userHandle, String packageName, ApplicationInfo applicationInfo)175   public void addApplicationInfo(
176       UserHandle userHandle, String packageName, ApplicationInfo applicationInfo) {
177     if (!applicationInfoList.containsKey(userHandle)) {
178       applicationInfoList.put(userHandle, new HashMap<>());
179     }
180     applicationInfoList.get(userHandle).put(packageName, applicationInfo);
181   }
182 
183   @Implementation(minSdk = Q)
startPackageInstallerSessionDetailsActivity( @onNull SessionInfo sessionInfo, @Nullable Rect sourceBounds, @Nullable Bundle opts)184   protected void startPackageInstallerSessionDetailsActivity(
185       @NonNull SessionInfo sessionInfo, @Nullable Rect sourceBounds, @Nullable Bundle opts) {
186     throw new UnsupportedOperationException(
187         "This method is not currently supported in Robolectric.");
188   }
189 
190   @Implementation
startAppDetailsActivity( ComponentName component, UserHandle user, Rect sourceBounds, Bundle opts)191   protected void startAppDetailsActivity(
192       ComponentName component, UserHandle user, Rect sourceBounds, Bundle opts) {
193     throw new UnsupportedOperationException(
194         "This method is not currently supported in Robolectric.");
195   }
196 
197   @Implementation(minSdk = O)
getShortcutConfigActivityList( @ullable String packageName, @NonNull UserHandle user)198   protected List<LauncherActivityInfo> getShortcutConfigActivityList(
199       @Nullable String packageName, @NonNull UserHandle user) {
200     return shortcutActivityList.get(user).stream()
201         .filter(matchesPackage(packageName))
202         .collect(Collectors.toList());
203   }
204 
205   @Implementation(minSdk = O)
206   @Nullable
getShortcutConfigActivityIntent(@onNull LauncherActivityInfo info)207   protected IntentSender getShortcutConfigActivityIntent(@NonNull LauncherActivityInfo info) {
208     throw new UnsupportedOperationException(
209         "This method is not currently supported in Robolectric.");
210   }
211 
212   @Implementation
isPackageEnabled(String packageName, UserHandle user)213   protected boolean isPackageEnabled(String packageName, UserHandle user) {
214     return enabledPackages.get(user).contains(packageName);
215   }
216 
217   @Implementation(minSdk = L)
getActivityList(String packageName, UserHandle user)218   protected List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
219     return activityList.get(user).stream()
220         .filter(matchesPackage(packageName))
221         .collect(Collectors.toList());
222   }
223 
224   @Implementation(minSdk = O)
getApplicationInfo( @onNull String packageName, int flags, @NonNull UserHandle user)225   protected ApplicationInfo getApplicationInfo(
226       @NonNull String packageName, int flags, @NonNull UserHandle user)
227       throws NameNotFoundException {
228     if (applicationInfoList.containsKey(user)) {
229       Map<String, ApplicationInfo> map = applicationInfoList.get(user);
230       if (map.containsKey(packageName)) {
231         return map.get(packageName);
232       }
233     }
234     throw new NameNotFoundException(
235         "Package " + packageName + " not found for user " + user.getIdentifier());
236   }
237 
238   /**
239    * Adds a {@link Bundle} to be retrieved by {@link #getSuspendedPackageLauncherExtras(String,
240    * UserHandle)}.
241    *
242    * @param userHandle the user handle to be added.
243    * @param packageName the package name to be added.
244    * @param bundle the bundle for the extras.
245    */
addSuspendedPackageLauncherExtras( UserHandle userHandle, String packageName, Bundle bundle)246   public void addSuspendedPackageLauncherExtras(
247       UserHandle userHandle, String packageName, Bundle bundle) {
248     if (!suspendedPackageLauncherExtras.containsKey(userHandle)) {
249       suspendedPackageLauncherExtras.put(userHandle, new HashMap<>());
250     }
251     suspendedPackageLauncherExtras.get(userHandle).put(packageName, bundle);
252   }
253 
254   @Implementation(minSdk = P)
255   @Nullable
getSuspendedPackageLauncherExtras(String packageName, UserHandle user)256   protected Bundle getSuspendedPackageLauncherExtras(String packageName, UserHandle user)
257       throws NameNotFoundException {
258     Map<String, Bundle> map = suspendedPackageLauncherExtras.get(user);
259     if (map != null && map.containsKey(packageName)) {
260       return map.get(packageName);
261     }
262 
263     throw new NameNotFoundException(
264         "Suspended package extras for  "
265             + packageName
266             + " not found for user "
267             + user.getIdentifier());
268   }
269 
270   @Implementation(minSdk = Q)
shouldHideFromSuggestions( @onNull String packageName, @NonNull UserHandle user)271   protected boolean shouldHideFromSuggestions(
272       @NonNull String packageName, @NonNull UserHandle user) {
273     throw new UnsupportedOperationException(
274         "This method is not currently supported in Robolectric.");
275   }
276 
277   @Implementation(minSdk = L)
isActivityEnabled(ComponentName component, UserHandle user)278   protected boolean isActivityEnabled(ComponentName component, UserHandle user) {
279     return enabledActivities.containsEntry(user, component);
280   }
281 
282   /**
283    * Sets the return value of {@link #hasShortcutHostPermission()}. If this isn't explicitly set,
284    * {@link #hasShortcutHostPermission()} defaults to returning false.
285    *
286    * @param permission boolean to be returned
287    */
setHasShortcutHostPermission(boolean permission)288   public void setHasShortcutHostPermission(boolean permission) {
289     hasShortcutHostPermission = permission;
290   }
291 
292   @Implementation(minSdk = N)
hasShortcutHostPermission()293   protected boolean hasShortcutHostPermission() {
294     return hasShortcutHostPermission;
295   }
296 
297   /**
298    * This method is an incomplete implementation of this API that only supports querying for pinned
299    * dynamic shortcuts. It also doesn't not support {@link ShortcutQuery#setChangedSince(long)}.
300    */
301   @Implementation(minSdk = N_MR1)
302   @Nullable
getShortcuts( @onNull ShortcutQuery query, @NonNull UserHandle user)303   protected List<ShortcutInfo> getShortcuts(
304       @NonNull ShortcutQuery query, @NonNull UserHandle user) {
305     if (reflector(ReflectorShortcutQuery.class, query).getChangedSince() != 0) {
306       throw new UnsupportedOperationException(
307           "Robolectric does not currently support ShortcutQueries that filter on time since"
308               + " change.");
309     }
310     int flags = reflector(ReflectorShortcutQuery.class, query).getQueryFlags();
311     if ((flags & ShortcutQuery.FLAG_MATCH_PINNED) == 0
312         || (flags & ShortcutQuery.FLAG_MATCH_DYNAMIC) == 0) {
313       throw new UnsupportedOperationException(
314           "Robolectric does not currently support ShortcutQueries that match non-dynamic"
315               + " Shortcuts.");
316     }
317     Iterable<ShortcutInfo> shortcutsItr = shortcuts;
318 
319     List<String> ids = reflector(ReflectorShortcutQuery.class, query).getShortcutIds();
320     if (ids != null) {
321       shortcutsItr = Iterables.filter(shortcutsItr, shortcut -> ids.contains(shortcut.getId()));
322     }
323     ComponentName activity = reflector(ReflectorShortcutQuery.class, query).getActivity();
324     if (activity != null) {
325       shortcutsItr =
326           Iterables.filter(shortcutsItr, shortcut -> shortcut.getActivity().equals(activity));
327     }
328     String packageName = reflector(ReflectorShortcutQuery.class, query).getPackage();
329     if (packageName != null && !packageName.isEmpty()) {
330       shortcutsItr =
331           Iterables.filter(shortcutsItr, shortcut -> shortcut.getPackage().equals(packageName));
332     }
333     return Lists.newArrayList(shortcutsItr);
334   }
335 
336   @Implementation(minSdk = N_MR1)
pinShortcuts( @onNull String packageName, @NonNull List<String> shortcutIds, @NonNull UserHandle user)337   protected void pinShortcuts(
338       @NonNull String packageName, @NonNull List<String> shortcutIds, @NonNull UserHandle user) {
339     Iterable<ShortcutInfo> changed =
340         Iterables.filter(shortcuts, shortcut -> !shortcutIds.contains(shortcut.getId()));
341     List<ShortcutInfo> ret = Lists.newArrayList(changed);
342     shortcuts =
343         Lists.newArrayList(
344             Iterables.filter(shortcuts, shortcut -> shortcutIds.contains(shortcut.getId())));
345 
346     shortcutsChanged(packageName, ret);
347   }
348 
349   @Implementation(minSdk = N_MR1)
startShortcut( @onNull String packageName, @NonNull String shortcutId, @Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions, @NonNull UserHandle user)350   protected void startShortcut(
351       @NonNull String packageName,
352       @NonNull String shortcutId,
353       @Nullable Rect sourceBounds,
354       @Nullable Bundle startActivityOptions,
355       @NonNull UserHandle user) {
356     throw new UnsupportedOperationException(
357         "This method is not currently supported in Robolectric.");
358   }
359 
360   @Implementation(minSdk = N_MR1)
startShortcut( @onNull ShortcutInfo shortcut, @Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions)361   protected void startShortcut(
362       @NonNull ShortcutInfo shortcut,
363       @Nullable Rect sourceBounds,
364       @Nullable Bundle startActivityOptions) {
365     throw new UnsupportedOperationException(
366         "This method is not currently supported in Robolectric.");
367   }
368 
369   @Implementation
registerCallback(LauncherApps.Callback callback)370   protected void registerCallback(LauncherApps.Callback callback) {
371     registerCallback(callback, null);
372   }
373 
374   @Implementation
registerCallback(LauncherApps.Callback callback, Handler handler)375   protected void registerCallback(LauncherApps.Callback callback, Handler handler) {
376     callbacks.add(
377         Pair.create(callback, handler != null ? handler : new Handler(Looper.myLooper())));
378   }
379 
380   @Implementation
unregisterCallback(LauncherApps.Callback callback)381   protected void unregisterCallback(LauncherApps.Callback callback) {
382     int index = Iterables.indexOf(this.callbacks, pair -> pair.first == callback);
383     if (index != -1) {
384       this.callbacks.remove(index);
385     }
386   }
387 
388   @Implementation(minSdk = Q)
registerPackageInstallerSessionCallback( @onNull Executor executor, @NonNull SessionCallback callback)389   protected void registerPackageInstallerSessionCallback(
390       @NonNull Executor executor, @NonNull SessionCallback callback) {
391     throw new UnsupportedOperationException(
392         "This method is not currently supported in Robolectric.");
393   }
394 
395   @Implementation(minSdk = Q)
unregisterPackageInstallerSessionCallback(@onNull SessionCallback callback)396   protected void unregisterPackageInstallerSessionCallback(@NonNull SessionCallback callback) {
397     throw new UnsupportedOperationException(
398         "This method is not currently supported in Robolectric.");
399   }
400 
401   @Implementation(minSdk = Q)
402   @NonNull
getAllPackageInstallerSessions()403   protected List<SessionInfo> getAllPackageInstallerSessions() {
404     throw new UnsupportedOperationException(
405         "This method is not currently supported in Robolectric.");
406   }
407 
matchesPackage(@ullable String packageName)408   private Predicate<LauncherActivityInfo> matchesPackage(@Nullable String packageName) {
409     return info ->
410         packageName == null
411             || (info.getComponentName() != null
412                 && packageName.equals(info.getComponentName().getPackageName()));
413   }
414 
415   @ForType(ShortcutQuery.class)
416   private interface ReflectorShortcutQuery {
417     @Accessor("mChangedSince")
getChangedSince()418     long getChangedSince();
419 
420     @Accessor("mQueryFlags")
getQueryFlags()421     int getQueryFlags();
422 
423     @Accessor("mShortcutIds")
getShortcutIds()424     List<String> getShortcutIds();
425 
426     @Accessor("mActivity")
getActivity()427     ComponentName getActivity();
428 
429     @Accessor("mPackage")
getPackage()430     String getPackage();
431   }
432 }
433