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