All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowAppOpsManager Maven / Gradle / Ivy

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;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.TIRAMISU;
import static java.util.stream.Collectors.toSet;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.annotation.NonNull;
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.NoteOpEvent;
import android.app.AppOpsManager.OnOpChangedListener;
import android.app.AppOpsManager.OpEntry;
import android.app.AppOpsManager.OpEventProxyInfo;
import android.app.AppOpsManager.PackageOps;
import android.app.SyncNotedAppOp;
import android.content.AttributionSource;
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.ArrayMap;
import android.util.LongSparseArray;
import android.util.LongSparseLongArray;
import androidx.annotation.RequiresApi;
import com.android.internal.app.IAppOpsService;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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 java.util.stream.IntStream;
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.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;

/** Shadow for {@link AppOpsManager}. */
@Implements(value = AppOpsManager.class, minSdk = KITKAT, looseSignatures = true)
public class ShadowAppOpsManager {

  // OpEntry fields that the shadow doesn't currently allow the test to configure.
  protected static final long OP_TIME = 1400000000L;
  protected static final long REJECT_TIME = 0L;
  protected static final int DURATION = 10;
  protected static final int PROXY_UID = 0;
  protected static final String PROXY_PACKAGE = "";

  @RealObject private AppOpsManager realObject;

  private static boolean staticallyInitialized = false;

  // Recorded operations, keyed by (uid, packageName)
  private final Multimap storedOps = HashMultimap.create();
  // (uid, packageName, opCode) => opMode
  private final Map appModeMap = new HashMap<>();

  // (uid, packageName, opCode)
  private final Set longRunningOp = new HashSet<>();

  private final Map> appOpListeners = new ArrayMap<>();

  // op | (usage << 8) => ModeAndExcpetion
  private final Map audioRestrictions = new HashMap<>();

  private Context context;

  @Implementation
  protected void __constructor__(Context context, IAppOpsService service) {
    this.context = context;
    invokeConstructor(
        AppOpsManager.class,
        realObject,
        ClassParameter.from(Context.class, context),
        ClassParameter.from(IAppOpsService.class, service));
  }

  @Implementation
  protected static void __staticInitializer__() {
    staticallyInitialized = true;
    Shadow.directInitialize(AppOpsManager.class);
  }

