From 90516b9200c331793f8d1864aba6573c976bcc4b Mon Sep 17 00:00:00 2001 From: Stanislav Zholnin Date: Mon, 20 Jan 2020 14:03:06 +0000 Subject: [PATCH] Stack Trace sampling and reporting. Sampling is done in cycles where length of single cycle is configurable server side. During one sampling cycle only one stack trace / message is reported. Two sampling strategies are used in this CL. 1. Sampling uniformly packages and appops, and waiting only for messages coming from sampled package (all other packages are silenced to presereve system resources). All appops are recorded, but sampled appop has preference and replaces any previous stack trace / message. This strategy is created to focus on all packages and all apps uniformly. 2. Creating list of rarely used packages - packages which were not using any dangerous permissions for more than a week. Any newly installed app is automatically added to this list, including apps which were updated. Whenever message is received for rarely used package, there is 50% probability that this message will replace whatever is being sampled in current sampling cycle. This strategy is created to focus on newly installed apps, on apps which are used infrequently and on apps which use dangerous permissions infrequently. Bug:136134050 Test: atest android.app.appops.cts.RuntimeMessageCollectionTest Change-Id: I3109b38bf0482481acf945d5441a26bfe704c9b5 --- api/current.txt | 6 +- api/system-current.txt | 14 + api/test-current.txt | 14 + core/java/android/app/AppOpsManager.java | 122 ++++++- .../app/RuntimeAppOpAccessMessage.aidl | 19 ++ .../app/RuntimeAppOpAccessMessage.java | 245 +++++++++++++++ core/java/android/app/SyncNotedAppOp.aidl | 19 ++ core/java/android/app/SyncNotedAppOp.java | 154 +++++++-- .../android/internal/app/IAppOpsService.aidl | 6 + .../internal/app/MessageSamplingConfig.aidl | 19 ++ .../internal/app/MessageSamplingConfig.java | 187 +++++++++++ .../android/server/appop/AppOpsService.java | 297 ++++++++++++++++++ 12 files changed, 1075 insertions(+), 27 deletions(-) create mode 100644 core/java/android/app/RuntimeAppOpAccessMessage.aidl create mode 100644 core/java/android/app/RuntimeAppOpAccessMessage.java create mode 100644 core/java/android/app/SyncNotedAppOp.aidl create mode 100644 core/java/com/android/internal/app/MessageSamplingConfig.aidl create mode 100644 core/java/com/android/internal/app/MessageSamplingConfig.java diff --git a/api/current.txt b/api/current.txt index 5e8974fb82d70..0db2d97cab9b6 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6426,9 +6426,13 @@ package android.app { public class StatusBarManager { } - public final class SyncNotedAppOp { + public final class SyncNotedAppOp implements android.os.Parcelable { + ctor public SyncNotedAppOp(@IntRange(from=0L) int, @Nullable String); + method public int describeContents(); method @Nullable public String getFeatureId(); method @NonNull public String getOp(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; } @Deprecated public class TabActivity extends android.app.ActivityGroup { diff --git a/api/system-current.txt b/api/system-current.txt index 8725538f1fd8e..744ff6d15e6ea 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -364,6 +364,7 @@ package android.app { } public class AppOpsManager { + method @Nullable @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) public android.app.RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage(); method @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) public void getHistoricalOps(@NonNull android.app.AppOpsManager.HistoricalOpsRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer); method public static String[] getOpStrs(); method @NonNull @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) public java.util.List getOpsForPackage(int, @NonNull String, @Nullable java.lang.String...); @@ -680,6 +681,19 @@ package android.app { method public void setNotificationAssistantAccessGranted(@Nullable android.content.ComponentName, boolean); } + public final class RuntimeAppOpAccessMessage implements android.os.Parcelable { + ctor public RuntimeAppOpAccessMessage(@IntRange(from=0L) int, @IntRange(from=0L) int, @NonNull String, @Nullable String, @NonNull String, int); + method public int describeContents(); + method @Nullable public String getFeatureId(); + method @NonNull public String getMessage(); + method @NonNull public String getOp(); + method @NonNull public String getPackageName(); + method public int getSamplingStrategy(); + method @IntRange(from=0L) public int getUid(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + public class SearchManager implements android.content.DialogInterface.OnCancelListener android.content.DialogInterface.OnDismissListener { method public void launchAssist(@Nullable android.os.Bundle); } diff --git a/api/test-current.txt b/api/test-current.txt index 4c8bb0290caef..7284fe64f74ea 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -164,6 +164,7 @@ package android.app { public class AppOpsManager { method @RequiresPermission("android.permission.MANAGE_APPOPS") public void addHistoricalOps(@NonNull android.app.AppOpsManager.HistoricalOps); method @RequiresPermission("android.permission.MANAGE_APPOPS") public void clearHistory(); + method @Nullable @RequiresPermission("android.permission.GET_APP_OPS_STATS") public android.app.RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage(); method @RequiresPermission("android.permission.GET_APP_OPS_STATS") public void getHistoricalOps(@NonNull android.app.AppOpsManager.HistoricalOpsRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer); method @RequiresPermission("android.permission.MANAGE_APPOPS") public void getHistoricalOpsFromDiskRaw(@NonNull android.app.AppOpsManager.HistoricalOpsRequest, @Nullable java.util.concurrent.Executor, @NonNull java.util.function.Consumer); method public static int getNumOps(); @@ -447,6 +448,19 @@ package android.app { method public android.graphics.Rect getSourceRectHint(); } + public final class RuntimeAppOpAccessMessage implements android.os.Parcelable { + ctor public RuntimeAppOpAccessMessage(@IntRange(from=0L) int, @IntRange(from=0L) int, @NonNull String, @Nullable String, @NonNull String, int); + method public int describeContents(); + method @Nullable public String getFeatureId(); + method @NonNull public String getMessage(); + method @NonNull public String getOp(); + method @NonNull public String getPackageName(); + method public int getSamplingStrategy(); + method @IntRange(from=0L) public int getUid(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + public class StatusBarManager { method public void collapsePanels(); method public void expandNotificationsPanel(); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 1de68ba56c6ac..f6bbc6824e710 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -16,6 +16,10 @@ package android.app; +import static android.util.StatsLogInternal.RUNTIME_APP_OP_ACCESS__SAMPLING_STRATEGY__DEFAULT; +import static android.util.StatsLogInternal.RUNTIME_APP_OP_ACCESS__SAMPLING_STRATEGY__RARELY_USED; +import static android.util.StatsLogInternal.RUNTIME_APP_OP_ACCESS__SAMPLING_STRATEGY__UNIFORM; + import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.IntDef; @@ -48,6 +52,8 @@ import android.os.Parcelable; import android.os.Process; import android.os.RemoteCallback; import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; import android.os.UserManager; import android.util.ArrayMap; import android.util.ArraySet; @@ -63,6 +69,7 @@ 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.app.MessageSamplingConfig; import com.android.internal.os.RuntimeInit; import com.android.internal.os.ZygoteInit; import com.android.internal.util.ArrayUtils; @@ -141,6 +148,13 @@ public class AppOpsManager { @UnsupportedAppUsage final IAppOpsService mService; + /** + * Service for the application context, to be used by static methods via + * {@link #getService()} + */ + @GuardedBy("sLock") + static IAppOpsService sService; + @GuardedBy("mModeWatchers") private final ArrayMap mModeWatchers = new ArrayMap<>(); @@ -159,6 +173,50 @@ public class AppOpsManager { @GuardedBy("sLock") private static @Nullable AppOpsCollector sNotedAppOpsCollector; + /** + * Additional collector that collect accesses and forwards a few of them them via + * {@link IAppOpsService#reportRuntimeAppOpAccessMessageAndGetConfig}. + */ + private static AppOpsCollector sMessageCollector = + new AppOpsCollector() { + @Override + public void onNoted(@NonNull SyncNotedAppOp op) { + reportStackTraceIfNeeded(op); + } + + @Override + public void onAsyncNoted(@NonNull AsyncNotedAppOp asyncOp) { + // collected directly in AppOpsService + } + + @Override + public void onSelfNoted(@NonNull SyncNotedAppOp op) { + reportStackTraceIfNeeded(op); + } + + private void reportStackTraceIfNeeded(@NonNull SyncNotedAppOp op) { + if (sConfig.getSampledOpCode() == OP_NONE + && sConfig.getExpirationTimeSinceBootMillis() + >= SystemClock.elapsedRealtime()) { + return; + } + + MessageSamplingConfig config = sConfig; + if (leftCircularDistance(strOpToOp(op.getOp()), config.getSampledOpCode(), + _NUM_OP) <= config.getAcceptableLeftDistance() + || config.getExpirationTimeSinceBootMillis() + < SystemClock.elapsedRealtime()) { + String stackTrace = getFormattedStackTrace(); + try { + sConfig = getService().reportRuntimeAppOpAccessMessageAndGetConfig( + ActivityThread.currentOpPackageName(), op, stackTrace); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + } + }; + static IBinder sClientId; /** @@ -550,7 +608,6 @@ public class AppOpsManager { }) public @interface OpFlags {} - /** @hide */ public static final String getFlagName(@OpFlags int flag) { switch (flag) { @@ -569,6 +626,18 @@ public class AppOpsManager { } } + /** + * Strategies used for message sampling + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"RUNTIME_APP_OPS_ACCESS__SAMPLING_STRATEGY__"}, value = { + RUNTIME_APP_OP_ACCESS__SAMPLING_STRATEGY__DEFAULT, + RUNTIME_APP_OP_ACCESS__SAMPLING_STRATEGY__UNIFORM, + RUNTIME_APP_OP_ACCESS__SAMPLING_STRATEGY__RARELY_USED + }) + public @interface SamplingStrategy {} + private static final int UID_STATE_OFFSET = 31; private static final int FLAGS_MASK = 0xFFFFFFFF; @@ -2225,6 +2294,10 @@ public class AppOpsManager { } } + /** Config used to control app ops access messages sampling */ + private static MessageSamplingConfig sConfig = + new MessageSamplingConfig(OP_NONE, 0, 0); + /** @hide */ public static final String KEY_HISTORICAL_OPS = "historical_ops"; @@ -7268,6 +7341,17 @@ public class AppOpsManager { } } + /** @hide */ + private static IAppOpsService getService() { + synchronized (sLock) { + if (sService == null) { + sService = IAppOpsService.Stub.asInterface( + ServiceManager.getService(Context.APP_OPS_SERVICE)); + } + return sService; + } + } + /** * @deprecated use {@link #startOp(String, int, String, String, String)} instead */ @@ -7614,6 +7698,7 @@ public class AppOpsManager { sNotedAppOpsCollector.onSelfNoted(new SyncNotedAppOp(op, featureId)); } } + sMessageCollector.onSelfNoted(new SyncNotedAppOp(op, featureId)); } /** @@ -7764,6 +7849,10 @@ public class AppOpsManager { } } } + for (int code = notedAppOps.nextSetBit(0); code != -1; + code = notedAppOps.nextSetBit(code + 1)) { + sMessageCollector.onNoted(new SyncNotedAppOp(code, featureId)); + } } } } @@ -7958,10 +8047,13 @@ public class AppOpsManager { StringBuilder sb = new StringBuilder(); for (int i = firstInteresting; i <= lastInteresting; i++) { - sb.append(trace[i]); - if (i != lastInteresting) { + if (i != firstInteresting) { sb.append('\n'); } + if (sb.length() + trace[i].toString().length() > 600) { + break; + } + sb.append(trace[i]); } return sb.toString(); @@ -8088,6 +8180,22 @@ public class AppOpsManager { } } + /** + * Pulls current AppOps access report and picks package and op to watch for next access report + * + * @hide + */ + @SystemApi + @TestApi + @RequiresPermission(Manifest.permission.GET_APP_OPS_STATS) + public @Nullable RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage() { + try { + return mService.collectRuntimeAppOpAccessMessage(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Returns all supported operation names. * @hide @@ -8297,4 +8405,12 @@ public class AppOpsManager { return AppOpsManager.MODE_DEFAULT; } + + /** + * Calculate left circular distance for two numbers modulo size. + * @hide + */ + public static int leftCircularDistance(int from, int to, int size) { + return (to + size - from) % size; + } } diff --git a/core/java/android/app/RuntimeAppOpAccessMessage.aidl b/core/java/android/app/RuntimeAppOpAccessMessage.aidl new file mode 100644 index 0000000000000..68e8819a3a8b5 --- /dev/null +++ b/core/java/android/app/RuntimeAppOpAccessMessage.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 RuntimeAppOpAccessMessage; diff --git a/core/java/android/app/RuntimeAppOpAccessMessage.java b/core/java/android/app/RuntimeAppOpAccessMessage.java new file mode 100644 index 0000000000000..a81b8e7ab13c3 --- /dev/null +++ b/core/java/android/app/RuntimeAppOpAccessMessage.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.annotation.SystemApi; +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.annotations.Immutable; +import com.android.internal.util.DataClass; + +/** + * Message for noted runtime permission access. + * @hide + */ +@Immutable +@TestApi +@SystemApi +/*@DataClass(genConstructor = false) +@DataClass.Suppress("getOpCode")*/ +public final class RuntimeAppOpAccessMessage implements Parcelable { + /** Uid of package for which runtime app op access message was collected */ + private final @IntRange(from = 0L) int mUid; + /** Op code of operation access which was collected */ + private final @IntRange(from = 0L, to = AppOpsManager._NUM_OP - 1) int mOpCode; + /** Name of package for which runtime app op access message was collected */ + private final @NonNull String mPackageName; + /** Feature of package for which runtime app op access message was collected */ + private final @Nullable String mFeatureId; + /** Message collected (including stacktrace for synchronous ops) */ + private final @NonNull String mMessage; + /** Sampling strategy used to collect this message. */ + private final @AppOpsManager.SamplingStrategy int mSamplingStrategy; + + public @NonNull String getOp() { + return AppOpsManager.opToPublicName(mOpCode); + } + + /** + * Creates a new RuntimeAppOpAccessMessage. + * + * @param uid + * Uid of package for which runtime app op access message was collected + * @param opCode + * Op code of operation access which was collected + * @param packageName + * Name of package for which runtime app op access message was collected + * @param featureId + * Feature of package for which runtime app op access message was collected + * @param message + * Message collected (including stacktrace for synchronous ops) + * @param samplingStrategy + * Sampling strategy used to collect this message. + */ + @DataClass.Generated.Member + public RuntimeAppOpAccessMessage( + @IntRange(from = 0L) int uid, + @IntRange(from = 0L) int opCode, + @NonNull String packageName, + @Nullable String featureId, + @NonNull String message, + @AppOpsManager.SamplingStrategy int samplingStrategy) { + this.mUid = uid; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mUid, + "from", 0L); + this.mOpCode = opCode; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mOpCode, + "from", 0L, + "to", AppOpsManager._NUM_OP - 1); + this.mPackageName = packageName; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mPackageName); + this.mFeatureId = featureId; + this.mMessage = message; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mMessage); + this.mSamplingStrategy = samplingStrategy; + com.android.internal.util.AnnotationValidations.validate( + AppOpsManager.SamplingStrategy.class, null, mSamplingStrategy); + + // onConstructed(); // You can define this method to get a callback + } + + + + + // Code below generated by codegen v1.0.14. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/app/RuntimeAppOpAccessMessage.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** + * Uid of package for which runtime app op access message was collected + */ + @DataClass.Generated.Member + public @IntRange(from = 0L) int getUid() { + return mUid; + } + + /** + * Name of package for which runtime app op access message was collected + */ + @DataClass.Generated.Member + public @NonNull String getPackageName() { + return mPackageName; + } + + /** + * Feature of package for which runtime app op access message was collected + */ + @DataClass.Generated.Member + public @Nullable String getFeatureId() { + return mFeatureId; + } + + /** + * Message collected (including stacktrace for synchronous ops) + */ + @DataClass.Generated.Member + public @NonNull String getMessage() { + return mMessage; + } + + /** + * Sampling strategy used to collect this message. + */ + @DataClass.Generated.Member + public @AppOpsManager.SamplingStrategy int getSamplingStrategy() { + return mSamplingStrategy; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mFeatureId != null) flg |= 0x8; + dest.writeByte(flg); + dest.writeInt(mUid); + dest.writeInt(mOpCode); + dest.writeString(mPackageName); + if (mFeatureId != null) dest.writeString(mFeatureId); + dest.writeString(mMessage); + dest.writeInt(mSamplingStrategy); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ RuntimeAppOpAccessMessage(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + int uid = in.readInt(); + int opCode = in.readInt(); + String packageName = in.readString(); + String featureId = (flg & 0x8) == 0 ? null : in.readString(); + String message = in.readString(); + int samplingStrategy = in.readInt(); + + this.mUid = uid; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mUid, + "from", 0L); + this.mOpCode = opCode; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mOpCode, + "from", 0L, + "to", AppOpsManager._NUM_OP - 1); + this.mPackageName = packageName; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mPackageName); + this.mFeatureId = featureId; + this.mMessage = message; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mMessage); + this.mSamplingStrategy = samplingStrategy; + com.android.internal.util.AnnotationValidations.validate( + AppOpsManager.SamplingStrategy.class, null, mSamplingStrategy); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public RuntimeAppOpAccessMessage[] newArray(int size) { + return new RuntimeAppOpAccessMessage[size]; + } + + @Override + public RuntimeAppOpAccessMessage createFromParcel(@NonNull Parcel in) { + return new RuntimeAppOpAccessMessage(in); + } + }; + + /*@DataClass.Generated( + time = 1581517099127L, + codegenVersion = "1.0.14", + sourceFile = "frameworks/base/core/java/android/app/RuntimeAppOpAccessMessage.java", + inputSignatures = "private final @android.annotation.IntRange(from=0L) int mUid\nprivate final @android.annotation.IntRange(from=0L, to=AppOpsManager._NUM_OP - 1) int mOpCode\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mFeatureId\nprivate final @android.annotation.NonNull java.lang.String mMessage\nprivate final @android.app.AppOpsManager.SamplingStrategy int mSamplingStrategy\npublic @android.annotation.NonNull java.lang.String getOp()\nclass RuntimeAppOpAccessMessage extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false)")*/ + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/app/SyncNotedAppOp.aidl b/core/java/android/app/SyncNotedAppOp.aidl new file mode 100644 index 0000000000000..ab062d241ac33 --- /dev/null +++ b/core/java/android/app/SyncNotedAppOp.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 SyncNotedAppOp; diff --git a/core/java/android/app/SyncNotedAppOp.java b/core/java/android/app/SyncNotedAppOp.java index 065d5de368ae7..aa11b9510c94e 100644 --- a/core/java/android/app/SyncNotedAppOp.java +++ b/core/java/android/app/SyncNotedAppOp.java @@ -19,8 +19,10 @@ 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; /** * Description of an app-op that was noted for the current process. @@ -32,10 +34,35 @@ import com.android.internal.annotations.Immutable; * itself}. */ @Immutable -public final class SyncNotedAppOp { - private final int mOpCode; +/*@DataClass( + genEqualsHashCode = true, + genConstructor = false +) +@DataClass.Suppress("getOpCode")*/ +public final class SyncNotedAppOp implements Parcelable { + + /** op code of synchronous appop noted */ + private final @IntRange(from = 0L, to = AppOpsManager._NUM_OP - 1) int mOpCode; + /** featureId of synchronous appop noted */ private final @Nullable String mFeatureId; + /** + * Creates a new SyncNotedAppOp. + * + * @param opCode + * op code of synchronous appop noted + * @param featureId + * featureId of synchronous appop noted + */ + public SyncNotedAppOp(@IntRange(from = 0L) int opCode, @Nullable String featureId) { + this.mOpCode = opCode; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mOpCode, + "from", 0, + "to", AppOpsManager._NUM_OP - 1); + this.mFeatureId = featureId; + } + /** * @return The op that was noted. */ @@ -43,37 +70,118 @@ public final class SyncNotedAppOp { return AppOpsManager.opToPublicName(mOpCode); } + // Code below generated by codegen v1.0.14. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/app/SyncNotedAppOp.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + /** - * @return The {@link android.content.Context#createFeatureContext Feature} in the app + * featureId of synchronous appop noted */ + @DataClass.Generated.Member public @Nullable String getFeatureId() { return mFeatureId; } - /** - * 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, - @Nullable String featureId) { - mOpCode = opCode; - mFeatureId = featureId; + @Override + @DataClass.Generated.Member + public boolean equals(@Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(SyncNotedAppOp other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + SyncNotedAppOp that = (SyncNotedAppOp) o; + //noinspection PointlessBooleanExpression + return true + && mOpCode == that.mOpCode + && java.util.Objects.equals(mFeatureId, that.mFeatureId); } @Override - public boolean equals(Object other) { - if (!(other instanceof SyncNotedAppOp)) { - return false; + @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 + java.util.Objects.hashCode(mFeatureId); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull 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 (mFeatureId != null) flg |= 0x2; + dest.writeByte(flg); + dest.writeInt(mOpCode); + if (mFeatureId != null) dest.writeString(mFeatureId); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ SyncNotedAppOp(@NonNull 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(); + String featureId = (flg & 0x2) == 0 ? null : in.readString(); + + this.mOpCode = opCode; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mOpCode, + "from", 0, + "to", AppOpsManager._NUM_OP - 1); + this.mFeatureId = featureId; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public SyncNotedAppOp[] newArray(int size) { + return new SyncNotedAppOp[size]; } - return mOpCode == ((SyncNotedAppOp) other).mOpCode; - } + @Override + public SyncNotedAppOp createFromParcel(@NonNull android.os.Parcel in) { + return new SyncNotedAppOp(in); + } + }; + + /*@DataClass.Generated( + time = 1579188889960L, + codegenVersion = "1.0.14", + sourceFile = "frameworks/base/core/java/android/app/SyncNotedAppOp.java", + inputSignatures = "private final @android.annotation.IntRange(from=0L, to=AppOpsManager._NUM_OP - 1) int mOpCode\nprivate final @android.annotation.Nullable java.lang.String mFeatureId\npublic @android.annotation.NonNull java.lang.String getOp()\npublic @android.annotation.SystemApi int getOpCode()\nclass SyncNotedAppOp extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genConstructor=false)")*/ + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code - @Override - public int hashCode() { - return mOpCode; - } } diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index dabaf5a8263ca..1c1c25459b663 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -18,6 +18,8 @@ package com.android.internal.app; import android.app.AppOpsManager; import android.app.AsyncNotedAppOp; +import android.app.SyncNotedAppOp; +import android.app.RuntimeAppOpAccessMessage; import android.content.pm.ParceledListSlice; import android.os.Bundle; import android.os.RemoteCallback; @@ -25,6 +27,7 @@ import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsActiveCallback; import com.android.internal.app.IAppOpsAsyncNotedCallback; import com.android.internal.app.IAppOpsNotedCallback; +import com.android.internal.app.MessageSamplingConfig; interface IAppOpsService { // These methods are also called by native code, so must @@ -54,6 +57,9 @@ interface IAppOpsService { // Remaining methods are only used in Java. int checkPackage(int uid, String packageName); + RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage(); + MessageSamplingConfig reportRuntimeAppOpAccessMessageAndGetConfig(String packageName, + in SyncNotedAppOp appOp, String message); @UnsupportedAppUsage List getPackagesForOps(in int[] ops); @UnsupportedAppUsage diff --git a/core/java/com/android/internal/app/MessageSamplingConfig.aidl b/core/java/com/android/internal/app/MessageSamplingConfig.aidl new file mode 100644 index 0000000000000..ab89ca2659811 --- /dev/null +++ b/core/java/com/android/internal/app/MessageSamplingConfig.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 com.android.internal.app; + +parcelable MessageSamplingConfig; diff --git a/core/java/com/android/internal/app/MessageSamplingConfig.java b/core/java/com/android/internal/app/MessageSamplingConfig.java new file mode 100644 index 0000000000000..5300c1c21e60e --- /dev/null +++ b/core/java/com/android/internal/app/MessageSamplingConfig.java @@ -0,0 +1,187 @@ +/* + * 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.annotation.IntRange; +import android.annotation.NonNull; +import android.app.AppOpsManager; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.annotations.Immutable; +import com.android.internal.util.DataClass; + +/** + * Configuration for AppOps access messages sampling. + */ +@Immutable +/*@DataClass*/ +public final class MessageSamplingConfig implements Parcelable { + /** Op code targeted during current sampling session */ + private final @IntRange(from = -1L, to = AppOpsManager._NUM_OP - 1) int mSampledOpCode; + /** Range of ops which should be reported during current sampling session */ + private final @IntRange(from = 0L, to = AppOpsManager._NUM_OP - 1) int mAcceptableLeftDistance; + /** Expiration time for this sampling config */ + private final @IntRange(from = 0L) long mExpirationTimeSinceBootMillis; + + + + + + + + // Code below generated by codegen v1.0.14. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/com/android/internal/app/MessageSamplingConfig.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** + * Creates a new MessageSamplingConfig. + * + * @param sampledOpCode + * Op code targeted during current sampling session + * @param acceptableLeftDistance + * Range of ops which should be reported during current sampling session + * @param expirationTimeSinceBootMillis + * Expiration time for this sampling config + */ + @DataClass.Generated.Member + public MessageSamplingConfig( + @IntRange(from = -1L, to = AppOpsManager._NUM_OP - 1) int sampledOpCode, + @IntRange(from = 0L, to = AppOpsManager._NUM_OP - 1) int acceptableLeftDistance, + @IntRange(from = 0L) long expirationTimeSinceBootMillis) { + this.mSampledOpCode = sampledOpCode; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mSampledOpCode, + "from", -1L, + "to", AppOpsManager._NUM_OP - 1); + this.mAcceptableLeftDistance = acceptableLeftDistance; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mAcceptableLeftDistance, + "from", 0L, + "to", AppOpsManager._NUM_OP - 1); + this.mExpirationTimeSinceBootMillis = expirationTimeSinceBootMillis; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mExpirationTimeSinceBootMillis, + "from", 0L); + + // onConstructed(); // You can define this method to get a callback + } + + /** + * Op code targeted during current sampling session + */ + @DataClass.Generated.Member + public @IntRange(from = -1L, to = AppOpsManager._NUM_OP - 1) int getSampledOpCode() { + return mSampledOpCode; + } + + /** + * Range of ops which should be reported during current sampling session + */ + @DataClass.Generated.Member + public @IntRange(from = 0L, to = AppOpsManager._NUM_OP - 1) int getAcceptableLeftDistance() { + return mAcceptableLeftDistance; + } + + /** + * Expiration time for this sampling config + */ + @DataClass.Generated.Member + public @IntRange(from = 0L) long getExpirationTimeSinceBootMillis() { + return mExpirationTimeSinceBootMillis; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeInt(mSampledOpCode); + dest.writeInt(mAcceptableLeftDistance); + dest.writeLong(mExpirationTimeSinceBootMillis); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ MessageSamplingConfig(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + int sampledOpCode = in.readInt(); + int acceptableLeftDistance = in.readInt(); + long expirationTimeSinceBootMillis = in.readLong(); + + this.mSampledOpCode = sampledOpCode; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mSampledOpCode, + "from", -1L, + "to", AppOpsManager._NUM_OP - 1); + this.mAcceptableLeftDistance = acceptableLeftDistance; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mAcceptableLeftDistance, + "from", 0L, + "to", AppOpsManager._NUM_OP - 1); + this.mExpirationTimeSinceBootMillis = expirationTimeSinceBootMillis; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mExpirationTimeSinceBootMillis, + "from", 0L); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public MessageSamplingConfig[] newArray(int size) { + return new MessageSamplingConfig[size]; + } + + @Override + public MessageSamplingConfig createFromParcel(@NonNull Parcel in) { + return new MessageSamplingConfig(in); + } + }; + + /*@DataClass.Generated( + time = 1580691255495L, + codegenVersion = "1.0.14", + sourceFile = "frameworks/base/core/java/com/android/internal/app/MessageSamplingConfig.java", + inputSignatures = "private final @android.annotation.IntRange(from=-1L, to=AppOpsManager._NUM_OP - 1) int mSampledOpCode\nprivate final @android.annotation.IntRange(from=0L, to=AppOpsManager._NUM_OP - 1) int mAcceptableLeftDistance\nprivate final @android.annotation.IntRange(from=0L) long mExpirationTimeSinceBootMillis\nclass MessageSamplingConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass")*/ + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 0edc160226405..41b5adcaae34e 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -34,6 +34,7 @@ import static android.app.AppOpsManager.OP_CAMERA; import static android.app.AppOpsManager.OP_COARSE_LOCATION; import static android.app.AppOpsManager.OP_FLAGS_ALL; import static android.app.AppOpsManager.OP_FLAG_SELF; +import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; import static android.app.AppOpsManager.OP_NONE; import static android.app.AppOpsManager.OP_PLAY_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO; @@ -52,11 +53,14 @@ import static android.app.AppOpsManager.extractUidStateFromKey; import static android.app.AppOpsManager.makeKey; import static android.app.AppOpsManager.modeToName; import static android.app.AppOpsManager.opToName; +import static android.app.AppOpsManager.opToPublicName; import static android.app.AppOpsManager.resolveFirstUnrestrictedUidState; import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.content.Intent.EXTRA_REPLACING; import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS; import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP; +import static android.util.StatsLogInternal.RUNTIME_APP_OP_ACCESS__SAMPLING_STRATEGY__RARELY_USED; +import static android.util.StatsLogInternal.RUNTIME_APP_OP_ACCESS__SAMPLING_STRATEGY__UNIFORM; import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS; @@ -79,6 +83,8 @@ import android.app.AppOpsManager.OpFlags; import android.app.AppOpsManagerInternal; import android.app.AppOpsManagerInternal.CheckOpsDelegate; import android.app.AsyncNotedAppOp; +import android.app.RuntimeAppOpAccessMessage; +import android.app.SyncNotedAppOp; import android.compat.Compatibility; import android.content.BroadcastReceiver; import android.content.ContentResolver; @@ -87,6 +93,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.PermissionInfo; @@ -137,6 +144,7 @@ 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.app.MessageSamplingConfig; import com.android.internal.os.Zygote; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; @@ -147,6 +155,7 @@ import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.LocalServices; import com.android.server.LockGuard; import com.android.server.SystemServerInitThreadPool; +import com.android.server.pm.PackageList; import libcore.util.EmptyArray; @@ -163,6 +172,8 @@ import java.io.IOException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -172,6 +183,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; public class AppOpsService extends IAppOpsService.Stub { static final String TAG = "AppOps"; @@ -308,6 +321,34 @@ public class AppOpsService extends IAppOpsService.Stub { private ActivityManagerInternal mActivityManagerInternal; + /** Package sampled for message collection in the current session */ + @GuardedBy("this") + private String mSampledPackage = null; + + /** Appop sampled for message collection in the current session */ + @GuardedBy("this") + private int mSampledAppOpCode = OP_NONE; + + /** Maximum distance for appop to be considered for message collection in the current session */ + @GuardedBy("this") + private int mAcceptableLeftDistance = 0; + + /** Number of messages collected for sampled package and appop in the current session */ + @GuardedBy("this") + private float mMessagesCollectedCount; + + /** List of rarely used packages priorities for message collection */ + @GuardedBy("this") + private ArraySet mRarelyUsedPackages = new ArraySet<>(); + + /** Sampling strategy used for current session */ + @GuardedBy("this") + @AppOpsManager.SamplingStrategy + private int mSamplingStrategy; + + /** Last runtime permission access message collected and ready for reporting */ + @GuardedBy("this") + private RuntimeAppOpAccessMessage mCollectedRuntimePermissionMessage; /** * An unsynchronized pool of {@link OpEventProxyInfo} objects. */ @@ -1535,6 +1576,38 @@ public class AppOpsService extends IAppOpsService.Stub { } }, packageSuspendFilter); + final IntentFilter packageAddedFilter = new IntentFilter(); + packageAddedFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + packageAddedFilter.addDataScheme("package"); + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final Uri data = intent.getData(); + + final String packageName = data.getSchemeSpecificPart(); + PackageInfo pi = LocalServices.getService( + PackageManagerInternal.class).getPackageInfo(packageName, + PackageManager.GET_PERMISSIONS, Process.myUid(), mContext.getUserId()); + if (isSamplingTarget(pi)) { + synchronized (this) { + mRarelyUsedPackages.add(packageName); + } + } + } + }, packageAddedFilter); + + List packageNames = getPackageNamesForSampling(); + synchronized (this) { + resamplePackageAndAppOpLocked(packageNames); + } + + AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { + @Override + public void run() { + initializeRarelyUsedPackagesList(new ArraySet<>(packageNames)); + } + }); + PackageManagerInternal packageManagerInternal = LocalServices.getService( PackageManagerInternal.class); packageManagerInternal.setExternalSourcesPolicy( @@ -3030,6 +3103,9 @@ public class AppOpsService extends IAppOpsService.Stub { featureId, message, System.currentTimeMillis()); final boolean[] wasNoteForwarded = {false}; + reportRuntimeAppOpAccessMessageAsyncLocked(uid, packageName, opCode, featureId, + message); + if (callbacks != null) { callbacks.broadcast((cb) -> { try { @@ -5494,6 +5570,227 @@ public class AppOpsService extends IAppOpsService.Stub { mHistoricalRegistry.clearHistory(); } + /** + * Report runtime access to AppOp together with message (including stack trace) + * + * @param packageName The package which reported the op + * @param notedAppOp contains code of op and featureId provided by developer + * @param message Message describing AppOp access (can be stack trace) + * + * @return Config for future sampling to reduce amount of reporting + */ + @Override + public MessageSamplingConfig reportRuntimeAppOpAccessMessageAndGetConfig( + String packageName, SyncNotedAppOp notedAppOp, String message) { + int uid = Binder.getCallingUid(); + Objects.requireNonNull(packageName); + synchronized (this) { + switchPackageIfRarelyUsedLocked(packageName); + if (!packageName.equals(mSampledPackage)) { + return new MessageSamplingConfig(OP_NONE, 0, + Instant.now().plus(1, ChronoUnit.HOURS).toEpochMilli()); + } + + Objects.requireNonNull(notedAppOp); + Objects.requireNonNull(message); + + reportRuntimeAppOpAccessMessageInternalLocked(uid, packageName, + AppOpsManager.strOpToOp(notedAppOp.getOp()), + notedAppOp.getFeatureId(), message); + + return new MessageSamplingConfig(mSampledAppOpCode, mAcceptableLeftDistance, + Instant.now().plus(1, ChronoUnit.HOURS).toEpochMilli()); + } + } + + /** + * Report runtime access to AppOp together with message (entry point for reporting + * asynchronous access) + * @param uid Uid of the package which reported the op + * @param packageName The package which reported the op + * @param opCode Code of AppOp + * @param featureId FeautreId of AppOp reported + * @param message Message describing AppOp access (can be stack trace) + */ + private void reportRuntimeAppOpAccessMessageAsyncLocked(int uid, + @NonNull String packageName, int opCode, @Nullable String featureId, + @NonNull String message) { + switchPackageIfRarelyUsedLocked(packageName); + if (!Objects.equals(mSampledPackage, packageName)) { + return; + } + reportRuntimeAppOpAccessMessageInternalLocked(uid, packageName, opCode, featureId, message); + } + + /** + * Decides whether reported message is within the range of watched AppOps and picks it for + * reporting uniformly at random across all received messages. + */ + private void reportRuntimeAppOpAccessMessageInternalLocked(int uid, + @NonNull String packageName, int opCode, @Nullable String featureId, + @NonNull String message) { + int newLeftDistance = AppOpsManager.leftCircularDistance(opCode, + mSampledAppOpCode, _NUM_OP); + + if (mAcceptableLeftDistance < newLeftDistance) { + return; + } + + if (mAcceptableLeftDistance > newLeftDistance) { + mAcceptableLeftDistance = newLeftDistance; + mMessagesCollectedCount = 0.0f; + } + + mMessagesCollectedCount += 1.0f; + if (ThreadLocalRandom.current().nextFloat() <= 1.0f / mMessagesCollectedCount) { + mCollectedRuntimePermissionMessage = new RuntimeAppOpAccessMessage(uid, opCode, + packageName, featureId, message, mSamplingStrategy); + } + return; + } + + /** Pulls current AppOps access report and resamples package and app op to watch */ + @Override + public @Nullable RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage() { + mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, + Binder.getCallingPid(), Binder.getCallingUid(), null); + RuntimeAppOpAccessMessage result; + List packageNames = getPackageNamesForSampling(); + synchronized (this) { + result = mCollectedRuntimePermissionMessage; + resamplePackageAndAppOpLocked(packageNames); + } + return result; + } + + /** + * Checks if package is in the list of rarely used package and starts watching the new package + * to collect incoming message. + * @param packageName + */ + private void switchPackageIfRarelyUsedLocked(@NonNull String packageName) { + if (mRarelyUsedPackages.contains(packageName)) { + mRarelyUsedPackages.remove(packageName); + if (ThreadLocalRandom.current().nextFloat() < 0.5f) { + mSamplingStrategy = RUNTIME_APP_OP_ACCESS__SAMPLING_STRATEGY__RARELY_USED; + resampleAppOpForPackageLocked(packageName); + } + } + } + + /** Resamples package and appop to watch from the list provided. */ + private void resamplePackageAndAppOpLocked(@NonNull List packageNames) { + if (!packageNames.isEmpty()) { + mSamplingStrategy = RUNTIME_APP_OP_ACCESS__SAMPLING_STRATEGY__UNIFORM; + resampleAppOpForPackageLocked(packageNames.get( + ThreadLocalRandom.current().nextInt(packageNames.size()))); + } + } + + /** Resamples appop for the chosen package and initializes sampling state */ + private void resampleAppOpForPackageLocked(@NonNull String packageName) { + mMessagesCollectedCount = 0.0f; + mSampledAppOpCode = ThreadLocalRandom.current().nextInt(_NUM_OP); + mAcceptableLeftDistance = _NUM_OP; + mSampledPackage = packageName; + mCollectedRuntimePermissionMessage = null; + } + + /** + * Creates list of rarely used packages - packages which were not used over last week or + * which declared but did not use permissions over last week. + * */ + private void initializeRarelyUsedPackagesList(@NonNull ArraySet candidates) { + AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class); + List runtimeAppOpsList = getRuntimeAppOpsList(); + AppOpsManager.HistoricalOpsRequest histOpsRequest = + new AppOpsManager.HistoricalOpsRequest.Builder( + Instant.now().minus(7, ChronoUnit.DAYS).toEpochMilli(), + Long.MAX_VALUE).setOpNames(runtimeAppOpsList).setFlags( + OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED).build(); + appOps.getHistoricalOps(histOpsRequest, AsyncTask.THREAD_POOL_EXECUTOR, + new Consumer() { + @Override + public void accept(HistoricalOps histOps) { + int uidCount = histOps.getUidCount(); + for (int uidIdx = 0; uidIdx < uidCount; uidIdx++) { + final AppOpsManager.HistoricalUidOps uidOps = histOps.getUidOpsAt( + uidIdx); + int pkgCount = uidOps.getPackageCount(); + for (int pkgIdx = 0; pkgIdx < pkgCount; pkgIdx++) { + String packageName = uidOps.getPackageOpsAt( + pkgIdx).getPackageName(); + if (!candidates.contains(packageName)) { + continue; + } + AppOpsManager.HistoricalPackageOps packageOps = + uidOps.getPackageOpsAt(pkgIdx); + if (packageOps.getOpCount() != 0) { + candidates.remove(packageName); + } + } + } + synchronized (this) { + mRarelyUsedPackages = candidates; + } + } + }); + } + + /** List of app ops related to runtime permissions */ + private List getRuntimeAppOpsList() { + ArrayList result = new ArrayList(); + for (int i = 0; i < _NUM_OP; i++) { + if (shouldCollectNotes(i)) { + result.add(opToPublicName(i)); + } + } + return result; + } + + /** Returns list of packages to be used for package sampling */ + private @NonNull List getPackageNamesForSampling() { + List packageNames = new ArrayList<>(); + PackageManagerInternal packageManagerInternal = LocalServices.getService( + PackageManagerInternal.class); + PackageList packages = packageManagerInternal.getPackageList(); + for (String packageName : packages.getPackageNames()) { + PackageInfo pkg = packageManagerInternal.getPackageInfo(packageName, + PackageManager.GET_PERMISSIONS, Process.myUid(), mContext.getUserId()); + if (isSamplingTarget(pkg)) { + packageNames.add(pkg.packageName); + } + } + return packageNames; + } + + /** Checks whether package should be included in sampling pool */ + private boolean isSamplingTarget(@Nullable PackageInfo pkg) { + if (pkg == null) { + return false; + } + if (pkg.applicationInfo.targetSdkVersion <= Build.VERSION_CODES.Q) { + return false; + } + + String[] requestedPermissions = pkg.requestedPermissions; + if (requestedPermissions == null) { + return false; + } + for (String permission : requestedPermissions) { + PermissionInfo permissionInfo; + try { + permissionInfo = mContext.getPackageManager().getPermissionInfo(permission, 0); + } catch (PackageManager.NameNotFoundException ignored) { + continue; + } + if (permissionInfo.getProtection() == PROTECTION_DANGEROUS) { + return true; + } + } + return false; + } + private void removeUidsForUserLocked(int userHandle) { for (int i = mUidStates.size() - 1; i >= 0; --i) { final int uid = mUidStates.keyAt(i);