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