1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.KITKAT; 4 import static android.os.Build.VERSION_CODES.LOLLIPOP; 5 import static android.os.Build.VERSION_CODES.M; 6 import static android.os.Build.VERSION_CODES.P; 7 // BEGIN-INTERNAL 8 import static android.os.Build.VERSION_CODES.Q; 9 // END-INTERNAL 10 import static org.robolectric.shadow.api.Shadow.invokeConstructor; 11 12 import android.annotation.Nullable; 13 import android.annotation.RequiresPermission; 14 import android.annotation.SystemApi; 15 import android.app.AppOpsManager; 16 import android.app.AppOpsManager.OnOpChangedListener; 17 import android.app.AppOpsManager.OpEntry; 18 import android.app.AppOpsManager.PackageOps; 19 import android.content.Context; 20 import android.content.pm.PackageManager.NameNotFoundException; 21 import android.media.AudioAttributes.AttributeUsage; 22 import android.os.Binder; 23 import android.os.Build; 24 import android.util.LongSparseArray; 25 import android.util.LongSparseLongArray; 26 import com.android.internal.app.IAppOpsService; 27 import com.google.common.collect.BiMap; 28 import com.google.common.collect.HashBiMap; 29 import com.google.common.collect.HashMultimap; 30 import com.google.common.collect.ImmutableList; 31 import com.google.common.collect.Multimap; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.Collections; 35 import java.util.HashMap; 36 import java.util.HashSet; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Objects; 40 import java.util.Set; 41 import org.robolectric.RuntimeEnvironment; 42 import org.robolectric.annotation.HiddenApi; 43 import org.robolectric.annotation.Implementation; 44 import org.robolectric.annotation.Implements; 45 import org.robolectric.annotation.RealObject; 46 import org.robolectric.shadow.api.Shadow; 47 import org.robolectric.util.ReflectionHelpers; 48 import org.robolectric.util.ReflectionHelpers.ClassParameter; 49 50 @Implements(value = AppOpsManager.class) 51 public class ShadowAppOpsManager { 52 53 // OpEntry fields that the shadow doesn't currently allow the test to configure. 54 private static final long OP_TIME = 1400000000L; 55 private static final long REJECT_TIME = 0L; 56 private static final int DURATION = 10; 57 private static final int PROXY_UID = 0; 58 private static final String PROXY_PACKAGE = ""; 59 60 @RealObject private AppOpsManager realObject; 61 62 // Recorded operations, keyed by "uid|packageName" 63 private Multimap<String, Integer> mStoredOps = HashMultimap.create(); 64 // "uid|packageName|opCode" => opMode 65 private Map<String, Integer> appModeMap = new HashMap<>(); 66 67 // "packageName|opCode" => listener 68 private BiMap<String, OnOpChangedListener> appOpListeners = HashBiMap.create(); 69 70 // op | (usage << 8) => ModeAndExcpetion 71 private Map<Integer, ModeAndException> audioRestrictions = new HashMap<>(); 72 73 private Context context; 74 75 @Implementation(minSdk = KITKAT) 76 protected void __constructor__(Context context, IAppOpsService service) { 77 this.context = context; 78 invokeConstructor( 79 AppOpsManager.class, 80 realObject, 81 ClassParameter.from(Context.class, context), 82 ClassParameter.from(IAppOpsService.class, service)); 83 } 84 85 /** 86 * Change the operating mode for the given op in the given app package. You must pass in both the 87 * uid and name of the application whose mode is being modified; if these do not match, the 88 * modification will not be applied. 89 * 90 * <p>This method is public for testing {@link #checkOpNoThrow}. If {@link #checkOpNoThrow} is 91 * called afterwards with the {@code op}, {@code ui}, and {@code packageName} provided, it will 92 * return the {@code mode} set here. 93 * 94 * @param op The operation to modify. One of the OPSTR_* constants. 95 * @param uid The user id of the application whose mode will be changed. 96 * @param packageName The name of the application package name whose mode will be changed. 97 */ 98 @Implementation(minSdk = P) 99 @HiddenApi 100 @SystemApi 101 @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) 102 public void setMode(String op, int uid, String packageName, int mode) { 103 setMode(AppOpsManager.strOpToOp(op), uid, packageName, mode); 104 } 105 106 /** 107 * Int version of {@link #setMode(String, int, String, int)}. 108 * 109 * <p>This method is public for testing {@link #checkOpNoThrow}. If {@link #checkOpNoThrow} is * 110 * called afterwards with the {@code op}, {@code ui}, and {@code packageName} provided, it will * 111 * return the {@code mode} set here. 112 */ 113 @Implementation(minSdk = KITKAT) 114 @HiddenApi 115 @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) 116 public void setMode(int op, int uid, String packageName, int mode) { 117 Integer oldMode = appModeMap.put(getOpMapKey(uid, packageName, op), mode); 118 OnOpChangedListener listener = appOpListeners.get(getListenerKey(op, packageName)); 119 if (listener != null && !Objects.equals(oldMode, mode)) { 120 String[] sOpToString = ReflectionHelpers.getStaticField(AppOpsManager.class, "sOpToString"); 121 listener.onOpChanged(sOpToString[op], packageName); 122 } 123 } 124 125 // BEGIN-INTERNAL 126 @Implementation(minSdk = Q) 127 public int unsafeCheckOpNoThrow(String op, int uid, String packageName) { 128 return checkOpNoThrow(AppOpsManager.strOpToOp(op), uid, packageName); 129 } 130 // END-INTERNAL 131 132 @Implementation(minSdk = P) 133 @Deprecated // renamed to unsafeCheckOpNoThrow 134 protected int checkOpNoThrow(String op, int uid, String packageName) { 135 return checkOpNoThrow(AppOpsManager.strOpToOp(op), uid, packageName); 136 } 137 138 /** 139 * Like {@link AppOpsManager#checkOp} but instead of throwing a {@link SecurityException} it 140 * returns {@link AppOpsManager#MODE_ERRORED}. 141 * 142 * <p>Made public for testing {@link #setMode} as the method is {@coe @hide}. 143 */ 144 @Implementation(minSdk = KITKAT) 145 @HiddenApi 146 public int checkOpNoThrow(int op, int uid, String packageName) { 147 Integer mode = appModeMap.get(getOpMapKey(uid, packageName, op)); 148 if (mode == null) { 149 return AppOpsManager.MODE_ALLOWED; 150 } 151 return mode; 152 } 153 154 @Implementation(minSdk = KITKAT) 155 public int noteOp(int op, int uid, String packageName) { 156 mStoredOps.put(getInternalKey(uid, packageName), op); 157 158 // Permission check not currently implemented in this shadow. 159 return AppOpsManager.MODE_ALLOWED; 160 } 161 162 @Implementation(minSdk = M) 163 @HiddenApi 164 protected int noteProxyOpNoThrow(int op, String proxiedPackageName) { 165 mStoredOps.put(getInternalKey(Binder.getCallingUid(), proxiedPackageName), op); 166 return checkOpNoThrow(op, Binder.getCallingUid(), proxiedPackageName); 167 } 168 169 @Implementation(minSdk = KITKAT) 170 @HiddenApi 171 public List<PackageOps> getOpsForPackage(int uid, String packageName, int[] ops) { 172 Set<Integer> opFilter = new HashSet<>(); 173 if (ops != null) { 174 for (int op : ops) { 175 opFilter.add(op); 176 } 177 } 178 179 List<OpEntry> opEntries = new ArrayList<>(); 180 for (Integer op : mStoredOps.get(getInternalKey(uid, packageName))) { 181 if (opFilter.isEmpty() || opFilter.contains(op)) { 182 opEntries.add(toOpEntry(op)); 183 } 184 } 185 186 return ImmutableList.of(new PackageOps(packageName, uid, opEntries)); 187 } 188 189 @Implementation(minSdk = KITKAT) 190 protected void checkPackage(int uid, String packageName) { 191 try { 192 // getPackageUid was introduced in API 24, so we call it on the shadow class 193 ShadowApplicationPackageManager shadowApplicationPackageManager = 194 Shadow.extract(context.getPackageManager()); 195 int packageUid = shadowApplicationPackageManager.getPackageUid(packageName, 0); 196 if (packageUid == uid) { 197 return; 198 } 199 throw new SecurityException("Package " + packageName + " belongs to " + packageUid); 200 } catch (NameNotFoundException e) { 201 throw new SecurityException("Package " + packageName + " doesn't belong to " + uid, e); 202 } 203 } 204 205 /** 206 * Sets audio restrictions. 207 * 208 * <p>This method is public for testing, as the original method is {@code @hide}. 209 */ 210 @Implementation(minSdk = LOLLIPOP) 211 @HiddenApi 212 public void setRestriction( 213 int code, @AttributeUsage int usage, int mode, String[] exceptionPackages) { 214 audioRestrictions.put( 215 getAudioRestrictionKey(code, usage), new ModeAndException(mode, exceptionPackages)); 216 } 217 218 @Nullable 219 public ModeAndException getRestriction(int code, @AttributeUsage int usage) { 220 // this gives us room for 256 op_codes. There are 78 as of P. 221 return audioRestrictions.get(getAudioRestrictionKey(code, usage)); 222 } 223 224 @Implementation(minSdk = KITKAT) 225 @HiddenApi 226 @RequiresPermission(value = android.Manifest.permission.WATCH_APPOPS) 227 protected void startWatchingMode(int op, String packageName, OnOpChangedListener callback) { 228 appOpListeners.put(getListenerKey(op, packageName), callback); 229 } 230 231 @Implementation(minSdk = KITKAT) 232 @RequiresPermission(value = android.Manifest.permission.WATCH_APPOPS) 233 protected void stopWatchingMode(OnOpChangedListener callback) { 234 appOpListeners.inverse().remove(callback); 235 } 236 237 private static OpEntry toOpEntry(Integer op) { 238 if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.M) { 239 return ReflectionHelpers.callConstructor( 240 OpEntry.class, 241 ClassParameter.from(int.class, op), 242 ClassParameter.from(int.class, AppOpsManager.MODE_ALLOWED), 243 ClassParameter.from(long.class, OP_TIME), 244 ClassParameter.from(long.class, REJECT_TIME), 245 ClassParameter.from(int.class, DURATION)); 246 // BEGIN-INTERNAL 247 } else if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.P) { 248 return ReflectionHelpers.callConstructor( 249 OpEntry.class, 250 ClassParameter.from(int.class, op), 251 ClassParameter.from(int.class, AppOpsManager.MODE_ALLOWED), 252 ClassParameter.from(long.class, OP_TIME), 253 ClassParameter.from(long.class, REJECT_TIME), 254 ClassParameter.from(int.class, DURATION), 255 ClassParameter.from(int.class, PROXY_UID), 256 ClassParameter.from(String.class, PROXY_PACKAGE)); 257 } 258 259 final long key = AppOpsManager.makeKey(AppOpsManager.UID_STATE_TOP, 260 AppOpsManager.OP_FLAG_SELF); 261 262 final LongSparseLongArray accessTimes = new LongSparseLongArray(); 263 accessTimes.put(key, OP_TIME); 264 265 final LongSparseLongArray rejectTimes = new LongSparseLongArray(); 266 rejectTimes.put(key, REJECT_TIME); 267 268 final LongSparseLongArray durations = new LongSparseLongArray(); 269 durations.put(key, DURATION); 270 271 final LongSparseLongArray proxyUids = new LongSparseLongArray(); 272 proxyUids.put(key, PROXY_UID); 273 274 final LongSparseArray<String> proxyPackages = new LongSparseArray<>(); 275 proxyPackages.put(key, PROXY_PACKAGE); 276 277 return new OpEntry(op, false, AppOpsManager.MODE_ALLOWED, accessTimes, 278 durations, rejectTimes, proxyUids, proxyPackages); 279 // END-INTERNAL 280 } 281 282 private static String getInternalKey(int uid, String packageName) { 283 return uid + "|" + packageName; 284 } 285 286 private static String getOpMapKey(int uid, String packageName, int opInt) { 287 return String.format("%s|%s|%s", uid, packageName, opInt); 288 } 289 290 private static int getAudioRestrictionKey(int code, @AttributeUsage int usage) { 291 return code | (usage << 8); 292 } 293 294 private static String getListenerKey(int op, String packageName) { 295 return String.format("%s|%s", op, packageName); 296 } 297 298 /** Class holding usage mode and excpetion packages. */ 299 public static class ModeAndException { 300 public final int mode; 301 public final List<String> exceptionPackages; 302 303 public ModeAndException(int mode, String[] exceptionPackages) { 304 this.mode = mode; 305 this.exceptionPackages = 306 exceptionPackages == null 307 ? Collections.emptyList() 308 : Collections.unmodifiableList(Arrays.asList(exceptionPackages)); 309 } 310 } 311 } 312