blob: 42ccbfb83bfa4728cd6a45fb2d95d9e4c5813e64 [file] [log] [blame]
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.P;
// BEGIN-INTERNAL
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
// END-INTERNAL
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.AppOpsManager;
import android.app.AppOpsManager.AttributedOpEntry;
import android.app.AppOpsManager.OnOpChangedListener;
import android.app.AppOpsManager.OpEntry;
import android.app.AppOpsManager.PackageOps;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.media.AudioAttributes.AttributeUsage;
import android.os.Binder;
import android.os.Build;
import android.util.LongSparseArray;
import android.util.LongSparseLongArray;
import com.android.internal.app.IAppOpsService;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
@Implements(value = AppOpsManager.class)
public class ShadowAppOpsManager {
// OpEntry fields that the shadow doesn't currently allow the test to configure.
private static final long OP_TIME = 1400000000L;
private static final long REJECT_TIME = 0L;
private static final int DURATION = 10;
private static final int PROXY_UID = 0;
private static final String PROXY_PACKAGE = "";
@RealObject private AppOpsManager realObject;
// Recorded operations, keyed by "uid|packageName"
private Multimap<String, Integer> mStoredOps = HashMultimap.create();
// "uid|packageName|opCode" => opMode
private Map<String, Integer> appModeMap = new HashMap<>();
// "packageName|opCode" => listener
private BiMap<String, OnOpChangedListener> appOpListeners = HashBiMap.create();
// op | (usage << 8) => ModeAndExcpetion
private Map<Integer, ModeAndException> audioRestrictions = new HashMap<>();
private Context context;
@Implementation(minSdk = KITKAT)
protected void __constructor__(Context context, IAppOpsService service) {
this.context = context;
invokeConstructor(
AppOpsManager.class,
realObject,
ClassParameter.from(Context.class, context),
ClassParameter.from(IAppOpsService.class, service));
}
/**
* Change the operating mode for the given op in the given app package. You must pass in both the
* uid and name of the application whose mode is being modified; if these do not match, the
* modification will not be applied.
*
* <p>This method is public for testing {@link #checkOpNoThrow}. If {@link #checkOpNoThrow} is
* called afterwards with the {@code op}, {@code ui}, and {@code packageName} provided, it will
* return the {@code mode} set here.
*
* @param op The operation to modify. One of the OPSTR_* constants.
* @param uid The user id of the application whose mode will be changed.
* @param packageName The name of the application package name whose mode will be changed.
*/
@Implementation(minSdk = P)
@HiddenApi
@SystemApi
@RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES)
public void setMode(String op, int uid, String packageName, int mode) {
setMode(AppOpsManager.strOpToOp(op), uid, packageName, mode);
}
/**
* Int version of {@link #setMode(String, int, String, int)}.
*
* <p>This method is public for testing {@link #checkOpNoThrow}. If {@link #checkOpNoThrow} is *
* called afterwards with the {@code op}, {@code ui}, and {@code packageName} provided, it will *
* return the {@code mode} set here.
*/
@Implementation(minSdk = KITKAT)
@HiddenApi
@RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES)
public void setMode(int op, int uid, String packageName, int mode) {
Integer oldMode = appModeMap.put(getOpMapKey(uid, packageName, op), mode);
OnOpChangedListener listener = appOpListeners.get(getListenerKey(op, packageName));
if (listener != null && !Objects.equals(oldMode, mode)) {
String[] sOpToString = ReflectionHelpers.getStaticField(AppOpsManager.class, "sOpToString");
listener.onOpChanged(sOpToString[op], packageName);
}
}
// BEGIN-INTERNAL
@Implementation(minSdk = Q)
public int unsafeCheckOpNoThrow(String op, int uid, String packageName) {
return checkOpNoThrow(AppOpsManager.strOpToOp(op), uid, packageName);
}
// END-INTERNAL
/**
* Like {@link #unsafeCheckOpNoThrow(String, int, String)} but returns the <em>raw</em> mode
* associated with the op. Does not throw a security exception, does not translate {@link
* AppOpsManager#MODE_FOREGROUND}.
*/
@Implementation(minSdk = Q)
protected int unsafeCheckOpRawNoThrow(String op, int uid, String packageName) {
return unsafeCheckOpRawNoThrow(AppOpsManager.strOpToOp(op), uid, packageName);
}
/**
* Returns the <em>raw</em> mode associated with the op.
* Does not throw a security exception, does not translate {@link AppOpsManager#MODE_FOREGROUND}.
*/
@Implementation(minSdk = Q)
protected int unsafeCheckOpRawNoThrow(int op, int uid, String packageName) {
Integer mode = appModeMap.get(getOpMapKey(uid, packageName, op));
if (mode == null) {
return AppOpsManager.MODE_ALLOWED;
}
return mode;
}
@Implementation(minSdk = P)
@Deprecated // renamed to unsafeCheckOpNoThrow
protected int checkOpNoThrow(String op, int uid, String packageName) {
return checkOpNoThrow(AppOpsManager.strOpToOp(op), uid, packageName);
}
/**
* Like {@link AppOpsManager#checkOp} but instead of throwing a {@link SecurityException} it
* returns {@link AppOpsManager#MODE_ERRORED}.
*
* <p>Made public for testing {@link #setMode} as the method is {@coe @hide}.
*/
@Implementation(minSdk = KITKAT)
@HiddenApi
public int checkOpNoThrow(int op, int uid, String packageName) {
Integer mode = appModeMap.get(getOpMapKey(uid, packageName, op));
if (mode == null) {
return AppOpsManager.MODE_ALLOWED;
}
return mode;
}
@Implementation(minSdk = KITKAT, maxSdk = Q)
public int noteOp(int op, int uid, String packageName) {
mStoredOps.put(getInternalKey(uid, packageName), op);
// Permission check not currently implemented in this shadow.
return AppOpsManager.MODE_ALLOWED;
}
@Implementation(minSdk = R)
@HiddenApi
public int noteOp(int op, int uid, String packageName, String message) {
mStoredOps.put(getInternalKey(uid, packageName), op);
// Permission check not currently implemented in this shadow.
return AppOpsManager.MODE_ALLOWED;
}
@Implementation(minSdk = M, maxSdk = Q)
@HiddenApi
protected int noteProxyOpNoThrow(int op, String proxiedPackageName) {
mStoredOps.put(getInternalKey(Binder.getCallingUid(), proxiedPackageName), op);
return checkOpNoThrow(op, Binder.getCallingUid(), proxiedPackageName);
}
@Implementation(minSdk = R)
@HiddenApi
protected int noteProxyOpNoThrow(int op, String proxiedPackageName, int proxiedUid,
String featureId, String message) {
if (featureId != null) {
throw new RuntimeException("non null featureIds are not supported by Robolectric yet");
}
mStoredOps.put(getInternalKey(proxiedUid, proxiedPackageName), op);
return checkOpNoThrow(op, proxiedUid, proxiedPackageName);
}
@Implementation(minSdk = KITKAT)
@HiddenApi
public List<PackageOps> getOpsForPackage(int uid, String packageName, int[] ops) {
Set<Integer> opFilter = new HashSet<>();
if (ops != null) {
for (int op : ops) {
opFilter.add(op);
}
}
List<OpEntry> opEntries = new ArrayList<>();
for (Integer op : mStoredOps.get(getInternalKey(uid, packageName))) {
if (opFilter.isEmpty() || opFilter.contains(op)) {
opEntries.add(toOpEntry(op));
}
}
return ImmutableList.of(new PackageOps(packageName, uid, opEntries));
}
@Implementation(minSdk = KITKAT)
protected void checkPackage(int uid, String packageName) {
try {
// getPackageUid was introduced in API 24, so we call it on the shadow class
ShadowApplicationPackageManager shadowApplicationPackageManager =
Shadow.extract(context.getPackageManager());
int packageUid = shadowApplicationPackageManager.getPackageUid(packageName, 0);
if (packageUid == uid) {
return;
}
throw new SecurityException("Package " + packageName + " belongs to " + packageUid);
} catch (NameNotFoundException e) {
throw new SecurityException("Package " + packageName + " doesn't belong to " + uid, e);
}
}
/**
* Sets audio restrictions.
*
* <p>This method is public for testing, as the original method is {@code @hide}.
*/
@Implementation(minSdk = LOLLIPOP)
@HiddenApi
public void setRestriction(
int code, @AttributeUsage int usage, int mode, String[] exceptionPackages) {
audioRestrictions.put(
getAudioRestrictionKey(code, usage), new ModeAndException(mode, exceptionPackages));
}
@Nullable
public ModeAndException getRestriction(int code, @AttributeUsage int usage) {
// this gives us room for 256 op_codes. There are 78 as of P.
return audioRestrictions.get(getAudioRestrictionKey(code, usage));
}
@Implementation(minSdk = KITKAT)
@HiddenApi
@RequiresPermission(value = android.Manifest.permission.WATCH_APPOPS)
protected void startWatchingMode(int op, String packageName, OnOpChangedListener callback) {
appOpListeners.put(getListenerKey(op, packageName), callback);
}
@Implementation(minSdk = KITKAT)
@RequiresPermission(value = android.Manifest.permission.WATCH_APPOPS)
protected void stopWatchingMode(OnOpChangedListener callback) {
appOpListeners.inverse().remove(callback);
}
private static OpEntry toOpEntry(Integer op) {
if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.M) {
return ReflectionHelpers.callConstructor(
OpEntry.class,
ClassParameter.from(int.class, op),
ClassParameter.from(int.class, AppOpsManager.MODE_ALLOWED),
ClassParameter.from(long.class, OP_TIME),
ClassParameter.from(long.class, REJECT_TIME),
ClassParameter.from(int.class, DURATION));
// BEGIN-INTERNAL
} else if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.P) {
return ReflectionHelpers.callConstructor(
OpEntry.class,
ClassParameter.from(int.class, op),
ClassParameter.from(int.class, AppOpsManager.MODE_ALLOWED),
ClassParameter.from(long.class, OP_TIME),
ClassParameter.from(long.class, REJECT_TIME),
ClassParameter.from(int.class, DURATION),
ClassParameter.from(int.class, PROXY_UID),
ClassParameter.from(String.class, PROXY_PACKAGE));
}
final long key = AppOpsManager.makeKey(AppOpsManager.UID_STATE_TOP,
AppOpsManager.OP_FLAG_SELF);
final LongSparseLongArray accessTimes = new LongSparseLongArray();
accessTimes.put(key, OP_TIME);
final LongSparseLongArray rejectTimes = new LongSparseLongArray();
rejectTimes.put(key, REJECT_TIME);
final LongSparseLongArray durations = new LongSparseLongArray();
durations.put(key, DURATION);
final LongSparseLongArray proxyUids = new LongSparseLongArray();
proxyUids.put(key, PROXY_UID);
final LongSparseArray<String> proxyPackages = new LongSparseArray<>();
proxyPackages.put(key, PROXY_PACKAGE);
if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.Q) {
return ReflectionHelpers.callConstructor(
OpEntry.class,
ClassParameter.from(int.class, op),
ClassParameter.from(boolean.class, false),
ClassParameter.from(int.class, AppOpsManager.MODE_ALLOWED),
ClassParameter.from(LongSparseLongArray.class, accessTimes),
ClassParameter.from(LongSparseLongArray.class, durations),
ClassParameter.from(LongSparseLongArray.class, rejectTimes),
ClassParameter.from(LongSparseLongArray.class, proxyUids),
ClassParameter.from(LongSparseArray.class, proxyPackages));
}
LongSparseArray<AppOpsManager.NoteOpEvent> accessEvents = new LongSparseArray<>();
LongSparseArray<AppOpsManager.NoteOpEvent> rejectEvents = new LongSparseArray<>();
accessEvents.put(key, new AppOpsManager.NoteOpEvent(OP_TIME, DURATION,
new AppOpsManager.OpEventProxyInfo(PROXY_UID, PROXY_PACKAGE, null)));
rejectEvents.put(key, new AppOpsManager.NoteOpEvent(REJECT_TIME, -1, null));
return new OpEntry(op, AppOpsManager.MODE_ALLOWED, Collections.singletonMap(null,
new AttributedOpEntry(op, false, accessEvents, rejectEvents)));
// END-INTERNAL
}
private static String getInternalKey(int uid, String packageName) {
return uid + "|" + packageName;
}
private static String getOpMapKey(int uid, String packageName, int opInt) {
return String.format("%s|%s|%s", uid, packageName, opInt);
}
private static int getAudioRestrictionKey(int code, @AttributeUsage int usage) {
return code | (usage << 8);
}
private static String getListenerKey(int op, String packageName) {
return String.format("%s|%s", op, packageName);
}
/** Class holding usage mode and excpetion packages. */
public static class ModeAndException {
public final int mode;
public final List<String> exceptionPackages;
public ModeAndException(int mode, String[] exceptionPackages) {
this.mode = mode;
this.exceptionPackages =
exceptionPackages == null
? Collections.emptyList()
: Collections.unmodifiableList(Arrays.asList(exceptionPackages));
}
}
}