Home | History | Annotate | Download | only in shadows
      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