1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.O; 4 import static android.os.Build.VERSION_CODES.P; 5 import static android.os.Build.VERSION_CODES.S; 6 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; 7 import static org.robolectric.Shadows.shadowOf; 8 9 import android.annotation.SuppressLint; 10 import android.content.Intent; 11 import android.content.IntentSender; 12 import android.content.IntentSender.SendIntentException; 13 import android.content.pm.PackageInstaller; 14 import android.content.pm.PackageInstaller.SessionInfo; 15 import android.content.pm.PackageManager; 16 import android.content.pm.VersionedPackage; 17 import android.graphics.Bitmap; 18 import android.os.Build.VERSION; 19 import android.os.Handler; 20 import android.os.PersistableBundle; 21 import com.google.common.collect.ImmutableList; 22 import java.io.IOException; 23 import java.io.OutputStream; 24 import java.util.ArrayList; 25 import java.util.Collections; 26 import java.util.HashMap; 27 import java.util.HashSet; 28 import java.util.Iterator; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.Set; 32 import javax.annotation.Nonnull; 33 import javax.annotation.Nullable; 34 import org.robolectric.RuntimeEnvironment; 35 import org.robolectric.annotation.ClassName; 36 import org.robolectric.annotation.Implementation; 37 import org.robolectric.annotation.Implements; 38 import org.robolectric.annotation.RealObject; 39 import org.robolectric.shadow.api.Shadow; 40 41 /** Shadow for PackageInstaller. */ 42 @Implements(value = PackageInstaller.class) 43 @SuppressLint("NewApi") 44 public class ShadowPackageInstaller { 45 /** Shadow for PackageInstaller.SessionInfo. */ 46 @Implements(value = PackageInstaller.SessionInfo.class) 47 public static class ShadowSessionInfo { 48 @RealObject private SessionInfo sessionInfo; 49 50 /** Real method makes a system call not available in tests. */ 51 @Implementation getAppIcon()52 protected Bitmap getAppIcon() { 53 return sessionInfo.appIcon; 54 } 55 } 56 57 // According to the documentation, the session ID is always non-zero: 58 // https://developer.android.com/reference/android/content/pm/PackageInstaller#createSession(android.content.pm.PackageInstaller.SessionParams) 59 private int nextSessionId = 1; 60 private Map<Integer, PackageInstaller.SessionInfo> sessionInfos = new HashMap<>(); 61 private Map<Integer, PackageInstaller.Session> sessions = new HashMap<>(); 62 private Set<CallbackInfo> callbackInfos = Collections.synchronizedSet(new HashSet<>()); 63 private final Map<String, UninstalledPackage> uninstalledPackages = new HashMap<>(); 64 65 private static class CallbackInfo { 66 PackageInstaller.SessionCallback callback; 67 Handler handler; 68 } 69 70 @Implementation getAllSessions()71 protected List<PackageInstaller.SessionInfo> getAllSessions() { 72 return ImmutableList.copyOf(sessionInfos.values()); 73 } 74 75 @Implementation getMySessions()76 protected List<PackageInstaller.SessionInfo> getMySessions() { 77 return getAllSessions(); 78 } 79 80 @Implementation registerSessionCallback( @onnull PackageInstaller.SessionCallback callback, @Nonnull Handler handler)81 protected void registerSessionCallback( 82 @Nonnull PackageInstaller.SessionCallback callback, @Nonnull Handler handler) { 83 CallbackInfo callbackInfo = new CallbackInfo(); 84 callbackInfo.callback = callback; 85 callbackInfo.handler = handler; 86 this.callbackInfos.add(callbackInfo); 87 } 88 89 @Implementation unregisterSessionCallback(@onnull PackageInstaller.SessionCallback callback)90 protected void unregisterSessionCallback(@Nonnull PackageInstaller.SessionCallback callback) { 91 for (Iterator<CallbackInfo> i = callbackInfos.iterator(); i.hasNext(); ) { 92 final CallbackInfo callbackInfo = i.next(); 93 if (callbackInfo.callback == callback) { 94 i.remove(); 95 return; 96 } 97 } 98 } 99 100 @Implementation 101 @Nullable getSessionInfo(int sessionId)102 protected PackageInstaller.SessionInfo getSessionInfo(int sessionId) { 103 return sessionInfos.get(sessionId); 104 } 105 106 @Implementation createSession(@onnull PackageInstaller.SessionParams params)107 protected int createSession(@Nonnull PackageInstaller.SessionParams params) throws IOException { 108 final PackageInstaller.SessionInfo sessionInfo = new PackageInstaller.SessionInfo(); 109 sessionInfo.sessionId = nextSessionId++; 110 sessionInfo.active = true; 111 sessionInfo.appPackageName = params.appPackageName; 112 sessionInfo.appLabel = params.appLabel; 113 sessionInfo.appIcon = params.appIcon; 114 if (VERSION.SDK_INT >= P) { 115 sessionInfo.installerPackageName = params.installerPackageName; 116 } 117 118 sessionInfos.put(sessionInfo.getSessionId(), sessionInfo); 119 120 for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) { 121 callbackInfo.handler.post(() -> callbackInfo.callback.onCreated(sessionInfo.sessionId)); 122 } 123 124 return sessionInfo.sessionId; 125 } 126 127 @Implementation abandonSession(int sessionId)128 protected void abandonSession(int sessionId) { 129 sessionInfos.remove(sessionId); 130 sessions.remove(sessionId); 131 132 for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) { 133 callbackInfo.handler.post(() -> callbackInfo.callback.onFinished(sessionId, false)); 134 } 135 } 136 137 @Implementation 138 @Nonnull openSession(int sessionId)139 protected PackageInstaller.Session openSession(int sessionId) throws IOException { 140 if (!sessionInfos.containsKey(sessionId)) { 141 throw new SecurityException("Invalid session Id: " + sessionId); 142 } 143 144 if (sessions.containsKey(sessionId) && sessions.get(sessionId) != null) { 145 return sessions.get(sessionId); 146 } 147 148 PackageInstaller.Session session = new PackageInstaller.Session(null); 149 ShadowSession shadowSession = Shadow.extract(session); 150 shadowSession.setShadowPackageInstaller(sessionId, this); 151 sessions.put(sessionId, session); 152 return session; 153 } 154 155 @Implementation updateSessionAppIcon(int sessionId, Bitmap appIcon)156 protected void updateSessionAppIcon(int sessionId, Bitmap appIcon) { 157 SessionInfo sessionInfo = sessionInfos.get(sessionId); 158 if (sessionInfo == null) { 159 throw new SecurityException("Invalid session Id: " + sessionId); 160 } 161 sessionInfo.appIcon = appIcon; 162 163 for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) { 164 callbackInfo.handler.post( 165 new Runnable() { 166 @Override 167 public void run() { 168 callbackInfo.callback.onBadgingChanged(sessionId); 169 } 170 }); 171 } 172 } 173 174 @Implementation updateSessionAppLabel(int sessionId, CharSequence appLabel)175 protected void updateSessionAppLabel(int sessionId, CharSequence appLabel) { 176 SessionInfo sessionInfo = sessionInfos.get(sessionId); 177 if (sessionInfo == null) { 178 throw new SecurityException("Invalid session Id: " + sessionId); 179 } 180 sessionInfo.appLabel = appLabel; 181 182 for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) { 183 callbackInfo.handler.post( 184 new Runnable() { 185 @Override 186 public void run() { 187 callbackInfo.callback.onBadgingChanged(sessionId); 188 } 189 }); 190 } 191 } 192 193 @Implementation(minSdk = UPSIDE_DOWN_CAKE) uninstall( VersionedPackage versionedPackage, int flags, IntentSender statusReceiver)194 protected void uninstall( 195 VersionedPackage versionedPackage, int flags, IntentSender statusReceiver) { 196 uninstalledPackages.put( 197 versionedPackage.getPackageName(), 198 new UninstalledPackage(versionedPackage.getLongVersionCode(), statusReceiver)); 199 } 200 201 @Implementation(minSdk = O) uninstall(VersionedPackage versionedPackage, IntentSender statusReceiver)202 protected void uninstall(VersionedPackage versionedPackage, IntentSender statusReceiver) { 203 if (VERSION.SDK_INT < P) { 204 uninstalledPackages.put( 205 versionedPackage.getPackageName(), 206 new UninstalledPackage((long) versionedPackage.getVersionCode(), statusReceiver)); 207 } else { 208 uninstalledPackages.put( 209 versionedPackage.getPackageName(), 210 new UninstalledPackage(versionedPackage.getLongVersionCode(), statusReceiver)); 211 } 212 } 213 214 @Implementation uninstall(String packageName, IntentSender statusReceiver)215 protected void uninstall(String packageName, IntentSender statusReceiver) { 216 uninstalledPackages.put( 217 packageName, 218 new UninstalledPackage((long) PackageManager.VERSION_CODE_HIGHEST, statusReceiver)); 219 } 220 221 @Implementation(minSdk = S) uninstallExistingPackage(String packageName, IntentSender statusReceiver)222 protected void uninstallExistingPackage(String packageName, IntentSender statusReceiver) { 223 uninstalledPackages.put( 224 packageName, 225 new UninstalledPackage((long) PackageManager.VERSION_CODE_HIGHEST, statusReceiver)); 226 } 227 getLastUninstalledVersion(String packageName)228 public Long getLastUninstalledVersion(String packageName) { 229 if (uninstalledPackages.get(packageName) == null) { 230 return null; 231 } 232 return uninstalledPackages.get(packageName).version; 233 } 234 getLastUninstalledStatusReceiver(String packageName)235 public IntentSender getLastUninstalledStatusReceiver(String packageName) { 236 if (uninstalledPackages.get(packageName) == null) { 237 return null; 238 } 239 return uninstalledPackages.get(packageName).intentSender; 240 } 241 getAllSessionCallbacks()242 public List<PackageInstaller.SessionCallback> getAllSessionCallbacks() { 243 return ImmutableList.copyOf(callbackInfos.stream().map(info -> info.callback).iterator()); 244 } 245 setSessionProgress(final int sessionId, final float progress)246 public void setSessionProgress(final int sessionId, final float progress) { 247 SessionInfo sessionInfo = sessionInfos.get(sessionId); 248 if (sessionInfo == null) { 249 throw new SecurityException("Invalid session Id: " + sessionId); 250 } 251 sessionInfo.progress = progress; 252 253 for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) { 254 callbackInfo.handler.post(() -> callbackInfo.callback.onProgressChanged(sessionId, progress)); 255 } 256 } 257 setSessionActiveState(final int sessionId, final boolean active)258 public void setSessionActiveState(final int sessionId, final boolean active) { 259 for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) { 260 callbackInfo.handler.post(() -> callbackInfo.callback.onActiveChanged(sessionId, active)); 261 } 262 } 263 264 /** 265 * Prefer instead to use the Android APIs to close the session {@link 266 * android.content.pm.PackageInstaller.Session#commit(IntentSender)} 267 */ 268 @Deprecated setSessionSucceeds(int sessionId)269 public void setSessionSucceeds(int sessionId) { 270 setSessionFinishes(sessionId, true); 271 } 272 setSessionFails(int sessionId)273 public void setSessionFails(int sessionId) { 274 setSessionFinishes(sessionId, false); 275 } 276 277 /** Approve the preapproval dialog. */ setPreapprovalDialogApproved(int sessionId)278 public void setPreapprovalDialogApproved(int sessionId) throws IntentSender.SendIntentException { 279 sendPreapprovalUpdate(sessionId, PackageInstaller.STATUS_SUCCESS); 280 } 281 282 /** Deny the preapproval dialog. */ setPreapprovalDialogDenied(int sessionId)283 public void setPreapprovalDialogDenied(int sessionId) throws IntentSender.SendIntentException { 284 sendPreapprovalUpdate(sessionId, PackageInstaller.STATUS_FAILURE); 285 } 286 287 /** Close the preapproval dialog. */ setPreapprovalDialogDismissed(int sessionId)288 public void setPreapprovalDialogDismissed(int sessionId) throws IntentSender.SendIntentException { 289 sendPreapprovalUpdate(sessionId, PackageInstaller.STATUS_FAILURE_ABORTED); 290 } 291 292 /** 293 * Sends an update to the preapproval status receiver. 294 * 295 * @param status refers to the Session status. See 296 * https://developer.android.com/reference/android/content/pm/PackageInstaller for possible 297 * values. 298 */ sendPreapprovalUpdate(int sessionId, int status)299 private void sendPreapprovalUpdate(int sessionId, int status) 300 throws IntentSender.SendIntentException { 301 ShadowSession shadowSession = shadowOf(sessions.get(sessionId)); 302 Intent fillIn = new Intent(); 303 fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); 304 fillIn.putExtra(PackageInstaller.EXTRA_STATUS, status); 305 fillIn.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL, true); 306 shadowSession.preapprovalStatusReceiver.sendIntent( 307 RuntimeEnvironment.getApplication(), 308 0, 309 fillIn, 310 null /* onFinished */, 311 null /* handler */, 312 null /* requiredPermission */); 313 } 314 setSessionFinishes(final int sessionId, final boolean success)315 private void setSessionFinishes(final int sessionId, final boolean success) { 316 for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) { 317 callbackInfo.handler.post(() -> callbackInfo.callback.onFinished(sessionId, success)); 318 } 319 320 PackageInstaller.Session session = sessions.get(sessionId); 321 ShadowSession shadowSession = Shadow.extract(session); 322 if (success) { 323 try { 324 shadowSession.statusReceiver.sendIntent( 325 RuntimeEnvironment.getApplication(), 0, null, null, null, null); 326 } catch (SendIntentException e) { 327 throw new RuntimeException(e); 328 } 329 } 330 } 331 332 /** Shadow for PackageInstaller.Session. */ 333 @Implements(value = PackageInstaller.Session.class) 334 public static class ShadowSession { 335 336 private OutputStream outputStream; 337 private boolean outputStreamOpen; 338 private IntentSender statusReceiver; 339 private IntentSender preapprovalStatusReceiver; 340 private int sessionId; 341 private ShadowPackageInstaller shadowPackageInstaller; 342 private PersistableBundle appMetadata = new PersistableBundle(); 343 344 @Implementation(minSdk = UPSIDE_DOWN_CAKE) requestUserPreapproval( @onnull @lassName"android.content.pm.PackageInstaller$PreapprovalDetails") Object details, @Nonnull IntentSender statusReceiver)345 protected void requestUserPreapproval( 346 @Nonnull @ClassName("android.content.pm.PackageInstaller$PreapprovalDetails") 347 Object details, 348 @Nonnull IntentSender statusReceiver) { 349 preapprovalStatusReceiver = statusReceiver; 350 } 351 352 @Implementation(minSdk = UPSIDE_DOWN_CAKE) setAppMetadata(@ullable PersistableBundle data)353 protected void setAppMetadata(@Nullable PersistableBundle data) throws IOException { 354 appMetadata = data; 355 } 356 357 @Implementation(minSdk = UPSIDE_DOWN_CAKE) 358 @Nonnull getAppMetadata()359 protected PersistableBundle getAppMetadata() { 360 return appMetadata; 361 } 362 363 @Implementation 364 @Nonnull openWrite(@onnull String name, long offsetBytes, long lengthBytes)365 protected OutputStream openWrite(@Nonnull String name, long offsetBytes, long lengthBytes) 366 throws IOException { 367 outputStream = 368 new OutputStream() { 369 @Override 370 public void write(int aByte) throws IOException {} 371 372 @Override 373 public void close() throws IOException { 374 outputStreamOpen = false; 375 } 376 }; 377 outputStreamOpen = true; 378 return outputStream; 379 } 380 381 @Implementation fsync(@onnull OutputStream out)382 protected void fsync(@Nonnull OutputStream out) throws IOException {} 383 384 @Implementation commit(@onnull IntentSender statusReceiver)385 protected void commit(@Nonnull IntentSender statusReceiver) { 386 this.statusReceiver = statusReceiver; 387 if (outputStreamOpen) { 388 throw new SecurityException("OutputStream still open"); 389 } 390 391 shadowPackageInstaller.setSessionSucceeds(sessionId); 392 } 393 394 @Implementation close()395 protected void close() {} 396 397 @Implementation abandon()398 protected void abandon() { 399 shadowPackageInstaller.abandonSession(sessionId); 400 } 401 setShadowPackageInstaller( int sessionId, ShadowPackageInstaller shadowPackageInstaller)402 private void setShadowPackageInstaller( 403 int sessionId, ShadowPackageInstaller shadowPackageInstaller) { 404 this.sessionId = sessionId; 405 this.shadowPackageInstaller = shadowPackageInstaller; 406 } 407 } 408 409 private static class UninstalledPackage { 410 Long version; 411 IntentSender intentSender; 412 UninstalledPackage(Long version, IntentSender intentSender)413 public UninstalledPackage(Long version, IntentSender intentSender) { 414 this.version = version; 415 this.intentSender = intentSender; 416 } 417 } 418 } 419