aboutsummaryrefslogtreecommitdiffstats
path: root/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppOpsManager.java
blob: fba48cf06cb173e2072cca25a9dcbaaf13e6ac2b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
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

  @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));
    }
  }
}