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