• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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