  /**
   * 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.
   *
   * 

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)}. * *

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 @HiddenApi public void setMode(int op, int uid, String packageName, int mode) { Integer oldMode = appModeMap.put(Key.create(uid, packageName, op), mode); if (Objects.equals(oldMode, mode)) { return; } for (Map.Entry> entry : appOpListeners.entrySet()) { for (Key key : entry.getValue()) { if (op == key.getOpCode() && (key.getPackageName() == null || key.getPackageName().equals(packageName))) { entry.getKey().onOpChanged(getOpString(op), packageName); } } } } protected String getOpString(int opCode) { if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) { String[] sOpToString = ReflectionHelpers.getStaticField(AppOpsManager.class, "sOpToString"); return sOpToString[opCode]; } else { Object[] sAppOpInfos = ReflectionHelpers.getStaticField(AppOpsManager.class, "sAppOpInfos"); return reflector(AppOpInfoReflector.class, sAppOpInfos[opCode]).getName(); } } /** * Returns app op details for all packages for which one of {@link #setMode} methods was used to * set the value of one of the given app ops (it does return those set to 'default' mode, while * the true implementation usually doesn't). Also, we don't enforce any permission checks which * might be needed in the true implementation. * * @param ops The set of operations you are interested in, or null if you want all of them. * @return app ops information about each package, containing only ops that were specified as an * argument */ @Implementation(minSdk = Q) @HiddenApi @SystemApi @NonNull protected List getPackagesForOps(@Nullable String[] ops) { List result = null; if (ops == null) { int[] intOps = null; result = getPackagesForOps(intOps); } else { List intOpsList = new ArrayList<>(); for (String op : ops) { intOpsList.add(AppOpsManager.strOpToOp(op)); } result = getPackagesForOps(intOpsList.stream().mapToInt(i -> i).toArray()); } return result != null ? result : new ArrayList<>(); } /** * Returns app op details for all packages for which one of {@link #setMode} methods was used to * set the value of one of the given app ops (it does return those set to 'default' mode, while * the true implementation usually doesn't). Also, we don't enforce any permission checks which * might be needed in the true implementation. * * @param ops The set of operations you are interested in, or null if you want all of them. * @return app ops information about each package, containing only ops that were specified as an * argument */ @Implementation @HiddenApi protected List getPackagesForOps(int[] ops) { Set relevantOps; if (ops != null) { relevantOps = IntStream.of(ops).boxed().collect(toSet()); } else { relevantOps = new HashSet<>(); } // Aggregating op data per each package. // (uid, packageName) => [(op, mode)] Multimap perPackageMap = MultimapBuilder.hashKeys().hashSetValues().build(); for (Map.Entry appOpInfo : appModeMap.entrySet()) { Key key = appOpInfo.getKey(); if (ops == null || relevantOps.contains(key.getOpCode())) { Key packageKey = Key.create(key.getUid(), key.getPackageName(), null); OpEntry opEntry = toOpEntry(key.getOpCode(), appOpInfo.getValue()); perPackageMap.put(packageKey, opEntry); } } List result = new ArrayList<>(); // Creating resulting PackageOps objects using all op info collected per package. for (Map.Entry> packageInfo : perPackageMap.asMap().entrySet()) { Key key = packageInfo.getKey(); result.add( new PackageOps( key.getPackageName(), key.getUid(), new ArrayList<>(packageInfo.getValue()))); } return result.isEmpty() ? null : result; } @Implementation(minSdk = Q) public int unsafeCheckOpNoThrow(String op, int uid, String packageName) { return checkOpNoThrow(AppOpsManager.strOpToOp(op), uid, packageName); } @Implementation(minSdk = R) protected int unsafeCheckOpRawNoThrow(int op, int uid, String packageName) { Integer mode = appModeMap.get(Key.create(uid, packageName, op)); if (mode == null) { return AppOpsManager.MODE_ALLOWED; } return mode; } /** * Like {@link #unsafeCheckOpNoThrow(String, int, String)} but returns the raw mode * associated with the op. Does not throw a security exception, does not translate {@link * AppOpsManager#MODE_FOREGROUND}. */ @Implementation(minSdk = Q) public int unsafeCheckOpRawNoThrow(String op, int uid, String packageName) { return unsafeCheckOpRawNoThrow(AppOpsManager.strOpToOp(op), uid, packageName); } /** Stores a fake long-running operation. It does not throw if a wrong uid is passed. */ @Implementation(minSdk = R) protected int startOp( String op, int uid, String packageName, String attributionTag, String message) { int mode = unsafeCheckOpRawNoThrow(op, uid, packageName); if (mode == AppOpsManager.MODE_ALLOWED) { longRunningOp.add(Key.create(uid, packageName, AppOpsManager.strOpToOp(op))); } return mode; } /** Stores a fake long-running operation. It does not throw if a wrong uid is passed. */ @Implementation(minSdk = KITKAT, maxSdk = Q) protected int startOpNoThrow(int op, int uid, String packageName) { int mode = unsafeCheckOpRawNoThrow(op, uid, packageName); if (mode == AppOpsManager.MODE_ALLOWED) { longRunningOp.add(Key.create(uid, packageName, op)); } return mode; } /** Stores a fake long-running operation. It does not throw if a wrong uid is passed. */ @Implementation(minSdk = R) protected int startOpNoThrow( String op, int uid, String packageName, String attributionTag, String message) { int mode = unsafeCheckOpRawNoThrow(op, uid, packageName); if (mode == AppOpsManager.MODE_ALLOWED) { longRunningOp.add(Key.create(uid, packageName, AppOpsManager.strOpToOp(op))); } return mode; } /** Removes a fake long-running operation from the set. */ @Implementation(maxSdk = Q) protected void finishOp(int op, int uid, String packageName) { longRunningOp.remove(Key.create(uid, packageName, op)); } /** Removes a fake long-running operation from the set. */ @Implementation(minSdk = R) protected void finishOp(String op, int uid, String packageName, String attributionTag) { longRunningOp.remove(Key.create(uid, packageName, AppOpsManager.strOpToOp(op))); } /** Checks whether op was previously set using {@link #setMode} */ @Implementation(minSdk = R) protected int checkOp(String op, int uid, String packageName) { return checkOpNoThrow(op, uid, packageName); } /** * Checks whether the given op is active, i.e. did someone call {@link #startOp(String, int, * String, String, String)} without {@link #finishOp(String, int, String, String)} yet. */ @Implementation(minSdk = R) public boolean isOpActive(String op, int uid, String packageName) { return longRunningOp.contains(Key.create(uid, packageName, AppOpsManager.strOpToOp(op))); } @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}. * *

Made public for testing {@link #setMode} as the method is {@code @hide}. */ @Implementation @HiddenApi public int checkOpNoThrow(int op, int uid, String packageName) { int mode = unsafeCheckOpRawNoThrow(op, uid, packageName); return mode == AppOpsManager.MODE_FOREGROUND ? AppOpsManager.MODE_ALLOWED : mode; } @Implementation public int noteOp(int op, int uid, String packageName) { return noteOpInternal(op, uid, packageName, "", ""); } private int noteOpInternal( int op, int uid, String packageName, String attributionTag, String message) { storedOps.put(Key.create(uid, packageName, null), op); if (RuntimeEnvironment.getApiLevel() >= R) { Object lock = ReflectionHelpers.getStaticField(AppOpsManager.class, "sLock"); synchronized (lock) { AppOpsManager.OnOpNotedCallback callback = ReflectionHelpers.getStaticField(AppOpsManager.class, "sOnOpNotedCallback"); if (callback != null) { callback.onSelfNoted(new SyncNotedAppOp(op, attributionTag)); } } } // Permission check not currently implemented in this shadow. return AppOpsManager.MODE_ALLOWED; } @Implementation(minSdk = R) protected int noteOp(int op, int uid, String packageName, String attributionTag, String message) { return noteOpInternal(op, uid, packageName, attributionTag, message); } @Implementation protected int noteOpNoThrow(int op, int uid, String packageName) { storedOps.put(Key.create(uid, packageName, null), op); return checkOpNoThrow(op, uid, packageName); } @Implementation(minSdk = R) protected int noteOpNoThrow( int op, int uid, @Nullable String packageName, @Nullable String attributionTag, @Nullable String message) { return noteOpNoThrow(op, uid, packageName); } @Implementation(minSdk = M, maxSdk = Q) @HiddenApi protected int noteProxyOpNoThrow(int op, String proxiedPackageName) { storedOps.put(Key.create(Binder.getCallingUid(), proxiedPackageName, null), op); return checkOpNoThrow(op, Binder.getCallingUid(), proxiedPackageName); } @Implementation(minSdk = Q, maxSdk = Q) @HiddenApi protected int noteProxyOpNoThrow(int op, String proxiedPackageName, int proxiedUid) { storedOps.put(Key.create(proxiedUid, proxiedPackageName, null), op); return checkOpNoThrow(op, proxiedUid, proxiedPackageName); } @Implementation(minSdk = R, maxSdk = R) @HiddenApi protected int noteProxyOpNoThrow( int op, String proxiedPackageName, int proxiedUid, String proxiedAttributionTag, String message) { storedOps.put(Key.create(proxiedUid, proxiedPackageName, null), op); return checkOpNoThrow(op, proxiedUid, proxiedPackageName); } @RequiresApi(api = S) @Implementation(minSdk = S) protected int noteProxyOpNoThrow( Object op, Object attributionSource, Object message, Object ignoredSkipProxyOperation) { Preconditions.checkArgument(op instanceof Integer); Preconditions.checkArgument(attributionSource instanceof AttributionSource); Preconditions.checkArgument(message == null || message instanceof String); Preconditions.checkArgument(ignoredSkipProxyOperation instanceof Boolean); AttributionSource castedAttributionSource = (AttributionSource) attributionSource; return noteProxyOpNoThrow( (int) op, castedAttributionSource.getNextPackageName(), castedAttributionSource.getNextUid(), castedAttributionSource.getNextAttributionTag(), (String) message); } @Implementation @HiddenApi public List getOpsForPackage(int uid, String packageName, int[] ops) { Set opFilter = new HashSet<>(); if (ops != null) { for (int op : ops) { opFilter.add(op); } } List opEntries = new ArrayList<>(); for (Integer op : storedOps.get(Key.create(uid, packageName, null))) { if (opFilter.isEmpty() || opFilter.contains(op)) { opEntries.add(toOpEntry(op, AppOpsManager.MODE_ALLOWED)); } } return ImmutableList.of(new PackageOps(packageName, uid, opEntries)); } @Implementation(minSdk = Q) @HiddenApi @SystemApi @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) protected List getOpsForPackage(int uid, String packageName, String[] ops) { if (ops == null) { int[] intOps = null; return getOpsForPackage(uid, packageName, intOps); } Map strOpToIntOp = ReflectionHelpers.getStaticField(AppOpsManager.class, "sOpStrToOp"); List intOpsList = new ArrayList<>(); for (String op : ops) { Integer intOp = strOpToIntOp.get(op); if (intOp != null) { intOpsList.add(intOp); } } return getOpsForPackage(uid, packageName, intOpsList.stream().mapToInt(i -> i).toArray()); } @Implementation 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. * *

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 protected void startWatchingMode(int op, String packageName, OnOpChangedListener callback) { startWatchingModeImpl(op, packageName, 0, callback); } @Implementation(minSdk = Q) protected void startWatchingMode( int op, String packageName, int flags, OnOpChangedListener callback) { startWatchingModeImpl(op, packageName, flags, callback); } private void startWatchingModeImpl( int op, String packageName, int flags, OnOpChangedListener callback) { Set keys = appOpListeners.get(callback); if (keys == null) { keys = new HashSet<>(); appOpListeners.put(callback, keys); } keys.add(Key.create(null, packageName, op)); } @Implementation protected void stopWatchingMode(OnOpChangedListener callback) { appOpListeners.remove(callback); } protected OpEntry toOpEntry(Integer op, int mode) { if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.M) { return ReflectionHelpers.callConstructor( OpEntry.class, ClassParameter.from(int.class, op), ClassParameter.from(int.class, mode), ClassParameter.from(long.class, OP_TIME), ClassParameter.from(long.class, REJECT_TIME), ClassParameter.from(int.class, DURATION)); } else if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.Q) { return ReflectionHelpers.callConstructor( OpEntry.class, ClassParameter.from(int.class, op), ClassParameter.from(int.class, mode), 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)); } else if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.R) { 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 proxyPackages = new LongSparseArray<>(); proxyPackages.put(key, PROXY_PACKAGE); return ReflectionHelpers.callConstructor( OpEntry.class, ClassParameter.from(int.class, op), ClassParameter.from(boolean.class, false), ClassParameter.from(int.class, mode), ClassParameter.from(LongSparseLongArray.class, accessTimes), ClassParameter.from(LongSparseLongArray.class, rejectTimes), ClassParameter.from(LongSparseLongArray.class, durations), ClassParameter.from(LongSparseLongArray.class, proxyUids), ClassParameter.from(LongSparseArray.class, proxyPackages)); } else { final long key = AppOpsManager.makeKey(AppOpsManager.UID_STATE_TOP, AppOpsManager.OP_FLAG_SELF); LongSparseArray accessEvents = new LongSparseArray<>(); LongSparseArray rejectEvents = new LongSparseArray<>(); accessEvents.put( key, new NoteOpEvent(OP_TIME, DURATION, new OpEventProxyInfo(PROXY_UID, PROXY_PACKAGE, null))); rejectEvents.put(key, new NoteOpEvent(REJECT_TIME, -1, null)); return new OpEntry( op, mode, Collections.singletonMap( null, new AttributedOpEntry(op, false, accessEvents, rejectEvents))); } } private static int getAudioRestrictionKey(int code, @AttributeUsage int usage) { return code | (usage << 8); } @AutoValue abstract static class Key { @Nullable abstract Integer getUid(); @Nullable abstract String getPackageName(); @Nullable abstract Integer getOpCode(); static Key create(Integer uid, String packageName, Integer opCode) { return new AutoValue_ShadowAppOpsManager_Key(uid, packageName, opCode); } } /** Class holding usage mode and excpetion packages. */ public static class ModeAndException { public final int mode; public final List exceptionPackages; public ModeAndException(int mode, String[] exceptionPackages) { this.mode = mode; this.exceptionPackages = exceptionPackages == null ? Collections.emptyList() : Collections.unmodifiableList(Arrays.asList(exceptionPackages)); } } @Resetter public static void reset() { // The callback passed in AppOpsManager#setOnOpNotedCallback is stored statically. // The check for staticallyInitialized is to make it so that we don't load AppOpsManager if it // hadn't already been loaded (both to save time and to also avoid any errors that might // happen if we tried to lazy load the class during reset) if (RuntimeEnvironment.getApiLevel() >= R && staticallyInitialized) { ReflectionHelpers.setStaticField(AppOpsManager.class, "sOnOpNotedCallback", null); } } @ForType(className = "android.app.AppOpInfo") interface AppOpInfoReflector { @Accessor("name") String getName(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy