
org.robolectric.shadows.ShadowPackageInstaller Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of shadows-framework Show documentation
Show all versions of shadows-framework Show documentation
An alternative Android testing framework.
The newest version!
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
import static org.robolectric.Shadows.shadowOf;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.PreapprovalDetails;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
import android.graphics.Bitmap;
import android.os.Build.VERSION;
import android.os.Handler;
import android.os.PersistableBundle;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
/** Shadow for PackageInstaller. */
@Implements(value = PackageInstaller.class)
@SuppressLint("NewApi")
public class ShadowPackageInstaller {
/** Shadow for PackageInstaller.SessionInfo. */
@Implements(value = PackageInstaller.SessionInfo.class)
public static class ShadowSessionInfo {
@RealObject private SessionInfo sessionInfo;
/** Real method makes a system call not available in tests. */
@Implementation
protected Bitmap getAppIcon() {
return sessionInfo.appIcon;
}
}
// According to the documentation, the session ID is always non-zero:
// https://developer.android.com/reference/android/content/pm/PackageInstaller#createSession(android.content.pm.PackageInstaller.SessionParams)
private int nextSessionId = 1;
private Map sessionInfos = new HashMap<>();
private Map sessions = new HashMap<>();
private Set callbackInfos = Collections.synchronizedSet(new HashSet<>());
private final Map uninstalledPackages = new HashMap<>();
private static class CallbackInfo {
PackageInstaller.SessionCallback callback;
Handler handler;
}
@Implementation
protected List getAllSessions() {
return ImmutableList.copyOf(sessionInfos.values());
}
@Implementation
protected List getMySessions() {
return getAllSessions();
}
@Implementation
protected void registerSessionCallback(
@NonNull PackageInstaller.SessionCallback callback, @NonNull Handler handler) {
CallbackInfo callbackInfo = new CallbackInfo();
callbackInfo.callback = callback;
callbackInfo.handler = handler;
this.callbackInfos.add(callbackInfo);
}
@Implementation
protected void unregisterSessionCallback(@NonNull PackageInstaller.SessionCallback callback) {
for (Iterator i = callbackInfos.iterator(); i.hasNext(); ) {
final CallbackInfo callbackInfo = i.next();
if (callbackInfo.callback == callback) {
i.remove();
return;
}
}
}
@Implementation
@Nullable
protected PackageInstaller.SessionInfo getSessionInfo(int sessionId) {
return sessionInfos.get(sessionId);
}
@Implementation
protected int createSession(@NonNull PackageInstaller.SessionParams params) throws IOException {
final PackageInstaller.SessionInfo sessionInfo = new PackageInstaller.SessionInfo();
sessionInfo.sessionId = nextSessionId++;
sessionInfo.active = true;
sessionInfo.appPackageName = params.appPackageName;
sessionInfo.appLabel = params.appLabel;
sessionInfo.appIcon = params.appIcon;
if (VERSION.SDK_INT >= P) {
sessionInfo.installerPackageName = params.installerPackageName;
}
sessionInfos.put(sessionInfo.getSessionId(), sessionInfo);
for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) {
callbackInfo.handler.post(() -> callbackInfo.callback.onCreated(sessionInfo.sessionId));
}
return sessionInfo.sessionId;
}
@Implementation
protected void abandonSession(int sessionId) {
sessionInfos.remove(sessionId);
sessions.remove(sessionId);
for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) {
callbackInfo.handler.post(() -> callbackInfo.callback.onFinished(sessionId, false));
}
}
@Implementation
@NonNull
protected PackageInstaller.Session openSession(int sessionId) throws IOException {
if (!sessionInfos.containsKey(sessionId)) {
throw new SecurityException("Invalid session Id: " + sessionId);
}
if (sessions.containsKey(sessionId) && sessions.get(sessionId) != null) {
return sessions.get(sessionId);
}
PackageInstaller.Session session = new PackageInstaller.Session(null);
ShadowSession shadowSession = Shadow.extract(session);
shadowSession.setShadowPackageInstaller(sessionId, this);
sessions.put(sessionId, session);
return session;
}
@Implementation
protected void updateSessionAppIcon(int sessionId, Bitmap appIcon) {
SessionInfo sessionInfo = sessionInfos.get(sessionId);
if (sessionInfo == null) {
throw new SecurityException("Invalid session Id: " + sessionId);
}
sessionInfo.appIcon = appIcon;
for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) {
callbackInfo.handler.post(
new Runnable() {
@Override
public void run() {
callbackInfo.callback.onBadgingChanged(sessionId);
}
});
}
}
@Implementation
protected void updateSessionAppLabel(int sessionId, CharSequence appLabel) {
SessionInfo sessionInfo = sessionInfos.get(sessionId);
if (sessionInfo == null) {
throw new SecurityException("Invalid session Id: " + sessionId);
}
sessionInfo.appLabel = appLabel;
for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) {
callbackInfo.handler.post(
new Runnable() {
@Override
public void run() {
callbackInfo.callback.onBadgingChanged(sessionId);
}
});
}
}
@Implementation(minSdk = UPSIDE_DOWN_CAKE)
protected void uninstall(
VersionedPackage versionedPackage, int flags, IntentSender statusReceiver) {
uninstalledPackages.put(
versionedPackage.getPackageName(),
new UninstalledPackage(versionedPackage.getLongVersionCode(), statusReceiver));
}
@Implementation(minSdk = O)
protected void uninstall(VersionedPackage versionedPackage, IntentSender statusReceiver) {
if (VERSION.SDK_INT < P) {
uninstalledPackages.put(
versionedPackage.getPackageName(),
new UninstalledPackage((long) versionedPackage.getVersionCode(), statusReceiver));
} else {
uninstalledPackages.put(
versionedPackage.getPackageName(),
new UninstalledPackage(versionedPackage.getLongVersionCode(), statusReceiver));
}
}
@Implementation
protected void uninstall(String packageName, IntentSender statusReceiver) {
uninstalledPackages.put(
packageName,
new UninstalledPackage((long) PackageManager.VERSION_CODE_HIGHEST, statusReceiver));
}
@Implementation(minSdk = S)
protected void uninstallExistingPackage(String packageName, IntentSender statusReceiver) {
uninstalledPackages.put(
packageName,
new UninstalledPackage((long) PackageManager.VERSION_CODE_HIGHEST, statusReceiver));
}
public Long getLastUninstalledVersion(String packageName) {
if (uninstalledPackages.get(packageName) == null) {
return null;
}
return uninstalledPackages.get(packageName).version;
}
public IntentSender getLastUninstalledStatusReceiver(String packageName) {
if (uninstalledPackages.get(packageName) == null) {
return null;
}
return uninstalledPackages.get(packageName).intentSender;
}
public List getAllSessionCallbacks() {
return ImmutableList.copyOf(callbackInfos.stream().map(info -> info.callback).iterator());
}
public void setSessionProgress(final int sessionId, final float progress) {
SessionInfo sessionInfo = sessionInfos.get(sessionId);
if (sessionInfo == null) {
throw new SecurityException("Invalid session Id: " + sessionId);
}
sessionInfo.progress = progress;
for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) {
callbackInfo.handler.post(() -> callbackInfo.callback.onProgressChanged(sessionId, progress));
}
}
public void setSessionActiveState(final int sessionId, final boolean active) {
for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) {
callbackInfo.handler.post(() -> callbackInfo.callback.onActiveChanged(sessionId, active));
}
}
/**
* Prefer instead to use the Android APIs to close the session {@link
* android.content.pm.PackageInstaller.Session#commit(IntentSender)}
*/
@Deprecated
public void setSessionSucceeds(int sessionId) {
setSessionFinishes(sessionId, true);
}
public void setSessionFails(int sessionId) {
setSessionFinishes(sessionId, false);
}
/** Approve the preapproval dialog. */
public void setPreapprovalDialogApproved(int sessionId) throws IntentSender.SendIntentException {
sendPreapprovalUpdate(sessionId, PackageInstaller.STATUS_SUCCESS);
}
/** Deny the preapproval dialog. */
public void setPreapprovalDialogDenied(int sessionId) throws IntentSender.SendIntentException {
sendPreapprovalUpdate(sessionId, PackageInstaller.STATUS_FAILURE);
}
/** Close the preapproval dialog. */
public void setPreapprovalDialogDismissed(int sessionId) throws IntentSender.SendIntentException {
sendPreapprovalUpdate(sessionId, PackageInstaller.STATUS_FAILURE_ABORTED);
}
/**
* Sends an update to the preapproval status receiver.
*
* @param status refers to the Session status. See
* https://developer.android.com/reference/android/content/pm/PackageInstaller for possible
* values.
*/
private void sendPreapprovalUpdate(int sessionId, int status)
throws IntentSender.SendIntentException {
ShadowSession shadowSession = shadowOf(sessions.get(sessionId));
Intent fillIn = new Intent();
fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
fillIn.putExtra(PackageInstaller.EXTRA_STATUS, status);
fillIn.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL, true);
shadowSession.preapprovalStatusReceiver.sendIntent(
RuntimeEnvironment.getApplication(),
0,
fillIn,
null /* onFinished */,
null /* handler */,
null /* requiredPermission */);
}
private void setSessionFinishes(final int sessionId, final boolean success) {
for (final CallbackInfo callbackInfo : new ArrayList<>(callbackInfos)) {
callbackInfo.handler.post(() -> callbackInfo.callback.onFinished(sessionId, success));
}
PackageInstaller.Session session = sessions.get(sessionId);
ShadowSession shadowSession = Shadow.extract(session);
if (success) {
try {
shadowSession.statusReceiver.sendIntent(
RuntimeEnvironment.getApplication(), 0, null, null, null, null);
} catch (SendIntentException e) {
throw new RuntimeException(e);
}
}
}
/** Shadow for PackageInstaller.Session. */
@Implements(value = PackageInstaller.Session.class)
public static class ShadowSession {
private OutputStream outputStream;
private boolean outputStreamOpen;
private IntentSender statusReceiver;
private IntentSender preapprovalStatusReceiver;
private int sessionId;
private ShadowPackageInstaller shadowPackageInstaller;
private PersistableBundle appMetadata = new PersistableBundle();
@Implementation(minSdk = UPSIDE_DOWN_CAKE)
protected void requestUserPreapproval(
@NonNull PreapprovalDetails details, @NonNull IntentSender statusReceiver) {
preapprovalStatusReceiver = statusReceiver;
}
@Implementation(minSdk = UPSIDE_DOWN_CAKE)
protected void setAppMetadata(@Nullable PersistableBundle data) throws IOException {
appMetadata = data;
}
@Implementation(minSdk = UPSIDE_DOWN_CAKE)
@NonNull
protected PersistableBundle getAppMetadata() {
return appMetadata;
}
@Implementation
@NonNull
protected OutputStream openWrite(@NonNull String name, long offsetBytes, long lengthBytes)
throws IOException {
outputStream =
new OutputStream() {
@Override
public void write(int aByte) throws IOException {}
@Override
public void close() throws IOException {
outputStreamOpen = false;
}
};
outputStreamOpen = true;
return outputStream;
}
@Implementation
protected void fsync(@NonNull OutputStream out) throws IOException {}
@Implementation
protected void commit(@NonNull IntentSender statusReceiver) {
this.statusReceiver = statusReceiver;
if (outputStreamOpen) {
throw new SecurityException("OutputStream still open");
}
shadowPackageInstaller.setSessionSucceeds(sessionId);
}
@Implementation
protected void close() {}
@Implementation
protected void abandon() {
shadowPackageInstaller.abandonSession(sessionId);
}
private void setShadowPackageInstaller(
int sessionId, ShadowPackageInstaller shadowPackageInstaller) {
this.sessionId = sessionId;
this.shadowPackageInstaller = shadowPackageInstaller;
}
}
private static class UninstalledPackage {
Long version;
IntentSender intentSender;
public UninstalledPackage(Long version, IntentSender intentSender) {
this.version = version;
this.intentSender = intentSender;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy