diff options
| -rw-r--r-- | api/current.txt | 44 | ||||
| -rw-r--r-- | core/java/android/app/AppOpsManager.java | 1043 | ||||
| -rw-r--r-- | core/java/android/app/AsyncNotedAppOp.aidl | 19 | ||||
| -rw-r--r-- | core/java/android/app/AsyncNotedAppOp.java | 245 | ||||
| -rw-r--r-- | core/java/android/app/SyncNotedAppOp.java | 68 | ||||
| -rw-r--r-- | core/java/android/content/PermissionChecker.java | 53 | ||||
| -rw-r--r-- | core/java/android/os/Binder.java | 13 | ||||
| -rw-r--r-- | core/java/android/os/BinderProxy.java | 10 | ||||
| -rw-r--r-- | core/java/android/os/IBinder.java | 5 | ||||
| -rw-r--r-- | core/java/android/os/Parcel.java | 21 | ||||
| -rw-r--r-- | core/java/com/android/internal/app/IAppOpsAsyncNotedCallback.aidl | 24 | ||||
| -rw-r--r-- | core/java/com/android/internal/app/IAppOpsService.aidl | 10 | ||||
| -rw-r--r-- | core/java/com/android/internal/util/AnnotationValidations.java | 44 | ||||
| -rw-r--r-- | services/core/java/com/android/server/appop/AppOpsService.java | 186 |
14 files changed, 1545 insertions, 240 deletions
diff --git a/api/current.txt b/api/current.txt index 6adddd064fb5..1c68aaf9620a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -4279,14 +4279,21 @@ package android.app { method public void checkPackage(int, @NonNull String); method public void finishOp(@NonNull String, int, @NonNull String); method public boolean isOpActive(@NonNull String, int, @NonNull String); - method public int noteOp(@NonNull String, int, @NonNull String); - method public int noteOpNoThrow(@NonNull String, int, @NonNull String); - method public int noteProxyOp(@NonNull String, @NonNull String); - method public int noteProxyOpNoThrow(@NonNull String, @NonNull String); - method public int noteProxyOpNoThrow(@NonNull String, @Nullable String, int); + method @Deprecated public int noteOp(@NonNull String, int, @NonNull String); + method public int noteOp(@NonNull String, int, @Nullable String, @Nullable String); + method @Deprecated public int noteOpNoThrow(@NonNull String, int, @NonNull String); + method public int noteOpNoThrow(@NonNull String, int, @NonNull String, @Nullable String); + method @Deprecated public int noteProxyOp(@NonNull String, @NonNull String); + method public int noteProxyOp(@NonNull String, @Nullable String, int, @Nullable String); + method @Deprecated public int noteProxyOpNoThrow(@NonNull String, @NonNull String); + method @Deprecated public int noteProxyOpNoThrow(@NonNull String, @Nullable String, int); + method public int noteProxyOpNoThrow(@NonNull String, @Nullable String, int, @Nullable String); method public static String permissionToOp(String); - method public int startOp(@NonNull String, int, @NonNull String); - method public int startOpNoThrow(@NonNull String, int, @NonNull String); + method public void setNotedAppOpsCollector(@Nullable android.app.AppOpsManager.AppOpsCollector); + method @Deprecated public int startOp(@NonNull String, int, @NonNull String); + method public int startOp(@NonNull String, int, @Nullable String, @Nullable String); + method @Deprecated public int startOpNoThrow(@NonNull String, int, @NonNull String); + method public int startOpNoThrow(@NonNull String, int, @NonNull String, @Nullable String); method public void startWatchingActive(@NonNull String[], @NonNull java.util.concurrent.Executor, @NonNull android.app.AppOpsManager.OnOpActiveChangedListener); method public void startWatchingMode(@NonNull String, @Nullable String, @NonNull android.app.AppOpsManager.OnOpChangedListener); method public void startWatchingMode(@NonNull String, @Nullable String, int, @NonNull android.app.AppOpsManager.OnOpChangedListener); @@ -4338,6 +4345,14 @@ package android.app { field public static final int WATCH_FOREGROUND_CHANGES = 1; // 0x1 } + public abstract static class AppOpsManager.AppOpsCollector { + ctor public AppOpsManager.AppOpsCollector(); + method @NonNull public java.util.concurrent.Executor getAsyncNotedExecutor(); + method public abstract void onAsyncNoted(@NonNull android.app.AsyncNotedAppOp); + method public abstract void onNoted(@NonNull android.app.SyncNotedAppOp); + method public abstract void onSelfNoted(@NonNull android.app.SyncNotedAppOp); + } + public static interface AppOpsManager.OnOpActiveChangedListener { method public void onOpActiveChanged(@NonNull String, int, @NonNull String, boolean); } @@ -4458,6 +4473,17 @@ package android.app { field public String serviceDetails; } + public final class AsyncNotedAppOp implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public String getMessage(); + method @Nullable public String getNotingPackageName(); + method @IntRange(from=0) public int getNotingUid(); + method @NonNull public String getOp(); + method @IntRange(from=0) public long getTime(); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.AsyncNotedAppOp> CREATOR; + } + public final class AuthenticationRequiredException extends java.lang.SecurityException implements android.os.Parcelable { ctor public AuthenticationRequiredException(Throwable, android.app.PendingIntent); method public int describeContents(); @@ -6267,6 +6293,10 @@ package android.app { public class StatusBarManager { } + public final class SyncNotedAppOp { + method @NonNull public String getOp(); + } + @Deprecated public class TabActivity extends android.app.ActivityGroup { ctor @Deprecated public TabActivity(); method @Deprecated public android.widget.TabHost getTabHost(); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 34045c995aed..d1be17c68dde 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -28,13 +28,19 @@ import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; import android.app.usage.UsageStatsManager; +import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ParceledListSlice; +import android.database.DatabaseUtils; import android.media.AudioAttributes.AttributeUsage; import android.os.Binder; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.HandlerThread; import android.os.IBinder; +import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.os.Process; @@ -49,9 +55,12 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.Immutable; import com.android.internal.app.IAppOpsActiveCallback; +import com.android.internal.app.IAppOpsAsyncNotedCallback; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsNotedCallback; import com.android.internal.app.IAppOpsService; +import com.android.internal.os.RuntimeInit; +import com.android.internal.os.ZygoteInit; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; @@ -59,10 +68,12 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; +import java.util.BitSet; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -118,6 +129,12 @@ public class AppOpsManager { private final ArrayMap<OnOpNotedListener, IAppOpsNotedCallback> mNotedWatchers = new ArrayMap<>(); + private static final Object sLock = new Object(); + + /** Current {@link AppOpsCollector}. Change via {@link #setNotedAppOpsCollector} */ + @GuardedBy("sLock") + private static @Nullable AppOpsCollector sNotedAppOpsCollector; + static IBinder sToken; /** @hide */ @@ -1126,6 +1143,22 @@ public class AppOpsManager { /** @hide Query all packages on device */ public static final String OPSTR_QUERY_ALL_PACKAGES = "android:query_all_packages"; + + /** {@link #sAppOpsToNote} not initialized yet for this op */ + private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; + /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ + private static final byte SHOULD_NOT_COLLECT_NOTE_OP = 1; + /** Should collect noting of this app-op in {@link #sAppOpsToNote} */ + private static final byte SHOULD_COLLECT_NOTE_OP = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "SHOULD_" }, value = { + SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED, + SHOULD_NOT_COLLECT_NOTE_OP, + SHOULD_COLLECT_NOTE_OP + }) + private @interface ShouldCollectNoteOp {} + // Warning: If an permission is added here it also has to be added to // com.android.packageinstaller.permission.utils.EventLogger private static final int[] RUNTIME_AND_APPOP_PERMISSIONS_OPS = { @@ -1988,6 +2021,27 @@ public class AppOpsManager { */ private static HashMap<String, Integer> sPermToOp = new HashMap<>(); + /** + * Set to the uid of the caller if this thread is currently executing a two-way binder + * transaction. Not set if this thread is currently not executing a two way binder transaction. + * + * @see #startNotedAppOpsCollection + * @see #markAppOpNoted + */ + private static final ThreadLocal<Integer> sBinderThreadCallingUid = new ThreadLocal<>(); + + /** + * If a thread is currently executing a two-way binder transaction, this stores the op-codes of + * the app-ops that were noted during this transaction. + * + * @see #markAppOpNoted + */ + private static final ThreadLocal<long[]> sAppOpsNotedInThisBinderTransaction = + new ThreadLocal<>(); + + /** Whether noting for an appop should be collected */ + private static final @ShouldCollectNoteOp byte[] sAppOpsToNote = new byte[_NUM_OP]; + static { if (sOpToSwitch.length != _NUM_OP) { throw new IllegalStateException("sOpToSwitch length " + sOpToSwitch.length @@ -2031,6 +2085,12 @@ public class AppOpsManager { sPermToOp.put(sOpPerms[op], op); } } + + if ((_NUM_OP + Long.SIZE - 1) / Long.SIZE != 2) { + // The code currently assumes that the length of sAppOpsNotedInThisBinderTransaction is + // two longs + throw new IllegalStateException("notedAppOps collection code assumes < 128 appops"); + } } /** @hide */ @@ -4350,8 +4410,8 @@ public class AppOpsManager { * The mode of the ops returned are set for the package but may not reflect their effective * state due to UID policy or because it's controlled by a different master op. * - * Use {@link #unsafeCheckOp(String, int, String)}} or {@link #noteOp(String, int, String)} - * if the effective mode is needed. + * Use {@link #unsafeCheckOp(String, int, String)}} or + * {@link #noteOp(String, int, String, String)} if the effective mode is needed. * * @param ops The set of operations you are interested in, or null if you want all of them. * @hide @@ -4374,8 +4434,8 @@ public class AppOpsManager { * The mode of the ops returned are set for the package but may not reflect their effective * state due to UID policy or because it's controlled by a different master op. * - * Use {@link #unsafeCheckOp(String, int, String)}} or {@link #noteOp(String, int, String)} - * if the effective mode is needed. + * Use {@link #unsafeCheckOp(String, int, String)}} or + * {@link #noteOp(String, int, String, String)} if the effective mode is needed. * * @param ops The set of operations you are interested in, or null if you want all of them. * @hide @@ -4396,8 +4456,8 @@ public class AppOpsManager { * The mode of the ops returned are set for the package but may not reflect their effective * state due to UID policy or because it's controlled by a different master op. * - * Use {@link #unsafeCheckOp(String, int, String)}} or {@link #noteOp(String, int, String)} - * if the effective mode is needed. + * Use {@link #unsafeCheckOp(String, int, String)}} or + * {@link #noteOp(String, int, String, String)} if the effective mode is needed. * * @param uid The uid of the application of interest. * @param packageName The name of the application of interest. @@ -4429,8 +4489,8 @@ public class AppOpsManager { * The mode of the ops returned are set for the package but may not reflect their effective * state due to UID policy or because it's controlled by a different master op. * - * Use {@link #unsafeCheckOp(String, int, String)}} or {@link #noteOp(String, int, String)} - * if the effective mode is needed. + * Use {@link #unsafeCheckOp(String, int, String)}} or + * {@link #noteOp(String, int, String, String)} if the effective mode is needed. * * @param uid The uid of the application of interest. * @param packageName The name of the application of interest. @@ -4820,7 +4880,7 @@ public class AppOpsManager { * * @see #isOperationActive * @see #stopWatchingActive - * @see #startOp(int, int, String) + * @see #startOp(int, int, String, boolean, String) * @see #finishOp(int, int, String) */ // TODO: Uncomment below annotation once b/73559440 is fixed @@ -4871,7 +4931,7 @@ public class AppOpsManager { * * @see #isOperationActive * @see #startWatchingActive - * @see #startOp(int, int, String) + * @see #startOp(int, int, String, boolean, String) * @see #finishOp(int, int, String) */ public void stopWatchingActive(@NonNull OnOpActiveChangedListener callback) { @@ -4902,7 +4962,7 @@ public class AppOpsManager { * * @see #startWatchingActive(int[], OnOpActiveChangedListener) * @see #stopWatchingNoted(OnOpNotedListener) - * @see #noteOp(String, int, String) + * @see #noteOp(String, int, String, String) * * @hide */ @@ -4934,7 +4994,7 @@ public class AppOpsManager { * Unregistering a non-registered callback has no effect. * * @see #startWatchingNoted(int[], OnOpNotedListener) - * @see #noteOp(String, int, String) + * @see #noteOp(String, int, String, String) * * @hide */ @@ -4969,15 +5029,16 @@ public class AppOpsManager { /** * Do a quick check for whether an application might be able to perform an operation. - * This is <em>not</em> a security check; you must use {@link #noteOp(String, int, String)} - * or {@link #startOp(String, int, String)} for your actual security checks, which also - * ensure that the given uid and package name are consistent. This function can just be - * used for a quick check to see if an operation has been disabled for the application, + * This is <em>not</em> a security check; you must use {@link #noteOp(String, int, String, + * String)} or {@link #startOp(String, int, String, String)} for your actual security checks, + * which also ensure that the given uid and package name are consistent. This function can just + * be used for a quick check to see if an operation has been disabled for the application, * as an early reject of some work. This does not modify the time stamp or other data * about the operation. * * <p>Important things this will not do (which you need to ultimate use - * {@link #noteOp(String, int, String)} or {@link #startOp(String, int, String)} to cover):</p> + * {@link #noteOp(String, int, String, String)} or + * {@link #startOp(String, int, String, String)} to cover):</p> * <ul> * <li>Verifying the uid and package are consistent, so callers can't spoof * their identity.</li> @@ -5048,126 +5109,305 @@ public class AppOpsManager { } /** + * @deprecated Use {@link #noteOp(String, int, String, String)} instead + */ + @Deprecated + public int noteOp(@NonNull String op, int uid, @NonNull String packageName) { + return noteOp(op, uid, packageName, null); + } + + /** + * @deprecated Use {@link #noteOp(String, int, String, String)} instead + * + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "Use {@link " + + "#noteOp(java.lang.String, int, java.lang.String, java.lang.String)} instead") + @Deprecated + public int noteOp(int op) { + return noteOp(op, Process.myUid(), mContext.getOpPackageName(), null); + } + + /** + * @deprecated Use {@link #noteOp(String, int, String, String)} instead + * + * @hide + */ + @Deprecated + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "Use {@link " + + "#noteOp(java.lang.String, int, java.lang.String, java.lang.String)} instead") + public int noteOp(int op, int uid, @Nullable String packageName) { + return noteOp(op, uid, packageName, null); + } + + /** * Make note of an application performing an operation. Note that you must pass * in both the uid and name of the application to be checked; this function will verify * that these two match, and if not, return {@link #MODE_IGNORED}. If this call * succeeds, the last execution time of the operation for this app will be updated to * the current time. + * * @param op The operation to note. One of the OPSTR_* constants. * @param uid The user id of the application attempting to perform the operation. * @param packageName The name of the application attempting to perform the operation. + * @param message A message describing the reason the op was noted + * * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without * causing the app to crash). + * * @throws SecurityException If the app has been configured to crash on this op. */ - public int noteOp(@NonNull String op, int uid, @NonNull String packageName) { - return noteOp(strOpToOp(op), uid, packageName); + public int noteOp(@NonNull String op, int uid, @Nullable String packageName, + @Nullable String message) { + return noteOp(strOpToOp(op), uid, packageName, message); } /** - * Like {@link #noteOp} but instead of throwing a {@link SecurityException} it - * returns {@link #MODE_ERRORED}. + * Make note of an application performing an operation. Note that you must pass + * in both the uid and name of the application to be checked; this function will verify + * that these two match, and if not, return {@link #MODE_IGNORED}. If this call + * succeeds, the last execution time of the operation for this app will be updated to + * the current time. + * + * @param op The operation to note. One of the OP_* constants. + * @param uid The user id of the application attempting to perform the operation. + * @param packageName The name of the application attempting to perform the operation. + * @param message A message describing the reason the op was noted + * + * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or + * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without + * causing the app to crash). + * + * @throws SecurityException If the app has been configured to crash on this op. + * + * @hide */ + public int noteOp(int op, int uid, @Nullable String packageName, @Nullable String message) { + final int mode = noteOpNoThrow(op, uid, packageName, message); + if (mode == MODE_ERRORED) { + throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName)); + } + return mode; + } + + /** + * @deprecated Use {@link #noteOpNoThrow(String, int, String, String)} instead + */ + @Deprecated public int noteOpNoThrow(@NonNull String op, int uid, @NonNull String packageName) { - return noteOpNoThrow(strOpToOp(op), uid, packageName); + return noteOpNoThrow(op, uid, packageName, null); } /** - * Make note of an application performing an operation on behalf of another - * application when handling an IPC. Note that you must pass the package name - * of the application that is being proxied while its UID will be inferred from - * the IPC state; this function will verify that the calling uid and proxied - * package name match, and if not, return {@link #MODE_IGNORED}. If this call - * succeeds, the last execution time of the operation for the proxied app and - * your app will be updated to the current time. + * @deprecated Use {@link #noteOpNoThrow(int, int, String, String)} instead + * + * @hide + */ + @Deprecated + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "Use {@link " + + "#noteOpNoThrow(java.lang.String, int, java.lang.String, java.lang.String)} instead") + public int noteOpNoThrow(int op, int uid, String packageName) { + return noteOpNoThrow(op, uid, packageName, null); + } + + /** + * Like {@link #noteOp(String, int, String, String)} but instead of throwing a + * {@link SecurityException} it returns {@link #MODE_ERRORED}. + * * @param op The operation to note. One of the OPSTR_* constants. - * @param proxiedPackageName The name of the application calling into the proxy application. + * @param uid The user id of the application attempting to perform the operation. + * @param packageName The name of the application attempting to perform the operation. + * @param message A message describing the reason the op was noted + * * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without * causing the app to crash). - * @throws SecurityException If the app has been configured to crash on this op. */ + public int noteOpNoThrow(@NonNull String op, int uid, @NonNull String packageName, + @Nullable String message) { + return noteOpNoThrow(strOpToOp(op), uid, packageName, message); + } + + /** + * Like {@link #noteOp(String, int, String, String)} but instead of throwing a + * {@link SecurityException} it returns {@link #MODE_ERRORED}. + * + * @param op The operation to note. One of the OP_* constants. + * @param uid The user id of the application attempting to perform the operation. + * @param packageName The name of the application attempting to perform the operation. + * @param message A message describing the reason the op was noted + * + * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or + * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without + * causing the app to crash). + * + * @hide + */ + public int noteOpNoThrow(int op, int uid, @Nullable String packageName, + @Nullable String message) { + try { + int mode = mService.noteOperation(op, uid, packageName); + if (mode == MODE_ALLOWED) { + markAppOpNoted(uid, packageName, op, message); + } + + return mode; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @deprecated Use {@link #noteProxyOp(String, String, int, String)} instead + */ + @Deprecated public int noteProxyOp(@NonNull String op, @NonNull String proxiedPackageName) { - return noteProxyOp(strOpToOp(op), proxiedPackageName); + return noteProxyOp(op, proxiedPackageName, Binder.getCallingUid(), null); } /** - * Like {@link #noteProxyOp(String, String)} but instead - * of throwing a {@link SecurityException} it returns {@link #MODE_ERRORED}. + * @deprecated Use {@link #noteProxyOp(String, String, int, String)} instead * - * <p>This API requires the package with the {@code proxiedPackageName} to belongs to - * {@link Binder#getCallingUid()}. + * @hide */ - public int noteProxyOpNoThrow(@NonNull String op, @NonNull String proxiedPackageName) { - return noteProxyOpNoThrow(strOpToOp(op), proxiedPackageName); + @Deprecated + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "Use {@link " + + "#noteProxyOp(java.lang.String, java.lang.String, int, java.lang.String)} instead") + public int noteProxyOp(int op, @Nullable String proxiedPackageName) { + return noteProxyOp(op, proxiedPackageName, Binder.getCallingUid(), null); } /** - * Like {@link #noteProxyOpNoThrow(String, String)} but allows to specify the proxied uid. + * Make note of an application performing an operation on behalf of another application when + * handling an IPC. This function will verify that the calling uid and proxied package name + * match, and if not, return {@link #MODE_IGNORED}. If this call succeeds, the last execution + * time of the operation for the proxied app and your app will be updated to the current time. * - * <p>This API requires package with the {@code proxiedPackageName} to belong to - * {@code proxiedUid}. + * @param op The operation to note. One of the OP_* constants. + * @param proxiedPackageName The name of the application calling into the proxy application. + * @param proxiedUid The uid of the proxied application + * @param message A message describing the reason the op was noted * - * @param op The op to note - * @param proxiedPackageName The package to note the op for or {@code null} if the op should be - * noted for the "android" package - * @param proxiedUid The uid the package belongs to + * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or {@link #MODE_IGNORED} + * if it is not allowed and should be silently ignored (without causing the app to crash). + * + * @throws SecurityException If the proxy or proxied app has been configured to crash on this + * op. + * + * @hide */ - public int noteProxyOpNoThrow(@NonNull String op, @Nullable String proxiedPackageName, - int proxiedUid) { - return noteProxyOpNoThrow(strOpToOp(op), proxiedPackageName, proxiedUid); + public int noteProxyOp(int op, @Nullable String proxiedPackageName, int proxiedUid, + @Nullable String message) { + int mode = noteProxyOpNoThrow(op, proxiedPackageName, proxiedUid, message); + if (mode == MODE_ERRORED) { + throw new SecurityException("Proxy package " + mContext.getOpPackageName() + + " from uid " + Process.myUid() + " or calling package " + proxiedPackageName + + " from uid " + proxiedUid + " not allowed to perform " + sOpNames[op]); + } + return mode; } /** - * Report that an application has started executing a long-running operation. Note that you - * must pass in both the uid and name of the application to be checked; this function will - * verify that these two match, and if not, return {@link #MODE_IGNORED}. If this call - * succeeds, the last execution time of the operation for this app will be updated to - * the current time and the operation will be marked as "running". In this case you must - * later call {@link #finishOp(String, int, String)} to report when the application is no - * longer performing the operation. - * @param op The operation to start. One of the OPSTR_* constants. - * @param uid The user id of the application attempting to perform the operation. - * @param packageName The name of the application attempting to perform the operation. - * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or - * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without - * causing the app to crash). - * @throws SecurityException If the app has been configured to crash on this op. + * Make note of an application performing an operation on behalf of another application when + * handling an IPC. This function will verify that the calling uid and proxied package name + * match, and if not, return {@link #MODE_IGNORED}. If this call succeeds, the last execution + * time of the operation for the proxied app and your app will be updated to the current time. + * + * @param op The operation to note. One of the OPSTR_* constants. + * @param proxiedPackageName The name of the application calling into the proxy application. + * @param proxiedUid The uid of the proxied application + * @param message A message describing the reason the op was noted + * + * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or {@link #MODE_IGNORED} + * if it is not allowed and should be silently ignored (without causing the app to crash). + * + * @throws SecurityException If the proxy or proxied app has been configured to crash on this + * op. */ - public int startOp(@NonNull String op, int uid, @NonNull String packageName) { - return startOp(strOpToOp(op), uid, packageName); + public int noteProxyOp(@NonNull String op, @Nullable String proxiedPackageName, int proxiedUid, + @Nullable String message) { + return noteProxyOp(strOpToOp(op), proxiedPackageName, proxiedUid, message); } /** - * Like {@link #startOp} but instead of throwing a {@link SecurityException} it - * returns {@link #MODE_ERRORED}. + * @deprecated Use {@link #noteProxyOpNoThrow(String, String, int, String)} instead */ - public int startOpNoThrow(@NonNull String op, int uid, @NonNull String packageName) { - return startOpNoThrow(strOpToOp(op), uid, packageName); + @Deprecated + public int noteProxyOpNoThrow(@NonNull String op, @NonNull String proxiedPackageName) { + return noteProxyOpNoThrow(op, proxiedPackageName, Binder.getCallingUid(), null); } /** - * Report that an application is no longer performing an operation that had previously - * been started with {@link #startOp(String, int, String)}. There is no validation of input - * or result; the parameters supplied here must be the exact same ones previously passed - * in when starting the operation. + * @deprecated Use {@link #noteProxyOpNoThrow(String, String, int, String)} instead */ - public void finishOp(@NonNull String op, int uid, @NonNull String packageName) { - finishOp(strOpToOp(op), uid, packageName); + @Deprecated + public int noteProxyOpNoThrow(@NonNull String op, @Nullable String proxiedPackageName, + int proxiedUid) { + return noteProxyOpNoThrow(op, proxiedPackageName, proxiedUid, null); + } + + /** + * Like {@link #noteProxyOp(String, String, int, String)} but instead + * of throwing a {@link SecurityException} it returns {@link #MODE_ERRORED}. + * + * <p>This API requires package with the {@code proxiedPackageName} to belong to + * {@code proxiedUid}. + * + * @param op The op to note + * @param proxiedPackageName The package to note the op for + * @param proxiedUid The uid the package belongs to + * @param message A message describing the reason the op was noted + */ + public int noteProxyOpNoThrow(@NonNull String op, @Nullable String proxiedPackageName, + int proxiedUid, @Nullable String message) { + return noteProxyOpNoThrow(strOpToOp(op), proxiedPackageName, proxiedUid, message); + } + + /** + * Like {@link #noteProxyOp(int, String, int, String)} but instead + * of throwing a {@link SecurityException} it returns {@link #MODE_ERRORED}. + * + * @param op The op to note + * @param proxiedPackageName The package to note the op for or {@code null} if the op should be + * noted for the "android" package + * @param proxiedUid The uid the package belongs to + * @param message A message describing the reason the op was noted + * + * @hide + */ + public int noteProxyOpNoThrow(int op, @Nullable String proxiedPackageName, int proxiedUid, + @Nullable String message) { + int myUid = Process.myUid(); + + try { + int mode = mService.noteProxyOperation(op, myUid, mContext.getOpPackageName(), + proxiedUid, proxiedPackageName); + if (mode == MODE_ALLOWED + // Only collect app-ops when the proxy is trusted + && mContext.checkPermission(Manifest.permission.UPDATE_APP_OPS_STATS, -1, myUid) + == PackageManager.PERMISSION_GRANTED) { + markAppOpNoted(proxiedUid, proxiedPackageName, op, message); + } + + return mode; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** * Do a quick check for whether an application might be able to perform an operation. - * This is <em>not</em> a security check; you must use {@link #noteOp(int, int, String)} - * or {@link #startOp(int, int, String)} for your actual security checks, which also - * ensure that the given uid and package name are consistent. This function can just be - * used for a quick check to see if an operation has been disabled for the application, - * as an early reject of some work. This does not modify the time stamp or other data - * about the operation. + * This is <em>not</em> a security check; you must use {@link #noteOp(String, int, String, + * String)} or {@link #startOp(int, int, String, boolean, String)} for your actual security + * checks, which also ensure that the given uid and package name are consistent. This function + * can just be used for a quick check to see if an operation has been disabled for the + * application, as an early reject of some work. This does not modify the time stamp or other + * data about the operation. * * <p>Important things this will not do (which you need to ultimate use - * {@link #noteOp(int, int, String)} or {@link #startOp(int, int, String)} to cover):</p> + * {@link #noteOp(String, int, String, String)} or + * {@link #startOp(int, int, String, boolean, String)} to cover):</p> * <ul> * <li>Verifying the uid and package are consistent, so callers can't spoof * their identity.</li> @@ -5259,171 +5499,102 @@ public class AppOpsManager { } } - /** - * Make note of an application performing an operation. Note that you must pass - * in both the uid and name of the application to be checked; this function will verify - * that these two match, and if not, return {@link #MODE_IGNORED}. If this call - * succeeds, the last execution time of the operation for this app will be updated to - * the current time. - * @param op The operation to note. One of the OP_* constants. - * @param uid The user id of the application attempting to perform the operation. - * @param packageName The name of the application attempting to perform the operation. - * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or - * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without - * causing the app to crash). - * @throws SecurityException If the app has been configured to crash on this op. - * @hide - */ + /** @hide */ @UnsupportedAppUsage - public int noteOp(int op, int uid, String packageName) { - final int mode = noteOpNoThrow(op, uid, packageName); - if (mode == MODE_ERRORED) { - throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName)); + public static IBinder getToken(IAppOpsService service) { + synchronized (AppOpsManager.class) { + if (sToken != null) { + return sToken; + } + try { + sToken = service.getToken(new Binder()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return sToken; } - return mode; } + /** - * Make note of an application performing an operation on behalf of another - * application when handling an IPC. Note that you must pass the package name - * of the application that is being proxied while its UID will be inferred from - * the IPC state; this function will verify that the calling uid and proxied - * package name match, and if not, return {@link #MODE_IGNORED}. If this call - * succeeds, the last execution time of the operation for the proxied app and - * your app will be updated to the current time. - * @param op The operation to note. One of the OPSTR_* constants. - * @param proxiedPackageName The name of the application calling into the proxy application. - * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or - * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without - * causing the app to crash). - * @throws SecurityException If the proxy or proxied app has been configured to - * crash on this op. - * - * @hide + * @deprecated use {@link #startOp(String, int, String, String)} instead */ - @UnsupportedAppUsage - public int noteProxyOp(int op, String proxiedPackageName) { - int mode = noteProxyOpNoThrow(op, proxiedPackageName); - if (mode == MODE_ERRORED) { - throw new SecurityException("Proxy package " + mContext.getOpPackageName() - + " from uid " + Process.myUid() + " or calling package " - + proxiedPackageName + " from uid " + Binder.getCallingUid() - + " not allowed to perform " + sOpNames[op]); - } - return mode; + @Deprecated + public int startOp(@NonNull String op, int uid, @NonNull String packageName) { + return startOp(op, uid, packageName, null); } /** - * Like {@link #noteProxyOp(int, String)} but instead - * of throwing a {@link SecurityException} it returns {@link #MODE_ERRORED}. + * @deprecated Use {@link #startOp(int, int, String, boolean, String)} instead + * * @hide */ - public int noteProxyOpNoThrow(int op, String proxiedPackageName, int proxiedUid) { - try { - return mService.noteProxyOperation(op, Process.myUid(), mContext.getOpPackageName(), - proxiedUid, proxiedPackageName); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + @Deprecated + public int startOp(int op) { + return startOp(op, Process.myUid(), mContext.getOpPackageName(), false, null); } /** - * Like {@link #noteProxyOp(int, String)} but instead - * of throwing a {@link SecurityException} it returns {@link #MODE_ERRORED}. - * - * <p>This API requires the package with {@code proxiedPackageName} to belongs to - * {@link Binder#getCallingUid()}. + * @deprecated Use {@link #startOp(int, int, String, boolean, String)} instead * * @hide */ - public int noteProxyOpNoThrow(int op, String proxiedPackageName) { - return noteProxyOpNoThrow(op, proxiedPackageName, Binder.getCallingUid()); + @Deprecated + public int startOp(int op, int uid, String packageName) { + return startOp(op, uid, packageName, false, null); } /** - * Like {@link #noteOp} but instead of throwing a {@link SecurityException} it - * returns {@link #MODE_ERRORED}. + * @deprecated Use {@link #startOp(int, int, String, boolean, String)} instead + * * @hide */ - @UnsupportedAppUsage - public int noteOpNoThrow(int op, int uid, String packageName) { - try { - return mService.noteOperation(op, uid, packageName); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** @hide */ - @UnsupportedAppUsage - public int noteOp(int op) { - return noteOp(op, Process.myUid(), mContext.getOpPackageName()); - } - - /** @hide */ - @UnsupportedAppUsage - public static IBinder getToken(IAppOpsService service) { - synchronized (AppOpsManager.class) { - if (sToken != null) { - return sToken; - } - try { - sToken = service.getToken(new Binder()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - return sToken; - } - } - - /** @hide */ - public int startOp(int op) { - return startOp(op, Process.myUid(), mContext.getOpPackageName()); + @Deprecated + public int startOp(int op, int uid, String packageName, boolean startIfModeDefault) { + return startOp(op, uid, packageName, startIfModeDefault, null); } /** - * Report that an application has started executing a long-running operation. Note that you - * must pass in both the uid and name of the application to be checked; this function will - * verify that these two match, and if not, return {@link #MODE_IGNORED}. If this call - * succeeds, the last execution time of the operation for this app will be updated to - * the current time and the operation will be marked as "running". In this case you must - * later call {@link #finishOp(int, int, String)} to report when the application is no - * longer performing the operation. + * Report that an application has started executing a long-running operation. * - * @param op The operation to start. One of the OP_* constants. + * @param op The operation to start. One of the OPSTR_* constants. * @param uid The user id of the application attempting to perform the operation. * @param packageName The name of the application attempting to perform the operation. + * @param message Description why op was started + * * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without * causing the app to crash). - * @throws SecurityException If the app has been configured to crash on this op. - * @hide + * + * @throws SecurityException If the app has been configured to crash on this op or + * the package is not in the passed in UID. */ - public int startOp(int op, int uid, String packageName) { - return startOp(op, uid, packageName, false); + public int startOp(@NonNull String op, int uid, @Nullable String packageName, + @Nullable String message) { + return startOp(strOpToOp(op), uid, packageName, false, message); } /** - * Report that an application has started executing a long-running operation. Similar - * to {@link #startOp(String, int, String) except that if the mode is {@link #MODE_DEFAULT} - * the operation should succeed since the caller has performed its standard permission - * checks which passed and would perform the protected operation for this mode. + * Report that an application has started executing a long-running operation. * * @param op The operation to start. One of the OP_* constants. * @param uid The user id of the application attempting to perform the operation. * @param packageName The name of the application attempting to perform the operation. + * @param startIfModeDefault Whether to start if mode is {@link #MODE_DEFAULT}. + * @param message Description why op was started + * * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without * causing the app to crash). - * @param startIfModeDefault Whether to start if mode is {@link #MODE_DEFAULT}. * * @throws SecurityException If the app has been configured to crash on this op or * the package is not in the passed in UID. * * @hide */ - public int startOp(int op, int uid, String packageName, boolean startIfModeDefault) { - final int mode = startOpNoThrow(op, uid, packageName, startIfModeDefault); + public int startOp(int op, int uid, @Nullable String packageName, boolean startIfModeDefault, + @Nullable String message) { + final int mode = startOpNoThrow(op, uid, packageName, startIfModeDefault, message); if (mode == MODE_ERRORED) { throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName)); } @@ -5431,45 +5602,111 @@ public class AppOpsManager { } /** - * Like {@link #startOp} but instead of throwing a {@link SecurityException} it - * returns {@link #MODE_ERRORED}. + * @deprecated use {@link #startOpNoThrow(String, int, String, String)} instead + */ + @Deprecated + public int startOpNoThrow(@NonNull String op, int uid, @NonNull String packageName) { + return startOpNoThrow(op, uid, packageName, null); + } + + /** + * @deprecated Use {@link #startOpNoThrow(int, int, String, boolean, String} instead + * * @hide */ + @Deprecated public int startOpNoThrow(int op, int uid, String packageName) { - return startOpNoThrow(op, uid, packageName, false); + return startOpNoThrow(op, uid, packageName, false, null); } /** - * Like {@link #startOp(int, int, String, boolean)} but instead of throwing a + * @deprecated Use {@link #startOpNoThrow(int, int, String, boolean, String} instead + * + * @hide + */ + @Deprecated + public int startOpNoThrow(int op, int uid, String packageName, boolean startIfModeDefault) { + return startOpNoThrow(op, uid, packageName, startIfModeDefault, null); + } + + /** + * Like {@link #startOp(String, int, String, String)} but instead of throwing a * {@link SecurityException} it returns {@link #MODE_ERRORED}. * * @param op The operation to start. One of the OP_* constants. * @param uid The user id of the application attempting to perform the operation. * @param packageName The name of the application attempting to perform the operation. + * @param message Description why op was started + * * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without * causing the app to crash). + */ + public int startOpNoThrow(@NonNull String op, int uid, @NonNull String packageName, + @Nullable String message) { + return startOpNoThrow(strOpToOp(op), uid, packageName, false, message); + } + + /** + * Like {@link #startOp(int, int, String, boolean, String)} but instead of throwing a + * {@link SecurityException} it returns {@link #MODE_ERRORED}. + * + * @param op The operation to start. One of the OP_* constants. + * @param uid The user id of the application attempting to perform the operation. + * @param packageName The name of the application attempting to perform the operation. * @param startIfModeDefault Whether to start if mode is {@link #MODE_DEFAULT}. + * @param message Description why op was started + * + * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or + * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without + * causing the app to crash). * * @hide */ - public int startOpNoThrow(int op, int uid, String packageName, boolean startIfModeDefault) { + public int startOpNoThrow(int op, int uid, @NonNull String packageName, + boolean startIfModeDefault, @Nullable String message) { try { - return mService.startOperation(getToken(mService), op, uid, packageName, + int mode = mService.startOperation(getToken(mService), op, uid, packageName, startIfModeDefault); + if (mode == MODE_ALLOWED) { + markAppOpNoted(uid, packageName, op, message); + } + + return mode; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** + * @deprecated Use {@link #finishOp(String, int, String)} instead + * + * @hide + */ + @Deprecated + public void finishOp(int op) { + finishOp(op, Process.myUid(), mContext.getOpPackageName()); + } + + /** * Report that an application is no longer performing an operation that had previously - * been started with {@link #startOp(int, int, String)}. There is no validation of input - * or result; the parameters supplied here must be the exact same ones previously passed + * been started with {@link #startOp(String, int, String, String)}. There is no validation of + * input or result; the parameters supplied here must be the exact same ones previously passed * in when starting the operation. + */ + public void finishOp(@NonNull String op, int uid, @NonNull String packageName) { + finishOp(strOpToOp(op), uid, packageName); + } + + /** + * Report that an application is no longer performing an operation that had previously + * been started with {@link #startOp(int, int, String, boolean, String)}. There is no + * validation of input or result; the parameters supplied here must be the exact same ones + * previously passed in when starting the operation. + * * @hide */ - public void finishOp(int op, int uid, String packageName) { + public void finishOp(int op, int uid, @NonNull String packageName) { try { mService.finishOperation(getToken(mService), op, uid, packageName); } catch (RemoteException e) { @@ -5477,25 +5714,399 @@ public class AppOpsManager { } } - /** @hide */ - public void finishOp(int op) { - finishOp(op, Process.myUid(), mContext.getOpPackageName()); - } - /** * Checks whether the given op for a package is active. * <p> * If you don't hold the {@code android.Manifest.permission#WATCH_APPOPS} * permission you can query only for your UID. * - * @see #finishOp(int) - * @see #startOp(int) + * @see #finishOp(String, int, String) + * @see #startOp(String, int, String, String) */ public boolean isOpActive(@NonNull String op, int uid, @NonNull String packageName) { return isOperationActive(strOpToOp(op), uid, packageName); } /** + * Start collection of noted appops on this thread. + * + * <p>Called at the beginning of a two way binder transaction. + * + * @see #finishNotedAppOpsCollection() + * + * @hide + */ + public static void startNotedAppOpsCollection(int callingUid) { + sBinderThreadCallingUid.set(callingUid); + } + + /** + * State of a temporarily paused noted app-ops collection. + * + * @see #pauseNotedAppOpsCollection() + * + * @hide + */ + public static class PausedNotedAppOpsCollection { + final int mUid; + final @Nullable long[] mCollectedNotedAppOps; + + PausedNotedAppOpsCollection(int uid, @Nullable long[] collectedNotedAppOps) { + mUid = uid; + mCollectedNotedAppOps = collectedNotedAppOps; + } + } + + /** + * Temporarily suspend collection of noted app-ops when binder-thread calls into the other + * process. During such a call there might be call-backs coming back on the same thread which + * should not be accounted to the current collection. + * + * @return a state needed to resume the collection + * + * @hide + */ + public static @Nullable PausedNotedAppOpsCollection pauseNotedAppOpsCollection() { + Integer previousUid = sBinderThreadCallingUid.get(); + if (previousUid != null) { + long[] previousCollectedNotedAppOps = sAppOpsNotedInThisBinderTransaction.get(); + + sBinderThreadCallingUid.remove(); + sAppOpsNotedInThisBinderTransaction.remove(); + + return new PausedNotedAppOpsCollection(previousUid, previousCollectedNotedAppOps); + } + + return null; + } + + /** + * Resume a collection paused via {@link #pauseNotedAppOpsCollection}. + * + * @param prevCollection The state of the previous collection + * + * @hide + */ + public static void resumeNotedAppOpsCollection( + @Nullable PausedNotedAppOpsCollection prevCollection) { + if (prevCollection != null) { + sBinderThreadCallingUid.set(prevCollection.mUid); + + if (prevCollection.mCollectedNotedAppOps != null) { + sAppOpsNotedInThisBinderTransaction.set(prevCollection.mCollectedNotedAppOps); + } + } + } + + /** + * Finish collection of noted appops on this thread. + * + * <p>Called at the end of a two way binder transaction. + * + * @see #startNotedAppOpsCollection(int) + * + * @hide + */ + public static void finishNotedAppOpsCollection() { + sBinderThreadCallingUid.remove(); + sAppOpsNotedInThisBinderTransaction.remove(); + } + + /** + * Mark an app-op as noted + */ + private void markAppOpNoted(int uid, @NonNull String packageName, int code, + @Nullable String message) { + // check it the appops needs to be collected and cache result + if (sAppOpsToNote[code] == SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED) { + boolean shouldCollectNotes; + try { + shouldCollectNotes = mService.shouldCollectNotes(code); + } catch (RemoteException e) { + return; + } + + if (shouldCollectNotes) { + sAppOpsToNote[code] = SHOULD_COLLECT_NOTE_OP; + } else { + sAppOpsToNote[code] = SHOULD_NOT_COLLECT_NOTE_OP; + } + } + + if (sAppOpsToNote[code] != SHOULD_COLLECT_NOTE_OP) { + return; + } + + Integer binderUid = sBinderThreadCallingUid.get(); + + synchronized (sLock) { + if (sNotedAppOpsCollector != null && uid == Process.myUid() && packageName.equals( + ActivityThread.currentOpPackageName())) { + // This app is noting an app-op for itself. Deliver immediately. + sNotedAppOpsCollector.onSelfNoted(new SyncNotedAppOp(code)); + } else if (binderUid != null && binderUid == uid) { + // We are inside of a two-way binder call. Delivered to caller via + // {@link #prefixParcelWithAppOpsIfNeeded} + long[] appOpsNotedInThisBinderTransaction; + + appOpsNotedInThisBinderTransaction = sAppOpsNotedInThisBinderTransaction.get(); + if (appOpsNotedInThisBinderTransaction == null) { + appOpsNotedInThisBinderTransaction = new long[2]; + sAppOpsNotedInThisBinderTransaction.set(appOpsNotedInThisBinderTransaction); + } + + if (code < 64) { + appOpsNotedInThisBinderTransaction[0] |= 1L << code; + } else { + appOpsNotedInThisBinderTransaction[1] |= 1L << (code - 64); + } + } else { + // We cannot deliver the note synchronous. Hence send it to the system server to + // notify the noted process. + if (message == null) { + // Default message is a stack trace + message = getFormattedStackTrace(); + } + + long token = Binder.clearCallingIdentity(); + try { + mService.noteAsyncOp(mContext.getOpPackageName(), uid, packageName, code, + message); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + } + + /** + * Append app-ops noted in the current two-way binder transaction to parcel. + * + * <p>This is called on the callee side of a two way binder transaction just before the + * transaction returns. + * + * @param p the parcel to append the noted app-ops to + * + * @hide + */ + public static void prefixParcelWithAppOpsIfNeeded(@NonNull Parcel p) { + long[] notedAppOps = sAppOpsNotedInThisBinderTransaction.get(); + if (notedAppOps == null || (notedAppOps[0] == 0 && notedAppOps[1] == 0)) { + return; + } + + p.writeInt(Parcel.EX_HAS_NOTED_APPOPS_REPLY_HEADER); + p.writeLong(notedAppOps[0]); + p.writeLong(notedAppOps[1]); + } + + /** + * Read app-ops noted during a two-way binder transaction from parcel. + * + * <p>This is called on the calling side of a two way binder transaction just after the + * transaction returns. + * + * <p>Note: Make sure to keep frameworks/native/libs/binder/Status.cpp::readAndLogNotedAppops + * in sync. + * + * @param p The parcel to read from + * + * @hide + */ + public static void readAndLogNotedAppops(@NonNull Parcel p) { + long[] rawNotedAppOps = new long[2]; + rawNotedAppOps[0] = p.readLong(); + rawNotedAppOps[1] = p.readLong(); + + if (rawNotedAppOps[0] != 0 || rawNotedAppOps[1] != 0) { + BitSet notedAppOps = BitSet.valueOf(rawNotedAppOps); + + synchronized (sLock) { + for (int code = notedAppOps.nextSetBit(0); code != -1; + code = notedAppOps.nextSetBit(code + 1)) { + if (sNotedAppOpsCollector != null) { + sNotedAppOpsCollector.onNoted(new SyncNotedAppOp(code)); + } + } + } + } + } + + /** + * Register a new {@link AppOpsCollector}. + * + * <p>There can only ever be one collector per process. If there currently is a collector + * registered, it will be unregistered. + * + * <p><b>Only appops related to dangerous permissions are collected.</b> + * + * @param collector The collector to set or {@code null} to unregister. + */ + public void setNotedAppOpsCollector(@Nullable AppOpsCollector collector) { + synchronized (sLock) { + if (sNotedAppOpsCollector != null) { + try { + mService.stopWatchingAsyncNoted(mContext.getPackageName(), + sNotedAppOpsCollector.mAsyncCb); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + sNotedAppOpsCollector = collector; + + if (sNotedAppOpsCollector != null) { + List<AsyncNotedAppOp> missedAsyncOps = null; + try { + mService.startWatchingAsyncNoted(mContext.getPackageName(), + sNotedAppOpsCollector.mAsyncCb); + missedAsyncOps = mService.extractAsyncOps(mContext.getPackageName()); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + if (missedAsyncOps != null) { + int numMissedAsyncOps = missedAsyncOps.size(); + for (int i = 0; i < numMissedAsyncOps; i++) { + final AsyncNotedAppOp asyncNotedAppOp = missedAsyncOps.get(i); + if (sNotedAppOpsCollector != null) { + sNotedAppOpsCollector.getAsyncNotedExecutor().execute( + () -> sNotedAppOpsCollector.onAsyncNoted( + asyncNotedAppOp)); + } + } + } + } + } + } + + /** + * @return {@code true} iff the process currently is currently collecting noted appops. + * + * @see #setNotedAppOpsCollector(AppOpsCollector) + * + * @hide + */ + public static boolean isCollectingNotedAppOps() { + synchronized (sLock) { + return sNotedAppOpsCollector != null; + } + } + + /** + * Callback an app can choose to {@link #setNotedAppOpsCollector register} to monitor it's noted + * appops. + * + * <p><b>Only appops related to dangerous permissions are collected.</b> + */ + public abstract static class AppOpsCollector { + /** Callback registered with the system. This will receive the async notes ops */ + private final IAppOpsAsyncNotedCallback mAsyncCb = new IAppOpsAsyncNotedCallback.Stub() { + @Override + public void opNoted(AsyncNotedAppOp op) { + Preconditions.checkNotNull(op); + + getAsyncNotedExecutor().execute(() -> onAsyncNoted(op)); + } + }; + + /** + * @return The executor for the system to use when calling {@link #onAsyncNoted}. + */ + public @NonNull Executor getAsyncNotedExecutor() { + return new HandlerExecutor(Handler.getMain()); + } + + /** + * Called when an app-op was noted for this package inside of a two-way binder-call. + * + * <p>Called on the calling thread just after executing the binder-call. This allows + * the app to e.g. collect stack traces to figure out where the access came from. + * + * @param op The op noted + */ + public abstract void onNoted(@NonNull SyncNotedAppOp op); + + /** + * Called when this app noted an app-op for its own package. + * + * <p>Called on the thread the noted the op. This allows the app to e.g. collect stack + * traces to figure out where the access came from. + * + * @param op The op noted + */ + public abstract void onSelfNoted(@NonNull SyncNotedAppOp op); + + /** + * Called when an app-op was noted for this package which cannot be delivered via the other + * two mechanisms. + * + * <p>Called as soon as possible after the app-op was noted, but the delivery delay is not + * guaranteed. Due to how async calls work in Android this might even be delivered slightly + * before the private data is delivered to the app. + * + * <p>If the app is not running or no {@link AppOpsCollector} is registered a small amount + * of noted app-ops are buffered and then delivered as soon as a collector is registered. + * + * @param asyncOp The op noted + */ + public abstract void onAsyncNoted(@NonNull AsyncNotedAppOp asyncOp); + } + + /** + * Generate a stack trace used for noted app-ops logging. + * + * <p>This strips away the first few and last few stack trace elements as they are not + * interesting to apps. + */ + private static String getFormattedStackTrace() { + StackTraceElement[] trace = new Exception().getStackTrace(); + + int firstInteresting = 0; + for (int i = 0; i < trace.length; i++) { + if (trace[i].getClassName().startsWith(AppOpsManager.class.getName()) + || trace[i].getClassName().startsWith(Parcel.class.getName()) + || trace[i].getClassName().contains("$Stub$Proxy") + || trace[i].getClassName().startsWith(DatabaseUtils.class.getName()) + || trace[i].getClassName().startsWith("android.content.ContentProviderProxy") + || trace[i].getClassName().startsWith(ContentResolver.class.getName())) { + firstInteresting = i; + } else { + break; + } + } + + int lastInteresting = trace.length - 1; + for (int i = trace.length - 1; i >= 0; i--) { + if (trace[i].getClassName().startsWith(HandlerThread.class.getName()) + || trace[i].getClassName().startsWith(Handler.class.getName()) + || trace[i].getClassName().startsWith(Looper.class.getName()) + || trace[i].getClassName().startsWith(Binder.class.getName()) + || trace[i].getClassName().startsWith(RuntimeInit.class.getName()) + || trace[i].getClassName().startsWith(ZygoteInit.class.getName()) + || trace[i].getClassName().startsWith(ActivityThread.class.getName()) + || trace[i].getClassName().startsWith(Method.class.getName()) + || trace[i].getClassName().startsWith("com.android.server.SystemServer")) { + lastInteresting = i; + } else { + break; + } + } + + StringBuilder sb = new StringBuilder(); + for (int i = firstInteresting; i <= lastInteresting; i++) { + sb.append(trace[i]); + if (i != lastInteresting) { + sb.append('\n'); + } + } + + return sb.toString(); + } + + /** * Checks whether the given op for a UID and package is active. * * <p> If you don't hold the {@link android.Manifest.permission#WATCH_APPOPS} permission @@ -5504,7 +6115,7 @@ public class AppOpsManager { * @see #startWatchingActive(int[], OnOpActiveChangedListener) * @see #stopWatchingMode(OnOpChangedListener) * @see #finishOp(int) - * @see #startOp(int) + * @see #startOp(int, int, String, boolean, String) * * @hide */ @TestApi diff --git a/core/java/android/app/AsyncNotedAppOp.aidl b/core/java/android/app/AsyncNotedAppOp.aidl new file mode 100644 index 000000000000..ebfefd0ba66c --- /dev/null +++ b/core/java/android/app/AsyncNotedAppOp.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +parcelable AsyncNotedAppOp; diff --git a/core/java/android/app/AsyncNotedAppOp.java b/core/java/android/app/AsyncNotedAppOp.java new file mode 100644 index 000000000000..64f886aa2f1d --- /dev/null +++ b/core/java/android/app/AsyncNotedAppOp.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcelable; + +import com.android.internal.annotations.Immutable; +import com.android.internal.util.DataClass; + +/** + * When an {@link AppOpsManager#noteOp(String, int, String, String) app-op is noted} and the + * app the app-op is noted for has a {@link AppOpsManager.AppOpsCollector} registered the note-event + * needs to be delivered to the collector. Usually this is done via an {@link SyncNotedAppOp}, but + * in some cases this is not possible. In this case an {@link AsyncNotedAppOp} is send to the system + * server and then forwarded to the {@link AppOpsManager.AppOpsCollector} in the app. + */ +@Immutable +@DataClass(genEqualsHashCode = true, + genAidl = true, + genHiddenConstructor = true) +// - We don't expose the opCode, but rather the public name of the op, hence use a non-standard +// getter +@DataClass.Suppress({"getOpCode"}) +public final class AsyncNotedAppOp implements Parcelable { + /** Op that was noted */ + private final @IntRange(from = 0, to = AppOpsManager._NUM_OP - 1) int mOpCode; + + /** Uid that noted the op */ + private final @IntRange(from = 0) int mNotingUid; + + /** + * Package that noted the op. {@code null} if the package name that noted the op could be not + * be determined (e.g. when the op is noted from native code). + */ + private final @Nullable String mNotingPackageName; + + /** Message associated with the noteOp. This message is set by the app noting the op */ + private final @NonNull String mMessage; + + /** Milliseconds since epoch when the op was noted */ + private final @IntRange(from = 0) long mTime; + + /** + * @return Op that was noted. + */ + public @NonNull String getOp() { + return AppOpsManager.opToPublicName(mOpCode); + } + + + + // Code below generated by codegen v1.0.0. + // + // DO NOT MODIFY! + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/app/AsyncNotedAppOp.java + // + // CHECKSTYLE:OFF Generated code + + /** + * Creates a new AsyncNotedAppOp. + * + * @param opCode + * Op that was noted + * @param notingUid + * Uid that noted the op + * @param notingPackageName + * Package that noted the op + * @param message + * Message associated with the noteOp. This message is set by the app noting the op + * @param time + * Milliseconds since epoch when the op was noted + * @hide + */ + @DataClass.Generated.Member + public AsyncNotedAppOp( + @IntRange(from = 0, to = AppOpsManager._NUM_OP - 1) int opCode, + @IntRange(from = 0) int notingUid, + @Nullable String notingPackageName, + @NonNull String message, + @IntRange(from = 0) long time) { + this.mOpCode = opCode; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mOpCode, + "from", 0, + "to", AppOpsManager._NUM_OP - 1); + this.mNotingUid = notingUid; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mNotingUid, + "from", 0); + this.mNotingPackageName = notingPackageName; + this.mMessage = message; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mMessage); + this.mTime = time; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mTime, + "from", 0); + + // onConstructed(); // You can define this method to get a callback + } + + /** + * Uid that noted the op + */ + @DataClass.Generated.Member + public @IntRange(from = 0) int getNotingUid() { + return mNotingUid; + } + + /** + * Package that noted the op + */ + @DataClass.Generated.Member + public @Nullable String getNotingPackageName() { + return mNotingPackageName; + } + + /** + * Message associated with the noteOp. This message is set by the app noting the op + */ + @DataClass.Generated.Member + public @NonNull String getMessage() { + return mMessage; + } + + /** + * Milliseconds since epoch when the op was noted + */ + @DataClass.Generated.Member + public @IntRange(from = 0) long getTime() { + return mTime; + } + + @Override + @DataClass.Generated.Member + public boolean equals(Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(AsyncNotedAppOp other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + AsyncNotedAppOp that = (AsyncNotedAppOp) o; + //noinspection PointlessBooleanExpression + return true + && mOpCode == that.mOpCode + && mNotingUid == that.mNotingUid + && java.util.Objects.equals(mNotingPackageName, that.mNotingPackageName) + && java.util.Objects.equals(mMessage, that.mMessage) + && mTime == that.mTime; + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + mOpCode; + _hash = 31 * _hash + mNotingUid; + _hash = 31 * _hash + java.util.Objects.hashCode(mNotingPackageName); + _hash = 31 * _hash + java.util.Objects.hashCode(mMessage); + _hash = 31 * _hash + Long.hashCode(mTime); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(android.os.Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mNotingPackageName != null) flg |= 0x4; + dest.writeByte(flg); + dest.writeInt(mOpCode); + dest.writeInt(mNotingUid); + if (mNotingPackageName != null) dest.writeString(mNotingPackageName); + dest.writeString(mMessage); + dest.writeLong(mTime); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<AsyncNotedAppOp> CREATOR + = new Parcelable.Creator<AsyncNotedAppOp>() { + @Override + public AsyncNotedAppOp[] newArray(int size) { + return new AsyncNotedAppOp[size]; + } + + @Override + @SuppressWarnings({"unchecked", "RedundantCast"}) + public AsyncNotedAppOp createFromParcel(android.os.Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + int opCode = in.readInt(); + int notingUid = in.readInt(); + String notingPackageName = (flg & 0x4) == 0 ? null : in.readString(); + String message = in.readString(); + long time = in.readLong(); + return new AsyncNotedAppOp( + opCode, + notingUid, + notingPackageName, + message, + time); + } + }; + + @DataClass.Generated( + time = 1566503083973L, + codegenVersion = "1.0.0", + sourceFile = "frameworks/base/core/java/android/app/AsyncNotedAppOp.java", + inputSignatures = "private final @android.annotation.IntRange(from=0L, to=90L) int mOpCode\nprivate final @android.annotation.IntRange(from=0L) int mNotingUid\nprivate final @android.annotation.Nullable java.lang.String mNotingPackageName\nprivate final @android.annotation.NonNull java.lang.String mMessage\nprivate final @android.annotation.IntRange(from=0L) long mTime\npublic @android.annotation.NonNull java.lang.String getOp()\nclass AsyncNotedAppOp extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genAidl=true, genHiddenConstructor=true)") + @Deprecated + private void __metadata() {} + +} diff --git a/core/java/android/app/SyncNotedAppOp.java b/core/java/android/app/SyncNotedAppOp.java new file mode 100644 index 000000000000..f7b83d409a02 --- /dev/null +++ b/core/java/android/app/SyncNotedAppOp.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.annotation.IntRange; +import android.annotation.NonNull; + +import com.android.internal.annotations.Immutable; + +/** + * Description of an app-op that was noted for the current process. + * + * <p>This is either delivered after a + * {@link AppOpsManager.AppOpsCollector#onNoted(SyncNotedAppOp) two way binder call} or + * when the app + * {@link AppOpsManager.AppOpsCollector#onSelfNoted(SyncNotedAppOp) notes an app-op for + * itself}. + */ +@Immutable +public final class SyncNotedAppOp { + private final int mOpCode; + + /** + * @return The op that was noted. + */ + public @NonNull String getOp() { + return AppOpsManager.opToPublicName(mOpCode); + } + + /** + * Create a new sync op description + * + * @param opCode The op that was noted + * + * @hide + */ + public SyncNotedAppOp(@IntRange(from = 0, to = AppOpsManager._NUM_OP - 1) int opCode) { + mOpCode = opCode; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof SyncNotedAppOp)) { + return false; + } + + return mOpCode == ((SyncNotedAppOp) other).mOpCode; + } + + @Override + public int hashCode() { + return mOpCode; + } +} diff --git a/core/java/android/content/PermissionChecker.java b/core/java/android/content/PermissionChecker.java index 6fe6e991fb1e..e24512ac525d 100644 --- a/core/java/android/content/PermissionChecker.java +++ b/core/java/android/content/PermissionChecker.java @@ -16,6 +16,8 @@ package android.content; +import static android.app.AppOpsManager.strOpToOp; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -75,6 +77,16 @@ public final class PermissionChecker { } /** + * @deprecated Use {@link #checkPermission(Context, String, int, int, String, String)} instead + */ + @Deprecated + @PermissionResult + public static int checkPermission(@NonNull Context context, @NonNull String permission, + int pid, int uid, @Nullable String packageName) { + return checkPermission(context, permission, pid, uid, packageName, null); + } + + /** * Checks whether a given package in a UID and PID has a given permission * and whether the app op that corresponds to this permission is allowed. * @@ -84,12 +96,14 @@ public final class PermissionChecker { * @param uid The uid for which to check. * @param packageName The package name for which to check. If null the * the first package for the calling UID will be used. + * @param message A message describing the reason the permission was checked + * * @return The permission check result which is either {@link #PERMISSION_GRANTED} * or {@link #PERMISSION_DENIED} or {@link #PERMISSION_DENIED_APP_OP}. */ @PermissionResult public static int checkPermission(@NonNull Context context, @NonNull String permission, - int pid, int uid, @Nullable String packageName) { + int pid, int uid, @Nullable String packageName, @Nullable String message) { if (context.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_DENIED) { return PERMISSION_DENIED; } @@ -108,7 +122,8 @@ public final class PermissionChecker { packageName = packageNames[0]; } - if (appOpsManager.noteProxyOpNoThrow(op, packageName, uid) != AppOpsManager.MODE_ALLOWED) { + if (appOpsManager.noteProxyOpNoThrow(strOpToOp(op), packageName, uid, message) + != AppOpsManager.MODE_ALLOWED) { return PERMISSION_DENIED_APP_OP; } @@ -131,7 +146,17 @@ public final class PermissionChecker { public static int checkSelfPermission(@NonNull Context context, @NonNull String permission) { return checkPermission(context, permission, Process.myPid(), - Process.myUid(), context.getPackageName()); + Process.myUid(), context.getPackageName(), null /* self access */); + } + + /** + * @deprecated Use {@link #checkCallingPermission(Context, String, String, String)} instead + */ + @Deprecated + @PermissionResult + public static int checkCallingPermission(@NonNull Context context, + @NonNull String permission, @Nullable String packageName) { + return checkCallingPermission(context, permission, packageName, null); } /** @@ -142,17 +167,29 @@ public final class PermissionChecker { * @param permission The permission to check. * @param packageName The package name making the IPC. If null the * the first package for the calling UID will be used. + * @param message A message describing the reason the permission was checked + * * @return The permission check result which is either {@link #PERMISSION_GRANTED} * or {@link #PERMISSION_DENIED} or {@link #PERMISSION_DENIED_APP_OP}. */ @PermissionResult public static int checkCallingPermission(@NonNull Context context, - @NonNull String permission, @Nullable String packageName) { + @NonNull String permission, @Nullable String packageName, @Nullable String message) { if (Binder.getCallingPid() == Process.myPid()) { return PERMISSION_DENIED; } return checkPermission(context, permission, Binder.getCallingPid(), - Binder.getCallingUid(), packageName); + Binder.getCallingUid(), packageName, message); + } + + /** + * @deprecated Use {@link #checkCallingOrSelfPermission(Context, String, String)} instead + */ + @Deprecated + @PermissionResult + public static int checkCallingOrSelfPermission(@NonNull Context context, + @NonNull String permission) { + return checkCallingOrSelfPermission(context, permission, null); } /** @@ -161,15 +198,17 @@ public final class PermissionChecker { * * @param context Context for accessing resources. * @param permission The permission to check. + * @param message A message describing the reason the permission was checked + * * @return The permission check result which is either {@link #PERMISSION_GRANTED} * or {@link #PERMISSION_DENIED} or {@link #PERMISSION_DENIED_APP_OP}. */ @PermissionResult public static int checkCallingOrSelfPermission(@NonNull Context context, - @NonNull String permission) { + @NonNull String permission, @Nullable String message) { String packageName = (Binder.getCallingPid() == Process.myPid()) ? context.getPackageName() : null; return checkPermission(context, permission, Binder.getCallingPid(), - Binder.getCallingUid(), packageName); + Binder.getCallingUid(), packageName, message); } } diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index 553372174a4d..2c9333bc1a7c 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.UnsupportedAppUsage; +import android.app.AppOpsManager; import android.util.ExceptionUtils; import android.util.Log; import android.util.Slog; @@ -1029,7 +1030,17 @@ public class Binder implements IBinder { Trace.traceBegin(Trace.TRACE_TAG_ALWAYS, getClass().getName() + ":" + (transactionName != null ? transactionName : code)); } - res = onTransact(code, data, reply, flags); + + if ((flags & FLAG_COLLECT_NOTED_APP_OPS) != 0) { + AppOpsManager.startNotedAppOpsCollection(callingUid); + try { + res = onTransact(code, data, reply, flags); + } finally { + AppOpsManager.finishNotedAppOpsCollection(); + } + } else { + res = onTransact(code, data, reply, flags); + } } catch (RemoteException|RuntimeException e) { if (observer != null) { observer.callThrewException(callSession, e); diff --git a/core/java/android/os/BinderProxy.java b/core/java/android/os/BinderProxy.java index c74cef85f3ea..b3b4f784a04a 100644 --- a/core/java/android/os/BinderProxy.java +++ b/core/java/android/os/BinderProxy.java @@ -18,6 +18,7 @@ package android.os; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.AppOpsManager; import android.util.Log; import android.util.SparseIntArray; @@ -506,9 +507,18 @@ public final class BinderProxy implements IBinder { } } + final AppOpsManager.PausedNotedAppOpsCollection prevCollection = + AppOpsManager.pauseNotedAppOpsCollection(); + + if ((flags & FLAG_ONEWAY) == 0 && AppOpsManager.isCollectingNotedAppOps()) { + flags |= FLAG_COLLECT_NOTED_APP_OPS; + } + try { return transactNative(code, data, reply, flags); } finally { + AppOpsManager.resumeNotedAppOpsCollection(prevCollection); + if (transactListener != null) { transactListener.onTransactEnded(session); } diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java index 83f88ad41cfc..12bce8a305f2 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -170,6 +170,11 @@ public interface IBinder { int FLAG_ONEWAY = 0x00000001; /** + * @hide + */ + int FLAG_COLLECT_NOTED_APP_OPS = 0x00000002; + + /** * Limit that should be placed on IPC sizes to keep them safely under the * transaction buffer limit. * @hide diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index e1b55422274a..50487e9e7a99 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; +import android.app.AppOpsManager; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -267,7 +268,9 @@ public final class Parcel { private static final int EX_UNSUPPORTED_OPERATION = -7; private static final int EX_SERVICE_SPECIFIC = -8; private static final int EX_PARCELABLE = -9; - private static final int EX_HAS_REPLY_HEADER = -128; // special; see below + /** @hide */ + public static final int EX_HAS_NOTED_APPOPS_REPLY_HEADER = -127; // special; see below + private static final int EX_HAS_STRICTMODE_REPLY_HEADER = -128; // special; see below // EX_TRANSACTION_FAILED is used exclusively in native code. // see libbinder's binder/Status.h private static final int EX_TRANSACTION_FAILED = -129; @@ -1866,6 +1869,8 @@ public final class Parcel { * @see #readException */ public final void writeException(@NonNull Exception e) { + AppOpsManager.prefixParcelWithAppOpsIfNeeded(this); + int code = 0; if (e instanceof Parcelable && (e.getClass().getClassLoader() == Parcelable.class.getClassLoader())) { @@ -1944,6 +1949,8 @@ public final class Parcel { * @see #readException */ public final void writeNoException() { + AppOpsManager.prefixParcelWithAppOpsIfNeeded(this); + // Despite the name of this function ("write no exception"), // it should instead be thought of as "write the RPC response // header", but because this function name is written out by @@ -1951,14 +1958,14 @@ public final class Parcel { // // The response header, in the non-exception case (see also // writeException above, also called by the AIDL compiler), is - // either a 0 (the default case), or EX_HAS_REPLY_HEADER if + // either a 0 (the default case), or EX_HAS_STRICTMODE_REPLY_HEADER if // StrictMode has gathered up violations that have occurred // during a Binder call, in which case we write out the number // of violations and their details, serialized, before the // actual RPC respons data. The receiving end of this is // readException(), below. if (StrictMode.hasGatheredViolations()) { - writeInt(EX_HAS_REPLY_HEADER); + writeInt(EX_HAS_STRICTMODE_REPLY_HEADER); final int sizePosition = dataPosition(); writeInt(0); // total size of fat header, to be filled in later StrictMode.writeGatheredViolationsToParcel(this); @@ -2005,7 +2012,13 @@ public final class Parcel { @TestApi public final int readExceptionCode() { int code = readInt(); - if (code == EX_HAS_REPLY_HEADER) { + if (code == EX_HAS_NOTED_APPOPS_REPLY_HEADER) { + AppOpsManager.readAndLogNotedAppops(this); + // Read next header or real exception if there is no more header + code = readInt(); + } + + if (code == EX_HAS_STRICTMODE_REPLY_HEADER) { int headerSize = readInt(); if (headerSize == 0) { Log.e(TAG, "Unexpected zero-sized Parcel reply header."); diff --git a/core/java/com/android/internal/app/IAppOpsAsyncNotedCallback.aidl b/core/java/com/android/internal/app/IAppOpsAsyncNotedCallback.aidl new file mode 100644 index 000000000000..163e4f7127fb --- /dev/null +++ b/core/java/com/android/internal/app/IAppOpsAsyncNotedCallback.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import android.app.AsyncNotedAppOp; + +// Interface to observe async noted appops +oneway interface IAppOpsAsyncNotedCallback { + void opNoted(in AsyncNotedAppOp op); +} diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 72dbbf3b8b87..fcf09a982463 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -17,12 +17,13 @@ package com.android.internal.app; import android.app.AppOpsManager; -import android.app.AppOpsManager; +import android.app.AsyncNotedAppOp; import android.content.pm.ParceledListSlice; import android.os.Bundle; import android.os.RemoteCallback; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsActiveCallback; +import com.android.internal.app.IAppOpsAsyncNotedCallback; import com.android.internal.app.IAppOpsNotedCallback; interface IAppOpsService { @@ -40,6 +41,9 @@ interface IAppOpsService { IBinder getToken(IBinder clientToken); int permissionToOpCode(String permission); int checkAudioOperation(int code, int usage, int uid, String packageName); + void noteAsyncOp(String callingPackageName, int uid, String packageName, int opCode, + String message); + boolean shouldCollectNotes(int opCode); // End of methods also called by native code. // Any new method exposed to native must be added after the last one, do not reorder @@ -82,6 +86,10 @@ interface IAppOpsService { void startWatchingNoted(in int[] ops, IAppOpsNotedCallback callback); void stopWatchingNoted(IAppOpsNotedCallback callback); + void startWatchingAsyncNoted(String packageName, IAppOpsAsyncNotedCallback callback); + void stopWatchingAsyncNoted(String packageName, IAppOpsAsyncNotedCallback callback); + List<AsyncNotedAppOp> extractAsyncOps(String packageName); + int checkOperationRaw(int code, int uid, String packageName); void reloadNonHistoricalState(); diff --git a/core/java/com/android/internal/util/AnnotationValidations.java b/core/java/com/android/internal/util/AnnotationValidations.java index c8afdd47f295..2d3b45023c9d 100644 --- a/core/java/com/android/internal/util/AnnotationValidations.java +++ b/core/java/com/android/internal/util/AnnotationValidations.java @@ -61,16 +61,52 @@ public class AnnotationValidations { } public static void validate(Class<IntRange> annotation, IntRange ignored, int value, - String paramName1, int param1, String paramName2, int param2) { + String paramName1, long param1, String paramName2, long param2) { validate(annotation, ignored, value, paramName1, param1); validate(annotation, ignored, value, paramName2, param2); } public static void validate(Class<IntRange> annotation, IntRange ignored, int value, - String paramName, int param) { + String paramName, long param) { switch (paramName) { - case "from": if (value < param) invalid(annotation, value, paramName, param); break; - case "to": if (value > param) invalid(annotation, value, paramName, param); break; + case "from": + if (value < param) { + invalid(annotation, value, paramName, param); + } + break; + case "to": + if (value > param) { + invalid(annotation, value, paramName, param); + } + break; + } + } + + /** + * Validate a long value with two parameters. + */ + public static void validate(Class<IntRange> annotation, IntRange ignored, long value, + String paramName1, long param1, String paramName2, long param2) { + validate(annotation, ignored, value, paramName1, param1); + validate(annotation, ignored, value, paramName2, param2); + } + + /** + * Validate a long value with one parameter. + */ + public static void validate(Class<IntRange> annotation, IntRange ignored, long value, + String paramName, long param) { + switch (paramName) { + case "from": + if (value < param) { + invalid(annotation, value, paramName, param); + } + break; + case "to": + if (value > param) { + invalid(annotation, value, paramName, param); + } + break; } } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 6d6a148c105d..6061589929d9 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -31,9 +31,11 @@ import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE_LOCATION; import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED; import static android.app.AppOpsManager.UID_STATE_PERSISTENT; import static android.app.AppOpsManager.UID_STATE_TOP; +import static android.app.AppOpsManager._NUM_OP; import static android.app.AppOpsManager.modeToName; import static android.app.AppOpsManager.opToName; import static android.app.AppOpsManager.resolveFirstUnrestrictedUidState; +import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS; import android.Manifest; import android.annotation.NonNull; @@ -49,6 +51,7 @@ import android.app.AppOpsManager.OpEntry; import android.app.AppOpsManager.OpFlags; import android.app.AppOpsManagerInternal; import android.app.AppOpsManagerInternal.CheckOpsDelegate; +import android.app.AsyncNotedAppOp; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -58,6 +61,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; +import android.content.pm.PermissionInfo; import android.content.pm.UserInfo; import android.database.ContentObserver; import android.media.AudioAttributes; @@ -69,6 +73,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Process; import android.os.RemoteCallback; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; @@ -86,6 +91,7 @@ import android.util.AtomicFile; import android.util.KeyValueListParser; import android.util.LongSparseArray; import android.util.LongSparseLongArray; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -96,6 +102,7 @@ import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IAppOpsActiveCallback; +import com.android.internal.app.IAppOpsAsyncNotedCallback; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsNotedCallback; import com.android.internal.app.IAppOpsService; @@ -181,6 +188,8 @@ public class AppOpsService extends IAppOpsService.Stub { OP_CAMERA, }; + private static final int MAX_UNFORWARED_OPS = 10; + Context mContext; final AtomicFile mFile; final Handler mHandler; @@ -188,6 +197,29 @@ public class AppOpsService extends IAppOpsService.Stub { private final AppOpsManagerInternalImpl mAppOpsManagerInternal = new AppOpsManagerInternalImpl(); + /** + * Registered callbacks, called from {@link #noteAsyncOp}. + * + * <p>(package name, uid) -> callbacks + * + * @see #getAsyncNotedOpsKey(String, int) + */ + @GuardedBy("this") + private final ArrayMap<Pair<String, Integer>, RemoteCallbackList<IAppOpsAsyncNotedCallback>> + mAsyncOpWatchers = new ArrayMap<>(); + + /** + * Async note-ops collected from {@link #noteAsyncOp} that have not been delivered to a + * callback yet. + * + * <p>(package name, uid) -> list<ops> + * + * @see #getAsyncNotedOpsKey(String, int) + */ + @GuardedBy("this") + private final ArrayMap<Pair<String, Integer>, ArrayList<AsyncNotedAppOp>> + mUnforwardedAsyncNotedOps = new ArrayMap<>(); + boolean mWriteScheduled; boolean mFastWriteScheduled; final Runnable mWriteRunner = new Runnable() { @@ -2098,6 +2130,141 @@ public class AppOpsService extends IAppOpsService.Stub { } @Override + public void noteAsyncOp(String callingPackageName, int uid, String packageName, int opCode, + String message) { + Preconditions.checkNotNull(message); + Preconditions.checkNotNull(packageName); + verifyAndGetIsPrivileged(uid, packageName); + + verifyIncomingUid(uid); + verifyIncomingOp(opCode); + + int callingUid = Binder.getCallingUid(); + long now = System.currentTimeMillis(); + + if (callingPackageName != null) { + verifyAndGetIsPrivileged(callingUid, callingPackageName); + } + + long token = Binder.clearCallingIdentity(); + try { + synchronized (this) { + Pair<String, Integer> key = getAsyncNotedOpsKey(packageName, uid); + + RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key); + AsyncNotedAppOp asyncNotedOp = new AsyncNotedAppOp(opCode, callingUid, + callingPackageName, message, now); + final boolean[] wasNoteForwarded = {false}; + + if (callbacks != null) { + callbacks.broadcast((cb) -> { + try { + cb.opNoted(asyncNotedOp); + wasNoteForwarded[0] = true; + } catch (RemoteException e) { + Slog.e(TAG, + "Could not forward noteOp of " + opCode + " to " + packageName + + "/" + uid, e); + } + }); + } + + if (!wasNoteForwarded[0]) { + ArrayList<AsyncNotedAppOp> unforwardedOps = mUnforwardedAsyncNotedOps.get(key); + if (unforwardedOps == null) { + unforwardedOps = new ArrayList<>(1); + mUnforwardedAsyncNotedOps.put(key, unforwardedOps); + } + + unforwardedOps.add(asyncNotedOp); + if (unforwardedOps.size() > MAX_UNFORWARED_OPS) { + unforwardedOps.remove(0); + } + } + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Compute a key to be used in {@link #mAsyncOpWatchers} and {@link #mUnforwardedAsyncNotedOps} + * + * @param packageName The package name of the app + * @param uid The uid of the app + * + * @return They key uniquely identifying the app + */ + private @NonNull Pair<String, Integer> getAsyncNotedOpsKey(@NonNull String packageName, + int uid) { + return new Pair<>(packageName, uid); + } + + @Override + public void startWatchingAsyncNoted(String packageName, IAppOpsAsyncNotedCallback callback) { + Preconditions.checkNotNull(packageName); + Preconditions.checkNotNull(callback); + + int uid = Binder.getCallingUid(); + Pair<String, Integer> key = getAsyncNotedOpsKey(packageName, uid); + + verifyAndGetIsPrivileged(uid, packageName); + + synchronized (this) { + RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key); + if (callbacks == null) { + callbacks = new RemoteCallbackList<IAppOpsAsyncNotedCallback>() { + @Override + public void onCallbackDied(IAppOpsAsyncNotedCallback callback) { + synchronized (AppOpsService.this) { + if (getRegisteredCallbackCount() == 0) { + mAsyncOpWatchers.remove(key); + } + } + } + }; + mAsyncOpWatchers.put(key, callbacks); + } + + callbacks.register(callback); + } + } + + @Override + public void stopWatchingAsyncNoted(String packageName, IAppOpsAsyncNotedCallback callback) { + Preconditions.checkNotNull(packageName); + Preconditions.checkNotNull(callback); + + int uid = Binder.getCallingUid(); + Pair<String, Integer> key = getAsyncNotedOpsKey(packageName, uid); + + verifyAndGetIsPrivileged(uid, packageName); + + synchronized (this) { + RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key); + if (callbacks != null) { + callbacks.unregister(callback); + if (callbacks.getRegisteredCallbackCount() == 0) { + mAsyncOpWatchers.remove(key); + } + } + } + } + + @Override + public List<AsyncNotedAppOp> extractAsyncOps(String packageName) { + Preconditions.checkNotNull(packageName); + + int uid = Binder.getCallingUid(); + + verifyAndGetIsPrivileged(uid, packageName); + + synchronized (this) { + return mUnforwardedAsyncNotedOps.remove(getAsyncNotedOpsKey(packageName, uid)); + } + } + + @Override public int startOperation(IBinder token, int code, int uid, String packageName, boolean startIfModeDefault) { verifyIncomingUid(uid); @@ -2340,6 +2507,25 @@ public class AppOpsService extends IAppOpsService.Stub { return AppOpsManager.permissionToOpCode(permission); } + @Override + public boolean shouldCollectNotes(int opCode) { + Preconditions.checkArgumentInRange(opCode, 0, _NUM_OP - 1, "opCode"); + + String perm = AppOpsManager.opToPermission(opCode); + if (perm == null) { + return false; + } + + PermissionInfo permInfo; + try { + permInfo = mContext.getPackageManager().getPermissionInfo(perm, 0); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + + return permInfo.getProtection() == PROTECTION_DANGEROUS; + } + void finishOperationLocked(Op op, boolean finishNested) { final int opCode = op.op; final int uid = op.uidState.uid; |