1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.KITKAT_WATCH; 4 import static android.os.Build.VERSION_CODES.LOLLIPOP; 5 6 import android.annotation.NonNull; 7 import android.annotation.Nullable; 8 import android.annotation.SuppressLint; 9 import android.content.IntentSender; 10 import android.content.IntentSender.SendIntentException; 11 import android.content.pm.PackageInstaller; 12 import android.os.Handler; 13 import com.google.common.collect.ImmutableList; 14 import java.io.IOException; 15 import java.io.OutputStream; 16 import java.util.HashMap; 17 import java.util.HashSet; 18 import java.util.List; 19 import java.util.Map; 20 import java.util.Set; 21 import org.robolectric.RuntimeEnvironment; 22 import org.robolectric.annotation.Implementation; 23 import org.robolectric.annotation.Implements; 24 import org.robolectric.shadow.api.Shadow; 25 26 @Implements(value = PackageInstaller.class, minSdk = LOLLIPOP) 27 @SuppressLint("NewApi") 28 public class ShadowPackageInstaller { 29 30 private int nextSessionId; 31 private Map<Integer, PackageInstaller.SessionInfo> sessionInfos = new HashMap<>(); 32 private Map<Integer, PackageInstaller.Session> sessions = new HashMap<>(); 33 private Set<CallbackInfo> callbackInfos = new HashSet<>(); 34 35 private static class CallbackInfo { 36 PackageInstaller.SessionCallback callback; 37 Handler handler; 38 } 39 40 @Implementation getAllSessions()41 protected List<PackageInstaller.SessionInfo> getAllSessions() { 42 return ImmutableList.copyOf(sessionInfos.values()); 43 } 44 45 @Implementation registerSessionCallback( @onNull PackageInstaller.SessionCallback callback, @NonNull Handler handler)46 protected void registerSessionCallback( 47 @NonNull PackageInstaller.SessionCallback callback, @NonNull Handler handler) { 48 CallbackInfo callbackInfo = new CallbackInfo(); 49 callbackInfo.callback = callback; 50 callbackInfo.handler = handler; 51 this.callbackInfos.add(callbackInfo); 52 } 53 54 @Implementation 55 @Nullable getSessionInfo(int sessionId)56 protected PackageInstaller.SessionInfo getSessionInfo(int sessionId) { 57 return sessionInfos.get(sessionId); 58 } 59 60 @Implementation createSession(@onNull PackageInstaller.SessionParams params)61 protected int createSession(@NonNull PackageInstaller.SessionParams params) throws IOException { 62 final PackageInstaller.SessionInfo sessionInfo = new PackageInstaller.SessionInfo(); 63 sessionInfo.sessionId = nextSessionId++; 64 sessionInfo.active = true; 65 sessionInfo.appPackageName = params.appPackageName; 66 sessionInfos.put(sessionInfo.getSessionId(), sessionInfo); 67 68 for (final CallbackInfo callbackInfo : callbackInfos) { 69 callbackInfo.handler.post(new Runnable() { 70 @Override 71 public void run() { 72 callbackInfo.callback.onCreated(sessionInfo.sessionId); 73 } 74 }); 75 } 76 77 return sessionInfo.sessionId; 78 } 79 80 @Implementation abandonSession(int sessionId)81 protected void abandonSession(int sessionId) { 82 sessionInfos.remove(sessionId); 83 sessions.remove(sessionId); 84 85 for (final CallbackInfo callbackInfo : callbackInfos) { 86 callbackInfo.handler.post(new Runnable() { 87 @Override 88 public void run() { 89 callbackInfo.callback.onFinished(sessionId, false); 90 } 91 }); 92 } 93 } 94 95 @Implementation 96 @NonNull openSession(int sessionId)97 protected PackageInstaller.Session openSession(int sessionId) throws IOException { 98 if (!sessionInfos.containsKey(sessionId)) { 99 throw new SecurityException("Invalid session Id: " + sessionId); 100 } 101 102 PackageInstaller.Session session = new PackageInstaller.Session(null); 103 ShadowSession shadowSession = Shadow.extract(session); 104 shadowSession.setShadowPackageInstaller(sessionId, this); 105 sessions.put(sessionId, session); 106 return session; 107 } 108 setSessionProgress(final int sessionId, final float progress)109 public void setSessionProgress(final int sessionId, final float progress) { 110 for (final CallbackInfo callbackInfo : callbackInfos) { 111 callbackInfo.handler.post(new Runnable() { 112 @Override 113 public void run() { 114 callbackInfo.callback.onProgressChanged(sessionId, progress); 115 } 116 }); 117 } 118 } 119 120 /** 121 * Prefer instead to use the Android APIs to close the session 122 * {@link android.content.pm.PackageInstaller.Session#commit(IntentSender)} 123 */ 124 @Deprecated setSessionSucceeds(int sessionId)125 public void setSessionSucceeds(int sessionId) { 126 setSessionFinishes(sessionId, true); 127 } 128 setSessionFails(int sessionId)129 public void setSessionFails(int sessionId) { 130 setSessionFinishes(sessionId, false); 131 } 132 setSessionFinishes(final int sessionId, final boolean success)133 private void setSessionFinishes(final int sessionId, final boolean success) { 134 for (final CallbackInfo callbackInfo : callbackInfos) { 135 callbackInfo.handler.post(new Runnable() { 136 @Override 137 public void run() { 138 callbackInfo.callback.onFinished(sessionId, success); 139 } 140 }); 141 } 142 143 PackageInstaller.Session session = sessions.get(sessionId); 144 ShadowSession shadowSession = Shadow.extract(session); 145 if (success) { 146 try { 147 shadowSession.statusReceiver 148 .sendIntent(RuntimeEnvironment.application, 0, null, null, null, null); 149 } catch (SendIntentException e) { 150 throw new RuntimeException(e); 151 } 152 } 153 } 154 155 @Implements(value = PackageInstaller.Session.class, minSdk = LOLLIPOP) 156 public static class ShadowSession { 157 158 private OutputStream outputStream; 159 private boolean outputStreamOpen; 160 private IntentSender statusReceiver; 161 private int sessionId; 162 private ShadowPackageInstaller shadowPackageInstaller; 163 164 @Implementation(maxSdk = KITKAT_WATCH) __constructor__()165 protected void __constructor__() {} 166 167 @Implementation 168 @NonNull openWrite(@onNull String name, long offsetBytes, long lengthBytes)169 protected OutputStream openWrite(@NonNull String name, long offsetBytes, long lengthBytes) 170 throws IOException { 171 outputStream = new OutputStream() { 172 @Override 173 public void write(int aByte) throws IOException { 174 175 } 176 177 @Override 178 public void close() throws IOException { 179 outputStreamOpen = false; 180 } 181 }; 182 outputStreamOpen = true; 183 return outputStream; 184 } 185 186 @Implementation fsync(@onNull OutputStream out)187 protected void fsync(@NonNull OutputStream out) throws IOException {} 188 189 @Implementation commit(@onNull IntentSender statusReceiver)190 protected void commit(@NonNull IntentSender statusReceiver) { 191 this.statusReceiver = statusReceiver; 192 if (outputStreamOpen) { 193 throw new SecurityException("OutputStream still open"); 194 } 195 196 shadowPackageInstaller.setSessionSucceeds(sessionId); 197 } 198 199 @Implementation close()200 protected void close() {} 201 202 @Implementation abandon()203 protected void abandon() { 204 shadowPackageInstaller.abandonSession(sessionId); 205 } 206 setShadowPackageInstaller(int sessionId, ShadowPackageInstaller shadowPackageInstaller)207 private void setShadowPackageInstaller(int sessionId, 208 ShadowPackageInstaller shadowPackageInstaller) { 209 this.sessionId = sessionId; 210 this.shadowPackageInstaller = shadowPackageInstaller; 211 } 212 } 213 } 214