diff options
| author | 2021-03-03 16:35:28 +0000 | |
|---|---|---|
| committer | 2021-03-03 16:35:28 +0000 | |
| commit | 4f5072825d599a897b25fe99eb7c8ea7b61a15b5 (patch) | |
| tree | 289bb69d467a25c98235e7101ee5eeef909fcf4a | |
| parent | 3d28144bbd6e52c69f336cb9fc3e1418ba6a6538 (diff) | |
| parent | 07e6855f8b451992927c19259880c967eeb0ba06 (diff) | |
Merge "Add API support for permission access timeline." into sc-dev
| -rw-r--r-- | core/api/system-current.txt | 9 | ||||
| -rw-r--r-- | core/api/test-current.txt | 4 | ||||
| -rw-r--r-- | core/java/android/app/AppOpsManager.java | 391 | ||||
| -rw-r--r-- | core/java/com/android/internal/app/IAppOpsService.aidl | 6 | ||||
| -rw-r--r-- | services/core/java/com/android/server/appop/AppOpsService.java | 30 | ||||
| -rw-r--r-- | services/core/java/com/android/server/appop/DiscreteRegistry.java | 616 | ||||
| -rw-r--r-- | services/core/java/com/android/server/appop/HistoricalRegistry.java | 165 |
7 files changed, 1115 insertions, 106 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 019e7ffe11ef..0cdfede12721 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -423,6 +423,9 @@ package android.app { method @Nullable public static String opToPermission(@NonNull String); method @RequiresPermission("android.permission.MANAGE_APP_OPS_MODES") public void setMode(@NonNull String, int, @Nullable String, int); method @RequiresPermission("android.permission.MANAGE_APP_OPS_MODES") public void setUidMode(@NonNull String, int, int); + field public static final int HISTORY_FLAGS_ALL = 3; // 0x3 + field public static final int HISTORY_FLAG_AGGREGATE = 1; // 0x1 + field public static final int HISTORY_FLAG_DISCRETE = 2; // 0x2 field public static final String OPSTR_ACCEPT_HANDOVER = "android:accept_handover"; field public static final String OPSTR_ACCESS_ACCESSIBILITY = "android:access_accessibility"; field public static final String OPSTR_ACCESS_NOTIFICATIONS = "android:access_notifications"; @@ -536,9 +539,14 @@ package android.app { method public long getAccessDuration(int, int, int); method public long getBackgroundAccessCount(int); method public long getBackgroundAccessDuration(int); + method @NonNull public java.util.List<android.app.AppOpsManager.AttributedOpEntry> getBackgroundDiscreteAccesses(int); method public long getBackgroundRejectCount(int); + method @NonNull public android.app.AppOpsManager.AttributedOpEntry getDiscreteAccessAt(@IntRange(from=0) int); + method @IntRange(from=0) public int getDiscreteAccessCount(); + method @NonNull public java.util.List<android.app.AppOpsManager.AttributedOpEntry> getDiscreteAccesses(int, int, int); method public long getForegroundAccessCount(int); method public long getForegroundAccessDuration(int); + method @NonNull public java.util.List<android.app.AppOpsManager.AttributedOpEntry> getForegroundDiscreteAccesses(int); method public long getForegroundRejectCount(int); method @NonNull public String getOpName(); method public long getRejectCount(int, int, int); @@ -565,6 +573,7 @@ package android.app { method @NonNull public android.app.AppOpsManager.HistoricalOpsRequest build(); method @NonNull public android.app.AppOpsManager.HistoricalOpsRequest.Builder setAttributionTag(@Nullable String); method @NonNull public android.app.AppOpsManager.HistoricalOpsRequest.Builder setFlags(int); + method @NonNull public android.app.AppOpsManager.HistoricalOpsRequest.Builder setHistoryFlags(int); method @NonNull public android.app.AppOpsManager.HistoricalOpsRequest.Builder setOpNames(@Nullable java.util.List<java.lang.String>); method @NonNull public android.app.AppOpsManager.HistoricalOpsRequest.Builder setPackageName(@Nullable String); method @NonNull public android.app.AppOpsManager.HistoricalOpsRequest.Builder setUid(int); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 1e5a6f12f96b..29ea74e21272 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -224,6 +224,9 @@ package android.app { field public static final int HISTORICAL_MODE_DISABLED = 0; // 0x0 field public static final int HISTORICAL_MODE_ENABLED_ACTIVE = 1; // 0x1 field public static final int HISTORICAL_MODE_ENABLED_PASSIVE = 2; // 0x2 + field public static final int HISTORY_FLAGS_ALL = 3; // 0x3 + field public static final int HISTORY_FLAG_AGGREGATE = 1; // 0x1 + field public static final int HISTORY_FLAG_DISCRETE = 2; // 0x2 field public static final String KEY_BG_STATE_SETTLE_TIME = "bg_state_settle_time"; field public static final String KEY_FG_SERVICE_STATE_SETTLE_TIME = "fg_service_state_settle_time"; field public static final String KEY_TOP_STATE_SETTLE_TIME = "top_state_settle_time"; @@ -238,6 +241,7 @@ package android.app { public static final class AppOpsManager.HistoricalOps implements android.os.Parcelable { ctor public AppOpsManager.HistoricalOps(long, long); + method public void addDiscreteAccess(int, int, @NonNull String, @Nullable String, int, int, long, long); method public void increaseAccessCount(int, int, @NonNull String, @Nullable String, int, int, long); method public void increaseAccessDuration(int, int, @NonNull String, @Nullable String, int, int, long); method public void increaseRejectCount(int, int, @NonNull String, @Nullable String, int, int, long); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 160844aacc46..dd1bc7c61547 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -16,6 +16,8 @@ package android.app; +import static java.lang.Long.max; + import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.IntDef; @@ -3385,6 +3387,13 @@ public class AppOpsManager { @DataClass.ParcelWith(LongSparseArrayParceling.class) private final @Nullable LongSparseArray<NoteOpEvent> mRejectEvents; + private AttributedOpEntry(@NonNull AttributedOpEntry other) { + mOp = other.mOp; + mRunning = other.mRunning; + mAccessEvents = other.mAccessEvents == null ? null : other.mAccessEvents.clone(); + mRejectEvents = other.mRejectEvents == null ? null : other.mRejectEvents.clone(); + } + /** * Returns all keys for which we have events. * @@ -3749,6 +3758,15 @@ public class AppOpsManager { return lastEvent.getProxy(); } + @NonNull + String getOpName() { + return AppOpsManager.opToPublicName(mOp); + } + + int getOp() { + return mOp; + } + private static class LongSparseArrayParceling implements Parcelling<LongSparseArray<NoteOpEvent>> { @Override @@ -4571,6 +4589,50 @@ public class AppOpsManager { } /** + * Flag for querying app op history: get only aggregate information and no + * discrete accesses. + * + * @see #getHistoricalOps(HistoricalOpsRequest, Executor, Consumer) + * + * @hide + */ + @TestApi + @SystemApi + public static final int HISTORY_FLAG_AGGREGATE = 1 << 0; + + /** + * Flag for querying app op history: get only discrete information and no + * aggregate accesses. + * + * @see #getHistoricalOps(HistoricalOpsRequest, Executor, Consumer) + * + * @hide + */ + @TestApi + @SystemApi + public static final int HISTORY_FLAG_DISCRETE = 1 << 1; + + /** + * Flag for querying app op history: get all types of historical accesses. + * + * @see #getHistoricalOps(HistoricalOpsRequest, Executor, Consumer) + * + * @hide + */ + @TestApi + @SystemApi + public static final int HISTORY_FLAGS_ALL = HISTORY_FLAG_AGGREGATE + | HISTORY_FLAG_DISCRETE; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "HISTORY_FLAG_" }, value = { + HISTORY_FLAG_AGGREGATE, + HISTORY_FLAG_DISCRETE + }) + public @interface OpHistoryFlags {} + + /** * Specifies what parameters to filter historical appop requests for * * @hide @@ -4625,6 +4687,7 @@ public class AppOpsManager { private final @Nullable String mPackageName; private final @Nullable String mAttributionTag; private final @Nullable List<String> mOpNames; + private final @OpHistoryFlags int mHistoryFlags; private final @HistoricalOpsRequestFilter int mFilter; private final long mBeginTimeMillis; private final long mEndTimeMillis; @@ -4632,12 +4695,13 @@ public class AppOpsManager { private HistoricalOpsRequest(int uid, @Nullable String packageName, @Nullable String attributionTag, @Nullable List<String> opNames, - @HistoricalOpsRequestFilter int filter, long beginTimeMillis, - long endTimeMillis, @OpFlags int flags) { + @OpHistoryFlags int historyFlags, @HistoricalOpsRequestFilter int filter, + long beginTimeMillis, long endTimeMillis, @OpFlags int flags) { mUid = uid; mPackageName = packageName; mAttributionTag = attributionTag; mOpNames = opNames; + mHistoryFlags = historyFlags; mFilter = filter; mBeginTimeMillis = beginTimeMillis; mEndTimeMillis = endTimeMillis; @@ -4655,6 +4719,7 @@ public class AppOpsManager { private @Nullable String mPackageName; private @Nullable String mAttributionTag; private @Nullable List<String> mOpNames; + private @OpHistoryFlags int mHistoryFlags; private @HistoricalOpsRequestFilter int mFilter; private final long mBeginTimeMillis; private final long mEndTimeMillis; @@ -4676,6 +4741,7 @@ public class AppOpsManager { "beginTimeMillis must be non negative and lesser than endTimeMillis"); mBeginTimeMillis = beginTimeMillis; mEndTimeMillis = endTimeMillis; + mHistoryFlags = HISTORY_FLAG_AGGREGATE; } /** @@ -4772,11 +4838,25 @@ public class AppOpsManager { } /** + * Specifies what type of historical information to query. + * + * @param flags Flags for the historical types to fetch which are any + * combination of {@link #HISTORY_FLAG_AGGREGATE}, {@link #HISTORY_FLAG_DISCRETE}, + * {@link #HISTORY_FLAGS_ALL}. The default is {@link #HISTORY_FLAG_AGGREGATE}. + * @return This builder. + */ + public @NonNull Builder setHistoryFlags(@OpHistoryFlags int flags) { + Preconditions.checkFlagsArgument(flags, HISTORY_FLAGS_ALL); + mHistoryFlags = flags; + return this; + } + + /** * @return a new {@link HistoricalOpsRequest}. */ public @NonNull HistoricalOpsRequest build() { return new HistoricalOpsRequest(mUid, mPackageName, mAttributionTag, mOpNames, - mFilter, mBeginTimeMillis, mEndTimeMillis, mFlags); + mHistoryFlags, mFilter, mBeginTimeMillis, mEndTimeMillis, mFlags); } } } @@ -4943,7 +5023,8 @@ public class AppOpsManager { * @hide */ public void filter(int uid, @Nullable String packageName, @Nullable String attributionTag, - @Nullable String[] opNames, @HistoricalOpsRequestFilter int filter, + @Nullable String[] opNames, @OpHistoryFlags int historyFilter, + @HistoricalOpsRequestFilter int filter, long beginTimeMillis, long endTimeMillis) { final long durationMillis = getDurationMillis(); mBeginTimeMillis = Math.max(mBeginTimeMillis, beginTimeMillis); @@ -4956,7 +5037,8 @@ public class AppOpsManager { if ((filter & FILTER_BY_UID) != 0 && uid != uidOp.getUid()) { mHistoricalUidOps.removeAt(i); } else { - uidOp.filter(packageName, attributionTag, opNames, filter, scaleFactor); + uidOp.filter(packageName, attributionTag, opNames, filter, historyFilter, + scaleFactor, mBeginTimeMillis, mEndTimeMillis); if (uidOp.getPackageCount() == 0) { mHistoricalUidOps.removeAt(i); } @@ -5013,6 +5095,16 @@ public class AppOpsManager { /** @hide */ @TestApi + public void addDiscreteAccess(int opCode, int uid, @NonNull String packageName, + @Nullable String attributionTag, @UidState int uidState, @OpFlags int opFlag, + long discreteAccessTime, long discreteAccessDuration) { + getOrCreateHistoricalUidOps(uid).addDiscreteAccess(opCode, packageName, attributionTag, + uidState, opFlag, discreteAccessTime, discreteAccessDuration); + }; + + + /** @hide */ + @TestApi public void offsetBeginAndEndTime(long offsetMillis) { mBeginTimeMillis += offsetMillis; mEndTimeMillis += offsetMillis; @@ -5288,7 +5380,8 @@ public class AppOpsManager { private void filter(@Nullable String packageName, @Nullable String attributionTag, @Nullable String[] opNames, @HistoricalOpsRequestFilter int filter, - double fractionToRemove) { + @OpHistoryFlags int historyFilter, double fractionToRemove, long beginTimeMillis, + long endTimeMillis) { final int packageCount = getPackageCount(); for (int i = packageCount - 1; i >= 0; i--) { final HistoricalPackageOps packageOps = getPackageOpsAt(i); @@ -5296,7 +5389,8 @@ public class AppOpsManager { packageOps.getPackageName())) { mHistoricalPackageOps.removeAt(i); } else { - packageOps.filter(attributionTag, opNames, filter, fractionToRemove); + packageOps.filter(attributionTag, opNames, filter, historyFilter, + fractionToRemove, beginTimeMillis, endTimeMillis); if (packageOps.getAttributedOpsCount() == 0) { mHistoricalPackageOps.removeAt(i); } @@ -5336,6 +5430,13 @@ public class AppOpsManager { opCode, attributionTag, uidState, flags, increment); } + private void addDiscreteAccess(int opCode, @NonNull String packageName, + @Nullable String attributionTag, @UidState int uidState, + @OpFlags int flag, long discreteAccessTime, long discreteAccessDuration) { + getOrCreateHistoricalPackageOps(packageName).addDiscreteAccess(opCode, attributionTag, + uidState, flag, discreteAccessTime, discreteAccessDuration); + }; + /** * @return The UID for which the data is related. */ @@ -5540,7 +5641,8 @@ public class AppOpsManager { } private void filter(@Nullable String attributionTag, @Nullable String[] opNames, - @HistoricalOpsRequestFilter int filter, double fractionToRemove) { + @HistoricalOpsRequestFilter int filter, @OpHistoryFlags int historyFilter, + double fractionToRemove, long beginTimeMillis, long endTimeMillis) { final int attributionCount = getAttributedOpsCount(); for (int i = attributionCount - 1; i >= 0; i--) { final AttributedHistoricalOps attributionOps = getAttributedOpsAt(i); @@ -5548,7 +5650,8 @@ public class AppOpsManager { attributionOps.getTag())) { mAttributedHistoricalOps.removeAt(i); } else { - attributionOps.filter(opNames, filter, fractionToRemove); + attributionOps.filter(opNames, filter, historyFilter, fractionToRemove, + beginTimeMillis, endTimeMillis); if (attributionOps.getOpCount() == 0) { mAttributedHistoricalOps.removeAt(i); } @@ -5593,6 +5696,13 @@ public class AppOpsManager { opCode, uidState, flags, increment); } + private void addDiscreteAccess(int opCode, @Nullable String attributionTag, + @UidState int uidState, @OpFlags int flag, long discreteAccessTime, + long discreteAccessDuration) { + getOrCreateAttributedHistoricalOps(attributionTag).addDiscreteAccess(opCode, uidState, + flag, discreteAccessTime, discreteAccessDuration); + } + /** * Gets the package name which the data represents. * @@ -5870,7 +5980,8 @@ public class AppOpsManager { } private void filter(@Nullable String[] opNames, @HistoricalOpsRequestFilter int filter, - double scaleFactor) { + @OpHistoryFlags int historyFilter, double scaleFactor, long beginTimeMillis, + long endTimeMillis) { final int opCount = getOpCount(); for (int i = opCount - 1; i >= 0; i--) { final HistoricalOp op = mHistoricalOps.valueAt(i); @@ -5878,7 +5989,7 @@ public class AppOpsManager { op.getOpName())) { mHistoricalOps.removeAt(i); } else { - op.filter(scaleFactor); + op.filter(historyFilter, scaleFactor, beginTimeMillis, endTimeMillis); } } } @@ -5909,6 +6020,12 @@ public class AppOpsManager { getOrCreateHistoricalOp(opCode).increaseAccessDuration(uidState, flags, increment); } + private void addDiscreteAccess(int opCode, @UidState int uidState, @OpFlags int flag, + long discreteAccessTime, long discreteAccessDuration) { + getOrCreateHistoricalOp(opCode).addDiscreteAccess(uidState,flag, discreteAccessTime, + discreteAccessDuration); + } + /** * Gets number historical app ops. * @@ -5970,8 +6087,6 @@ public class AppOpsManager { return op; } - - // Code below generated by codegen v1.0.14. // // DO NOT MODIFY! @@ -6121,6 +6236,9 @@ public class AppOpsManager { private @Nullable LongSparseLongArray mRejectCount; private @Nullable LongSparseLongArray mAccessDuration; + /** Discrete Ops for this Op */ + private @Nullable List<AttributedOpEntry> mDiscreteAccesses; + /** @hide */ public HistoricalOp(int op) { mOp = op; @@ -6137,6 +6255,12 @@ public class AppOpsManager { if (other.mAccessDuration != null) { mAccessDuration = other.mAccessDuration.clone(); } + final int historicalOpCount = other.getDiscreteAccessCount(); + for (int i = 0; i < historicalOpCount; i++) { + final AttributedOpEntry origOp = other.getDiscreteAccessAt(i); + final AttributedOpEntry cloneOp = new AttributedOpEntry(origOp); + getOrCreateDiscreteAccesses().add(cloneOp); + } } private HistoricalOp(@NonNull Parcel parcel) { @@ -6144,22 +6268,45 @@ public class AppOpsManager { mAccessCount = readLongSparseLongArrayFromParcel(parcel); mRejectCount = readLongSparseLongArrayFromParcel(parcel); mAccessDuration = readLongSparseLongArrayFromParcel(parcel); + mDiscreteAccesses = readDiscreteAccessArrayFromParcel(parcel); } - private void filter(double scaleFactor) { - scale(mAccessCount, scaleFactor); - scale(mRejectCount, scaleFactor); - scale(mAccessDuration, scaleFactor); + private void filter(@OpHistoryFlags int historyFlag, double scaleFactor, + long beginTimeMillis, long endTimeMillis) { + if ((historyFlag & HISTORY_FLAG_AGGREGATE) == 0) { + mAccessCount = null; + mRejectCount = null; + mAccessDuration = null; + } else { + scale(mAccessCount, scaleFactor); + scale(mRejectCount, scaleFactor); + scale(mAccessDuration, scaleFactor); + } + if ((historyFlag & HISTORY_FLAG_DISCRETE) == 0) { + mDiscreteAccesses = null; + return; + } + final int discreteOpCount = getDiscreteAccessCount(); + for (int i = discreteOpCount - 1; i >= 0; i--) { + final AttributedOpEntry op = mDiscreteAccesses.get(i); + long opBeginTime = op.getLastAccessTime(OP_FLAGS_ALL); + long opEndTime = opBeginTime + op.getLastDuration(OP_FLAGS_ALL); + opEndTime = max(opBeginTime, opEndTime); + if (opEndTime < beginTimeMillis || opBeginTime > endTimeMillis) { + mDiscreteAccesses.remove(i); + } + } } private boolean isEmpty() { return !hasData(mAccessCount) && !hasData(mRejectCount) - && !hasData(mAccessDuration); + && !hasData(mAccessDuration) + && (mDiscreteAccesses == null); } private boolean hasData(@NonNull LongSparseLongArray array) { - return (array != null && array.size() > 0); + return array != null && array.size() > 0; } private @Nullable HistoricalOp splice(double fractionToRemove) { @@ -6191,6 +6338,32 @@ public class AppOpsManager { merge(this::getOrCreateAccessCount, other.mAccessCount); merge(this::getOrCreateRejectCount, other.mRejectCount); merge(this::getOrCreateAccessDuration, other.mAccessDuration); + + if (other.mDiscreteAccesses == null) { + return; + } + if (mDiscreteAccesses == null) { + mDiscreteAccesses = new ArrayList(other.mDiscreteAccesses); + return; + } + List<AttributedOpEntry> historicalDiscreteAccesses = new ArrayList<>(); + final int otherHistoricalOpCount = other.getDiscreteAccessCount(); + final int historicalOpCount = getDiscreteAccessCount(); + int i = 0; + int j = 0; + while (i < otherHistoricalOpCount || j < historicalOpCount) { + if (i == otherHistoricalOpCount) { + historicalDiscreteAccesses.add(mDiscreteAccesses.get(j++)); + } else if (j == historicalOpCount) { + historicalDiscreteAccesses.add(other.mDiscreteAccesses.get(i++)); + } else if (mDiscreteAccesses.get(j).getLastAccessTime(OP_FLAGS_ALL) + < other.mDiscreteAccesses.get(i).getLastAccessTime(OP_FLAGS_ALL)) { + historicalDiscreteAccesses.add(mDiscreteAccesses.get(j++)); + } else { + historicalDiscreteAccesses.add(other.mDiscreteAccesses.get(i++)); + } + } + mDiscreteAccesses = historicalDiscreteAccesses; } private void increaseAccessCount(@UidState int uidState, @OpFlags int flags, @@ -6218,6 +6391,23 @@ public class AppOpsManager { } } + private void addDiscreteAccess(@UidState int uidState, @OpFlags int flag, + long discreteAccessTime, long discreteAccessDuration) { + List<AttributedOpEntry> discreteAccesses = getOrCreateDiscreteAccesses(); + LongSparseArray<NoteOpEvent> accessEvents = new LongSparseArray<>(); + long key = makeKey(uidState, flag); + NoteOpEvent note = new NoteOpEvent(discreteAccessTime, discreteAccessDuration, null); + accessEvents.append(key, note); + AttributedOpEntry access = new AttributedOpEntry(mOp, false, accessEvents, null); + for (int i = discreteAccesses.size() - 1; i >= 0; i--) { + if (discreteAccesses.get(i).getLastAccessTime(OP_FLAGS_ALL) < discreteAccessTime) { + discreteAccesses.add(i + 1, access); + return; + } + } + discreteAccesses.add(0, access); + } + /** * Gets the op name. * @@ -6233,6 +6423,33 @@ public class AppOpsManager { } /** + * Gets number of discrete historical app ops. + * + * @return The number historical app ops. + * @see #getOpAt(int) + */ + public @IntRange(from = 0) int getDiscreteAccessCount() { + if (mDiscreteAccesses == null) { + return 0; + } + return mDiscreteAccesses.size(); + } + + /** + * Gets the historical op at a given index. + * + * @param index The index to lookup. + * @return The op at the given index. + * @see #getOpCount() + */ + public @NonNull AttributedOpEntry getDiscreteAccessAt(@IntRange(from = 0) int index) { + if (mDiscreteAccesses == null) { + throw new IndexOutOfBoundsException(); + } + return mDiscreteAccesses.get(index); + } + + /** * Gets the number times the op was accessed (performed) in the foreground. * * @param flags The flags which are any combination of @@ -6251,6 +6468,25 @@ public class AppOpsManager { } /** + * Gets the discrete events the op was accessed (performed) in the foreground. + * + * @param flags The flags which are any combination of + * {@link #OP_FLAG_SELF}, {@link #OP_FLAG_TRUSTED_PROXY}, + * {@link #OP_FLAG_UNTRUSTED_PROXY}, {@link #OP_FLAG_TRUSTED_PROXIED}, + * {@link #OP_FLAG_UNTRUSTED_PROXIED}. You can use {@link #OP_FLAGS_ALL} + * for any flag. + * @return The list of discrete ops accessed in the foreground. + * + * @see #getBackgroundDiscreteAccesses(int) + * @see #getDiscreteAccesses(int, int, int) + */ + @NonNull + public List<AttributedOpEntry> getForegroundDiscreteAccesses(@OpFlags int flags) { + return listForFlagsInStates(mDiscreteAccesses, MAX_PRIORITY_UID_STATE, + resolveFirstUnrestrictedUidState(mOp), flags); + } + + /** * Gets the number times the op was accessed (performed) in the background. * * @param flags The flags which are any combination of @@ -6269,6 +6505,25 @@ public class AppOpsManager { } /** + * Gets the discrete events the op was accessed (performed) in the background. + * + * @param flags The flags which are any combination of + * {@link #OP_FLAG_SELF}, {@link #OP_FLAG_TRUSTED_PROXY}, + * {@link #OP_FLAG_UNTRUSTED_PROXY}, {@link #OP_FLAG_TRUSTED_PROXIED}, + * {@link #OP_FLAG_UNTRUSTED_PROXIED}. You can use {@link #OP_FLAGS_ALL} + * for any flag. + * @return The list of discrete ops accessed in the background. + * + * @see #getForegroundDiscreteAccesses(int) + * @see #getDiscreteAccesses(int, int, int) + */ + @NonNull + public List<AttributedOpEntry> getBackgroundDiscreteAccesses(@OpFlags int flags) { + return listForFlagsInStates(mDiscreteAccesses, resolveLastRestrictedUidState(mOp), + MIN_PRIORITY_UID_STATE, flags); + } + + /** * Gets the number times the op was accessed (performed) for a * range of uid states. * @@ -6294,6 +6549,26 @@ public class AppOpsManager { } /** + * Gets the discrete events the op was accessed (performed) for a + * range of uid states. + * + * @param flags The flags which are any combination of + * {@link #OP_FLAG_SELF}, {@link #OP_FLAG_TRUSTED_PROXY}, + * {@link #OP_FLAG_UNTRUSTED_PROXY}, {@link #OP_FLAG_TRUSTED_PROXIED}, + * {@link #OP_FLAG_UNTRUSTED_PROXIED}. You can use {@link #OP_FLAGS_ALL} + * for any flag. + * @return The discrete the op was accessed in the background. + * + * @see #getBackgroundDiscreteAccesses(int) + * @see #getForegroundDiscreteAccesses(int) + */ + @NonNull + public List<AttributedOpEntry> getDiscreteAccesses(@UidState int fromUidState, + @UidState int toUidState, @OpFlags int flags) { + return listForFlagsInStates(mDiscreteAccesses, fromUidState, toUidState, flags); + } + + /** * Gets the number times the op was rejected in the foreground. * * @param flags The flags which are any combination of @@ -6427,6 +6702,7 @@ public class AppOpsManager { writeLongSparseLongArrayToParcel(mAccessCount, parcel); writeLongSparseLongArrayToParcel(mRejectCount, parcel); writeLongSparseLongArrayToParcel(mAccessDuration, parcel); + writeDiscreteAccessArrayToParcel(mDiscreteAccesses, parcel); } @Override @@ -6447,7 +6723,11 @@ public class AppOpsManager { if (!equalsLongSparseLongArray(mRejectCount, other.mRejectCount)) { return false; } - return equalsLongSparseLongArray(mAccessDuration, other.mAccessDuration); + if (!equalsLongSparseLongArray(mAccessDuration, other.mAccessDuration)) { + return false; + } + return mDiscreteAccesses == null ? (other.mDiscreteAccesses == null ? true + : false) : mDiscreteAccesses.equals(other.mDiscreteAccesses); } @Override @@ -6456,6 +6736,7 @@ public class AppOpsManager { result = 31 * result + Objects.hashCode(mAccessCount); result = 31 * result + Objects.hashCode(mRejectCount); result = 31 * result + Objects.hashCode(mAccessDuration); + result = 31 * result + Objects.hashCode(mDiscreteAccesses); return result; } @@ -6484,6 +6765,13 @@ public class AppOpsManager { return mAccessDuration; } + private @NonNull List<AttributedOpEntry> getOrCreateDiscreteAccesses() { + if (mDiscreteAccesses == null) { + mDiscreteAccesses = new ArrayList<>(); + } + return mDiscreteAccesses; + } + /** * Multiplies the entries in the array with the passed in scale factor and * rounds the result at up 0.5 boundary. @@ -6574,6 +6862,32 @@ public class AppOpsManager { } /** + * Returns list of events filtered by UidState and UID flags. + * + * @param accesses The events list. + * @param beginUidState The beginning UID state (inclusive). + * @param endUidState The end UID state (inclusive). + * @param flags The UID flags. + * @return filtered list of events. + */ + private static List<AttributedOpEntry> listForFlagsInStates(List<AttributedOpEntry> accesses, + @UidState int beginUidState, @UidState int endUidState, @OpFlags int flags) { + List<AttributedOpEntry> result = new ArrayList<>(); + if (accesses == null) { + return result; + } + int nAccesses = accesses.size(); + for (int i = 0; i < nAccesses; i++) { + AttributedOpEntry entry = accesses.get(i); + if (entry.getLastAccessTime(beginUidState, endUidState, flags) == -1) { + continue; + } + result.add(entry); + } + return result; + } + + /** * Callback for notification of changes to operation state. */ public interface OnOpChangedListener { @@ -6796,8 +7110,9 @@ public class AppOpsManager { Objects.requireNonNull(callback, "callback cannot be null"); try { mService.getHistoricalOps(request.mUid, request.mPackageName, request.mAttributionTag, - request.mOpNames, request.mFilter, request.mBeginTimeMillis, - request.mEndTimeMillis, request.mFlags, new RemoteCallback((result) -> { + request.mOpNames, request.mHistoryFlags, request.mFilter, + request.mBeginTimeMillis, request.mEndTimeMillis, request.mFlags, + new RemoteCallback((result) -> { final HistoricalOps ops = result.getParcelable(KEY_HISTORICAL_OPS); final long identity = Binder.clearCallingIdentity(); try { @@ -6835,9 +7150,9 @@ public class AppOpsManager { Objects.requireNonNull(callback, "callback cannot be null"); try { mService.getHistoricalOpsFromDiskRaw(request.mUid, request.mPackageName, - request.mAttributionTag, request.mOpNames, request.mFilter, - request.mBeginTimeMillis, request.mEndTimeMillis, request.mFlags, - new RemoteCallback((result) -> { + request.mAttributionTag, request.mOpNames, request.mHistoryFlags, + request.mFilter, request.mBeginTimeMillis, request.mEndTimeMillis, + request.mFlags, new RemoteCallback((result) -> { final HistoricalOps ops = result.getParcelable(KEY_HISTORICAL_OPS); final long identity = Binder.clearCallingIdentity(); try { @@ -9072,6 +9387,32 @@ public class AppOpsManager { return array; } + private static void writeDiscreteAccessArrayToParcel( + @Nullable List<AttributedOpEntry> array, @NonNull Parcel parcel) { + if (array != null) { + final int size = array.size(); + parcel.writeInt(size); + for (int i = 0; i < size; i++) { + array.get(i).writeToParcel(parcel, 0); + } + } else { + parcel.writeInt(-1); + } + } + + private static @Nullable List<AttributedOpEntry> readDiscreteAccessArrayFromParcel( + @NonNull Parcel parcel) { + final int size = parcel.readInt(); + if (size < 0) { + return null; + } + final List<AttributedOpEntry> array = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + array.add(new AttributedOpEntry(parcel)); + } + return array; + } + /** * Collects the keys from an array to the result creating the result if needed. * diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 3a9f3b9c1128..eecd0cfe66a4 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -74,11 +74,11 @@ interface IAppOpsService { @UnsupportedAppUsage List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, in int[] ops); void getHistoricalOps(int uid, String packageName, String attributionTag, in List<String> ops, - int filter, long beginTimeMillis, long endTimeMillis, int flags, + int historyFlags, int filter, long beginTimeMillis, long endTimeMillis, int flags, in RemoteCallback callback); void getHistoricalOpsFromDiskRaw(int uid, String packageName, String attributionTag, - in List<String> ops, int filter, long beginTimeMillis, long endTimeMillis, int flags, - in RemoteCallback callback); + in List<String> ops, int historyFlags, int filter, long beginTimeMillis, + long endTimeMillis, int flags, in RemoteCallback callback); void offsetHistory(long duration); void setHistoryParameters(int mode, long baseSnapshotInterval, int compressionStep); void addHistoricalOps(in AppOpsManager.HistoricalOps ops); diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 44dcc205a9d0..11125dd55665 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -843,11 +843,15 @@ public class AppOpsService extends IAppOpsService.Stub { public void accessed(int proxyUid, @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, @AppOpsManager.UidState int uidState, @OpFlags int flags) { - accessed(System.currentTimeMillis(), -1, proxyUid, proxyPackageName, + long accessTime = System.currentTimeMillis(); + accessed(accessTime, -1, proxyUid, proxyPackageName, proxyAttributionTag, uidState, flags); mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, tag, uidState, flags); + + mHistoricalRegistry.mDiscreteRegistry.recordDiscreteAccess(parent.uid, + parent.packageName, parent.op, tag, flags, uidState, accessTime, -1); } /** @@ -1004,8 +1008,10 @@ public class AppOpsService extends IAppOpsService.Stub { OpEventProxyInfo proxyCopy = event.getProxy() != null ? new OpEventProxyInfo(event.getProxy()) : null; + long accessDurationMillis = + SystemClock.elapsedRealtime() - event.getStartElapsedTime(); NoteOpEvent finishedEvent = new NoteOpEvent(event.getStartTime(), - SystemClock.elapsedRealtime() - event.getStartElapsedTime(), proxyCopy); + accessDurationMillis, proxyCopy); mAccessEvents.put(makeKey(event.getUidState(), event.getFlags()), finishedEvent); @@ -1013,6 +1019,10 @@ public class AppOpsService extends IAppOpsService.Stub { parent.packageName, tag, event.getUidState(), event.getFlags(), finishedEvent.getDuration()); + mHistoricalRegistry.mDiscreteRegistry.recordDiscreteAccess(parent.uid, + parent.packageName, parent.op, tag, event.getFlags(), event.getUidState(), + event.getStartTime(), accessDurationMillis); + mInProgressStartOpEventPool.release(event); if (mInProgressEvents.isEmpty()) { @@ -2087,8 +2097,8 @@ public class AppOpsService extends IAppOpsService.Stub { @Override public void getHistoricalOps(int uid, String packageName, String attributionTag, - List<String> opNames, int filter, long beginTimeMillis, long endTimeMillis, - int flags, RemoteCallback callback) { + List<String> opNames, int dataType, int filter, long beginTimeMillis, + long endTimeMillis, int flags, RemoteCallback callback) { PackageManager pm = mContext.getPackageManager(); ensureHistoricalOpRequestIsValid(uid, packageName, attributionTag, opNames, filter, @@ -2120,14 +2130,14 @@ public class AppOpsService extends IAppOpsService.Stub { // Must not hold the appops lock mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOps, - mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, filter, - beginTimeMillis, endTimeMillis, flags, callback).recycleOnUse()); + mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType, + filter, beginTimeMillis, endTimeMillis, flags, callback).recycleOnUse()); } @Override public void getHistoricalOpsFromDiskRaw(int uid, String packageName, String attributionTag, - List<String> opNames, int filter, long beginTimeMillis, long endTimeMillis, - int flags, RemoteCallback callback) { + List<String> opNames, int dataType, int filter, long beginTimeMillis, + long endTimeMillis, int flags, RemoteCallback callback) { ensureHistoricalOpRequestIsValid(uid, packageName, attributionTag, opNames, filter, beginTimeMillis, endTimeMillis, flags); Objects.requireNonNull(callback, "callback cannot be null"); @@ -2140,7 +2150,7 @@ public class AppOpsService extends IAppOpsService.Stub { // Must not hold the appops lock mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOpsFromDiskRaw, - mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, + mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType, filter, beginTimeMillis, endTimeMillis, flags, callback).recycleOnUse()); } @@ -4759,6 +4769,7 @@ public class AppOpsService extends IAppOpsService.Stub { mFile.failWrite(stream); } } + mHistoricalRegistry.mDiscreteRegistry.writeAndClearAccessHistory(); } static class Shell extends ShellCommand { @@ -6115,6 +6126,7 @@ public class AppOpsService extends IAppOpsService.Stub { "clearHistory"); // Must not hold the appops lock mHistoricalRegistry.clearHistory(); + mHistoricalRegistry.mDiscreteRegistry.clearHistory(); } @Override diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java new file mode 100644 index 000000000000..76990453ee03 --- /dev/null +++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java @@ -0,0 +1,616 @@ +/* + * Copyright (C) 2021 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.server.appop; + +import static android.app.AppOpsManager.FILTER_BY_ATTRIBUTION_TAG; +import static android.app.AppOpsManager.FILTER_BY_OP_NAMES; +import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME; +import static android.app.AppOpsManager.FILTER_BY_UID; +import static android.app.AppOpsManager.OP_CAMERA; +import static android.app.AppOpsManager.OP_COARSE_LOCATION; +import static android.app.AppOpsManager.OP_FINE_LOCATION; +import static android.app.AppOpsManager.OP_FLAG_SELF; +import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; +import static android.app.AppOpsManager.OP_RECORD_AUDIO; + +import static java.lang.Math.max; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppOpsManager; +import android.os.Environment; +import android.os.FileUtils; +import android.os.Process; +import android.util.ArrayMap; +import android.util.Slog; +import android.util.TypedXmlPullParser; +import android.util.TypedXmlSerializer; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.XmlUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * This class manages information about recent accesses to ops for + * permission usage timeline. + * + * The timeline history is kept for limited time (initial default is 24 hours) and + * discarded after that. + * + * Every time state is saved (default is 30 minutes), memory state is dumped to a + * new file and memory state is cleared. Files older than time limit are deleted + * during the process. + * + * When request comes in, files are read and requested information is collected + * and delivered. + */ + +final class DiscreteRegistry { + static final String TIMELINE_FILE_SUFFIX = "tl"; + private static final String TAG = DiscreteRegistry.class.getSimpleName(); + + private static final long TIMELINE_HISTORY_CUTOFF = Duration.ofHours(24).toMillis(); + private static final String TAG_HISTORY = "h"; + private static final String ATTR_VERSION = "v"; + private static final int CURRENT_VERSION = 1; + + private static final String TAG_UID = "u"; + private static final String ATTR_UID = "ui"; + + private static final String TAG_PACKAGE = "p"; + private static final String ATTR_PACKAGE_NAME = "pn"; + + private static final String TAG_OP = "o"; + private static final String ATTR_OP_ID = "op"; + + private static final String TAG_TAG = "a"; + private static final String ATTR_TAG = "at"; + + private static final String TAG_ENTRY = "e"; + private static final String ATTR_NOTE_TIME = "nt"; + private static final String ATTR_NOTE_DURATION = "nd"; + private static final String ATTR_UID_STATE = "us"; + private static final String ATTR_FLAGS = "f"; + + // Lock for read/write access to on disk state + private final Object mOnDiskLock = new Object(); + + //Lock for read/write access to in memory state + private final @NonNull Object mInMemoryLock; + + @GuardedBy("mOnDiskLock") + private final File mDiscreteAccessDir; + + @GuardedBy("mInMemoryLock") + private DiscreteOps mDiscreteOps; + + DiscreteRegistry(Object inMemoryLock) { + mInMemoryLock = inMemoryLock; + mDiscreteAccessDir = new File(new File(Environment.getDataSystemDirectory(), "appops"), + "discrete"); + createDiscreteAccessDir(); + mDiscreteOps = new DiscreteOps(); + } + + private void createDiscreteAccessDir() { + if (!mDiscreteAccessDir.exists()) { + if (!mDiscreteAccessDir.mkdirs()) { + Slog.e(TAG, "Failed to create DiscreteRegistry directory"); + } + FileUtils.setPermissions(mDiscreteAccessDir.getPath(), + FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IXOTH, -1, -1); + } + } + + void recordDiscreteAccess(int uid, String packageName, int op, @Nullable String attributionTag, + @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, long accessTime, + long accessDuration) { + if (!isDiscreteOp(op, uid, flags)) { + return; + } + synchronized (mInMemoryLock) { + mDiscreteOps.addDiscreteAccess(op, uid, packageName, attributionTag, flags, uidState, + accessTime, accessDuration); + } + } + + void writeAndClearAccessHistory() { + synchronized (mOnDiskLock) { + final File[] files = mDiscreteAccessDir.listFiles(); + if (files != null && files.length > 0) { + for (File f : files) { + final String fileName = f.getName(); + if (!fileName.endsWith(TIMELINE_FILE_SUFFIX)) { + continue; + } + try { + long timestamp = Long.valueOf(fileName.substring(0, + fileName.length() - TIMELINE_FILE_SUFFIX.length())); + if (Instant.now().minus(TIMELINE_HISTORY_CUTOFF, + ChronoUnit.MILLIS).toEpochMilli() > timestamp) { + f.delete(); + Slog.e(TAG, "Deleting file " + fileName); + + } + } catch (Throwable t) { + Slog.e(TAG, "Error while cleaning timeline files: " + t.getMessage() + " " + + t.getStackTrace()); + } + } + } + } + DiscreteOps discreteOps; + synchronized (mInMemoryLock) { + discreteOps = mDiscreteOps; + mDiscreteOps = new DiscreteOps(); + } + if (discreteOps.isEmpty()) { + return; + } + long currentTimeStamp = Instant.now().toEpochMilli(); + try { + final File file = new File(mDiscreteAccessDir, currentTimeStamp + TIMELINE_FILE_SUFFIX); + discreteOps.writeToFile(file); + } catch (Throwable t) { + Slog.e(TAG, + "Error writing timeline state: " + t.getMessage() + " " + + Arrays.toString(t.getStackTrace())); + } + } + + void getHistoricalDiscreteOps(AppOpsManager.HistoricalOps result, long beginTimeMillis, + long endTimeMillis, @AppOpsManager.HistoricalOpsRequestFilter int filter, int uidFilter, + @Nullable String packageNameFilter, @Nullable String[] opNamesFilter, + @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter) { + writeAndClearAccessHistory(); + DiscreteOps discreteOps = new DiscreteOps(); + readDiscreteOpsFromDisk(discreteOps, beginTimeMillis, endTimeMillis, filter, uidFilter, + packageNameFilter, opNamesFilter, attributionTagFilter, flagsFilter); + discreteOps.applyToHistoricalOps(result); + return; + } + + private void readDiscreteOpsFromDisk(DiscreteOps discreteOps, long beginTimeMillis, + long endTimeMillis, @AppOpsManager.HistoricalOpsRequestFilter int filter, int uidFilter, + @Nullable String packageNameFilter, @Nullable String[] opNamesFilter, + @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter) { + synchronized (mOnDiskLock) { + long historyBeginTimeMillis = Instant.now().minus(TIMELINE_HISTORY_CUTOFF, + ChronoUnit.MILLIS).toEpochMilli(); + if (historyBeginTimeMillis > endTimeMillis) { + return; + } + beginTimeMillis = max(beginTimeMillis, historyBeginTimeMillis); + + final File[] files = mDiscreteAccessDir.listFiles(); + if (files != null && files.length > 0) { + for (File f : files) { + final String fileName = f.getName(); + if (!fileName.endsWith(TIMELINE_FILE_SUFFIX)) { + continue; + } + long timestamp = Long.valueOf(fileName.substring(0, + fileName.length() - TIMELINE_FILE_SUFFIX.length())); + if (timestamp < beginTimeMillis) { + continue; + } + discreteOps.readFromFile(f, beginTimeMillis, endTimeMillis, filter, uidFilter, + packageNameFilter, opNamesFilter, attributionTagFilter, flagsFilter); + } + } + } + } + + void clearHistory() { + synchronized (mOnDiskLock) { + synchronized (mInMemoryLock) { + mDiscreteOps = new DiscreteOps(); + } + FileUtils.deleteContentsAndDir(mDiscreteAccessDir); + createDiscreteAccessDir(); + } + } + + public static boolean isDiscreteOp(int op, int uid, @AppOpsManager.OpFlags int flags) { + if (!isDiscreteOp(op)) { + return false; + } + if (!isDiscreteUid(uid)) { + return false; + } + if ((flags & (OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED)) == 0) { + return false; + } + return true; + } + + static boolean isDiscreteOp(int op) { + if (op != OP_CAMERA && op != OP_RECORD_AUDIO && op != OP_FINE_LOCATION + && op != OP_COARSE_LOCATION) { + return false; + } + return true; + } + + static boolean isDiscreteUid(int uid) { + if (uid < Process.FIRST_APPLICATION_UID) { + return false; + } + return true; + } + + private final class DiscreteOps { + ArrayMap<Integer, DiscreteUidOps> mUids; + + DiscreteOps() { + mUids = new ArrayMap<>(); + } + + void addDiscreteAccess(int op, int uid, @NonNull String packageName, + @Nullable String attributionTag, @AppOpsManager.OpFlags int flags, + @AppOpsManager.UidState int uidState, long accessTime, long accessDuration) { + getOrCreateDiscreteUidOps(uid).addDiscreteAccess(op, packageName, attributionTag, flags, + uidState, accessTime, accessDuration); + } + + private void applyToHistoricalOps(AppOpsManager.HistoricalOps result) { + int nUids = mUids.size(); + for (int i = 0; i < nUids; i++) { + mUids.valueAt(i).applyToHistory(result, mUids.keyAt(i)); + } + } + + private void writeToFile(File f) throws Exception { + FileOutputStream stream = new FileOutputStream(f); + TypedXmlSerializer out = Xml.resolveSerializer(stream); + + out.startDocument(null, true); + out.startTag(null, TAG_HISTORY); + out.attributeInt(null, ATTR_VERSION, CURRENT_VERSION); + + int nUids = mUids.size(); + for (int i = 0; i < nUids; i++) { + out.startTag(null, TAG_UID); + out.attributeInt(null, ATTR_UID, mUids.keyAt(i)); + mUids.valueAt(i).serialize(out); + out.endTag(null, TAG_UID); + } + out.endTag(null, TAG_HISTORY); + out.endDocument(); + stream.close(); + } + + private DiscreteUidOps getOrCreateDiscreteUidOps(int uid) { + DiscreteUidOps result = mUids.get(uid); + if (result == null) { + result = new DiscreteUidOps(); + mUids.put(uid, result); + } + return result; + } + + boolean isEmpty() { + return mUids.isEmpty(); + } + + private void readFromFile(File f, long beginTimeMillis, long endTimeMillis, + @AppOpsManager.HistoricalOpsRequestFilter int filter, + int uidFilter, @Nullable String packageNameFilter, @Nullable String[] opNamesFilter, + @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter) { + try { + FileInputStream stream = new FileInputStream(f); + TypedXmlPullParser parser = Xml.resolvePullParser(stream); + XmlUtils.beginDocument(parser, TAG_HISTORY); + + // We haven't released version 1 and have more detailed + // accounting - just nuke the current state + final int version = parser.getAttributeInt(null, ATTR_VERSION); + if (version != CURRENT_VERSION) { + throw new IllegalStateException("Dropping unsupported discrete history " + f); + } + + int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (TAG_UID.equals(parser.getName())) { + int uid = parser.getAttributeInt(null, ATTR_UID, -1); + if ((filter & FILTER_BY_UID) != 0 && uid != uidFilter) { + continue; + } + getOrCreateDiscreteUidOps(uid).deserialize(parser, beginTimeMillis, + endTimeMillis, filter, packageNameFilter, opNamesFilter, + attributionTagFilter, flagsFilter); + } + } + } catch (Throwable t) { + Slog.e(TAG, "Failed to read file " + f.getName() + " " + t.getMessage() + " " + + Arrays.toString(t.getStackTrace())); + } + + } + } + + private final class DiscreteUidOps { + ArrayMap<String, DiscretePackageOps> mPackages; + + DiscreteUidOps() { + mPackages = new ArrayMap<>(); + } + + void addDiscreteAccess(int op, @NonNull String packageName, @Nullable String attributionTag, + @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, + long accessTime, long accessDuration) { + getOrCreateDiscretePackageOps(packageName).addDiscreteAccess(op, attributionTag, flags, + uidState, accessTime, accessDuration); + } + + private DiscretePackageOps getOrCreateDiscretePackageOps(String packageName) { + DiscretePackageOps result = mPackages.get(packageName); + if (result == null) { + result = new DiscretePackageOps(); + mPackages.put(packageName, result); + } + return result; + } + + private void applyToHistory(AppOpsManager.HistoricalOps result, int uid) { + int nPackages = mPackages.size(); + for (int i = 0; i < nPackages; i++) { + mPackages.valueAt(i).applyToHistory(result, uid, mPackages.keyAt(i)); + } + } + + void serialize(TypedXmlSerializer out) throws Exception { + int nPackages = mPackages.size(); + for (int i = 0; i < nPackages; i++) { + out.startTag(null, TAG_PACKAGE); + out.attribute(null, ATTR_PACKAGE_NAME, mPackages.keyAt(i)); + mPackages.valueAt(i).serialize(out); + out.endTag(null, TAG_PACKAGE); + } + } + + void deserialize(TypedXmlPullParser parser, long beginTimeMillis, + long endTimeMillis, @AppOpsManager.HistoricalOpsRequestFilter int filter, + @Nullable String packageNameFilter, + @Nullable String[] opNamesFilter, @Nullable String attributionTagFilter, + @AppOpsManager.OpFlags int flagsFilter) throws Exception { + int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (TAG_PACKAGE.equals(parser.getName())) { + String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME); + if ((filter & FILTER_BY_PACKAGE_NAME) != 0 + && !packageName.equals(packageNameFilter)) { + continue; + } + getOrCreateDiscretePackageOps(packageName).deserialize(parser, beginTimeMillis, + endTimeMillis, filter, opNamesFilter, attributionTagFilter, + flagsFilter); + } + } + } + } + + private final class DiscretePackageOps { + ArrayMap<Integer, DiscreteOp> mPackageOps; + + DiscretePackageOps() { + mPackageOps = new ArrayMap<>(); + } + + void addDiscreteAccess(int op, @Nullable String attributionTag, + @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, + long accessTime, long accessDuration) { + getOrCreateDiscreteOp(op).addDiscreteAccess(attributionTag, flags, uidState, accessTime, + accessDuration); + } + + private DiscreteOp getOrCreateDiscreteOp(int op) { + DiscreteOp result = mPackageOps.get(op); + if (result == null) { + result = new DiscreteOp(); + mPackageOps.put(op, result); + } + return result; + } + + private void applyToHistory(AppOpsManager.HistoricalOps result, int uid, + @NonNull String packageName) { + int nPackageOps = mPackageOps.size(); + for (int i = 0; i < nPackageOps; i++) { + mPackageOps.valueAt(i).applyToHistory(result, uid, packageName, + mPackageOps.keyAt(i)); + } + } + + void serialize(TypedXmlSerializer out) throws Exception { + int nOps = mPackageOps.size(); + for (int i = 0; i < nOps; i++) { + out.startTag(null, TAG_OP); + out.attributeInt(null, ATTR_OP_ID, mPackageOps.keyAt(i)); + mPackageOps.valueAt(i).serialize(out); + out.endTag(null, TAG_OP); + } + } + + void deserialize(TypedXmlPullParser parser, long beginTimeMillis, long endTimeMillis, + @AppOpsManager.HistoricalOpsRequestFilter int filter, + @Nullable String[] opNamesFilter, @Nullable String attributionTagFilter, + @AppOpsManager.OpFlags int flagsFilter) throws Exception { + int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (TAG_OP.equals(parser.getName())) { + int op = parser.getAttributeInt(null, ATTR_OP_ID); + if ((filter & FILTER_BY_OP_NAMES) != 0 && !ArrayUtils.contains(opNamesFilter, + AppOpsManager.opToPublicName(op))) { + continue; + } + getOrCreateDiscreteOp(op).deserialize(parser, beginTimeMillis, endTimeMillis, + filter, attributionTagFilter, flagsFilter); + } + } + } + } + + private final class DiscreteOp { + ArrayMap<String, List<DiscreteOpEvent>> mAttributedOps; + + DiscreteOp() { + mAttributedOps = new ArrayMap<>(); + } + + void addDiscreteAccess(@Nullable String attributionTag, + @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, + long accessTime, long accessDuration) { + List<DiscreteOpEvent> attributedOps = getOrCreateDiscreteOpEventsList( + attributionTag); + accessTime = Instant.ofEpochMilli(accessTime).truncatedTo( + ChronoUnit.MINUTES).toEpochMilli(); + + int nAttributedOps = attributedOps.size(); + for (int i = nAttributedOps - 1; i >= 0; i--) { + DiscreteOpEvent previousOp = attributedOps.get(i); + if (previousOp.mNoteTime < accessTime) { + break; + } + if (previousOp.mOpFlag == flags && previousOp.mUidState == uidState) { + return; + } + } + attributedOps.add(new DiscreteOpEvent(accessTime, accessDuration, uidState, flags)); + } + + private List<DiscreteOpEvent> getOrCreateDiscreteOpEventsList(String attributionTag) { + List<DiscreteOpEvent> result = mAttributedOps.get(attributionTag); + if (result == null) { + result = new ArrayList<>(); + mAttributedOps.put(attributionTag, result); + } + return result; + } + + private void applyToHistory(AppOpsManager.HistoricalOps result, int uid, + @NonNull String packageName, int op) { + int nOps = mAttributedOps.size(); + for (int i = 0; i < nOps; i++) { + String tag = mAttributedOps.keyAt(i); + List<DiscreteOpEvent> events = mAttributedOps.valueAt(i); + int nEvents = events.size(); + for (int j = 0; j < nEvents; j++) { + DiscreteOpEvent event = events.get(j); + result.addDiscreteAccess(op, uid, packageName, tag, event.mUidState, + event.mOpFlag, event.mNoteTime, event.mNoteDuration); + } + } + } + + void serialize(TypedXmlSerializer out) throws Exception { + int nAttributions = mAttributedOps.size(); + for (int i = 0; i < nAttributions; i++) { + out.startTag(null, TAG_TAG); + String tag = mAttributedOps.keyAt(i); + if (tag != null) { + out.attribute(null, ATTR_TAG, mAttributedOps.keyAt(i)); + } + List<DiscreteOpEvent> ops = mAttributedOps.valueAt(i); + int nOps = ops.size(); + for (int j = 0; j < nOps; j++) { + out.startTag(null, TAG_ENTRY); + ops.get(j).serialize(out); + out.endTag(null, TAG_ENTRY); + } + out.endTag(null, TAG_TAG); + } + } + + void deserialize(TypedXmlPullParser parser, long beginTimeMillis, long endTimeMillis, + @AppOpsManager.HistoricalOpsRequestFilter int filter, + @Nullable String attributionTagFilter, + @AppOpsManager.OpFlags int flagsFilter) throws Exception { + int outerDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + if (TAG_TAG.equals(parser.getName())) { + String attributionTag = parser.getAttributeValue(null, ATTR_TAG); + if ((filter & FILTER_BY_ATTRIBUTION_TAG) != 0 && !attributionTag.equals( + attributionTagFilter)) { + continue; + } + List<DiscreteOpEvent> events = getOrCreateDiscreteOpEventsList( + attributionTag); + int innerDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, innerDepth)) { + if (TAG_ENTRY.equals(parser.getName())) { + long noteTime = parser.getAttributeLong(null, ATTR_NOTE_TIME); + long noteDuration = parser.getAttributeLong(null, ATTR_NOTE_DURATION, + -1); + int uidState = parser.getAttributeInt(null, ATTR_UID_STATE); + int opFlags = parser.getAttributeInt(null, ATTR_FLAGS); + if ((flagsFilter & opFlags) == 0) { + continue; + } + if ((noteTime + noteDuration < beginTimeMillis + && noteTime > endTimeMillis)) { + continue; + } + DiscreteOpEvent event = new DiscreteOpEvent(noteTime, noteDuration, + uidState, opFlags); + events.add(event); + } + } + Collections.sort(events, (a, b) -> a.mNoteTime < b.mNoteTime ? -1 + : (a.mNoteTime == b.mNoteTime ? 0 : 1)); + } + } + } + } + + private final class DiscreteOpEvent { + final long mNoteTime; + final long mNoteDuration; + final @AppOpsManager.UidState int mUidState; + final @AppOpsManager.OpFlags int mOpFlag; + + DiscreteOpEvent(long noteTime, long noteDuration, @AppOpsManager.UidState int uidState, + @AppOpsManager.OpFlags int opFlag) { + mNoteTime = noteTime; + mNoteDuration = noteDuration; + mUidState = uidState; + mOpFlag = opFlag; + } + + private void serialize(TypedXmlSerializer out) throws Exception { + out.attributeLong(null, ATTR_NOTE_TIME, mNoteTime); + if (mNoteDuration != -1) { + out.attributeLong(null, ATTR_NOTE_DURATION, mNoteDuration); + } + out.attributeInt(null, ATTR_UID_STATE, mUidState); + out.attributeInt(null, ATTR_FLAGS, mOpFlag); + } + } +} + diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java index 17fd32c57e09..1c43fedd3112 100644 --- a/services/core/java/com/android/server/appop/HistoricalRegistry.java +++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java @@ -19,6 +19,8 @@ import static android.app.AppOpsManager.FILTER_BY_ATTRIBUTION_TAG; import static android.app.AppOpsManager.FILTER_BY_OP_NAMES; import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME; import static android.app.AppOpsManager.FILTER_BY_UID; +import static android.app.AppOpsManager.HISTORY_FLAG_AGGREGATE; +import static android.app.AppOpsManager.HISTORY_FLAG_DISCRETE; import android.annotation.NonNull; import android.annotation.Nullable; @@ -30,6 +32,7 @@ import android.app.AppOpsManager.HistoricalOpsRequestFilter; import android.app.AppOpsManager.HistoricalPackageOps; import android.app.AppOpsManager.HistoricalUidOps; import android.app.AppOpsManager.OpFlags; +import android.app.AppOpsManager.OpHistoryFlags; import android.app.AppOpsManager.UidState; import android.content.ContentResolver; import android.database.ContentObserver; @@ -61,9 +64,7 @@ import com.android.internal.util.XmlUtils; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.FgThread; -import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileInputStream; @@ -71,7 +72,6 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -85,7 +85,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; /** - * This class managers historical app op state. This includes reading, persistence, + * This class manages historical app op state. This includes reading, persistence, * accounting, querying. * <p> * The history is kept forever in multiple files. Each file time contains the @@ -138,6 +138,8 @@ final class HistoricalRegistry { private static final String PARAMETER_ASSIGNMENT = "="; private static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_enabled"; + volatile @NonNull DiscreteRegistry mDiscreteRegistry; + @GuardedBy("mLock") private @NonNull LinkedList<HistoricalOps> mPendingWrites = new LinkedList<>(); @@ -199,6 +201,7 @@ final class HistoricalRegistry { HistoricalRegistry(@NonNull Object lock) { mInMemoryLock = lock; + mDiscreteRegistry = new DiscreteRegistry(lock); } HistoricalRegistry(@NonNull HistoricalRegistry other) { @@ -352,36 +355,49 @@ final class HistoricalRegistry { } } - void getHistoricalOpsFromDiskRaw(int uid, @NonNull String packageName, + void getHistoricalOpsFromDiskRaw(int uid, @Nullable String packageName, @Nullable String attributionTag, @Nullable String[] opNames, - @HistoricalOpsRequestFilter int filter, long beginTimeMillis, long endTimeMillis, - @OpFlags int flags, @NonNull RemoteCallback callback) { + @OpHistoryFlags int historyFlags, @HistoricalOpsRequestFilter int filter, + long beginTimeMillis, long endTimeMillis, @OpFlags int flags, + @NonNull RemoteCallback callback) { if (!isApiEnabled()) { callback.sendResult(new Bundle()); return; } - synchronized (mOnDiskLock) { - synchronized (mInMemoryLock) { - if (!isPersistenceInitializedMLocked()) { - Slog.e(LOG_TAG, "Interaction before persistence initialized"); - callback.sendResult(new Bundle()); - return; + final HistoricalOps result = new HistoricalOps(beginTimeMillis, endTimeMillis); + + if ((historyFlags & HISTORY_FLAG_AGGREGATE) != 0) { + synchronized (mOnDiskLock) { + synchronized (mInMemoryLock) { + if (!isPersistenceInitializedMLocked()) { + Slog.e(LOG_TAG, "Interaction before persistence initialized"); + callback.sendResult(new Bundle()); + return; + } + mPersistence.collectHistoricalOpsDLocked(result, uid, packageName, + attributionTag, + opNames, filter, beginTimeMillis, endTimeMillis, flags); + } - final HistoricalOps result = new HistoricalOps(beginTimeMillis, endTimeMillis); - mPersistence.collectHistoricalOpsDLocked(result, uid, packageName, attributionTag, - opNames, filter, beginTimeMillis, endTimeMillis, flags); - final Bundle payload = new Bundle(); - payload.putParcelable(AppOpsManager.KEY_HISTORICAL_OPS, result); - callback.sendResult(payload); } } + + if ((historyFlags & HISTORY_FLAG_DISCRETE) != 0) { + mDiscreteRegistry.getHistoricalDiscreteOps(result, beginTimeMillis, endTimeMillis, + filter, uid, packageName, opNames, attributionTag, + flags); + } + + final Bundle payload = new Bundle(); + payload.putParcelable(AppOpsManager.KEY_HISTORICAL_OPS, result); + callback.sendResult(payload); } - void getHistoricalOps(int uid, @NonNull String packageName, @Nullable String attributionTag, - @Nullable String[] opNames, @HistoricalOpsRequestFilter int filter, - long beginTimeMillis, long endTimeMillis, @OpFlags int flags, - @NonNull RemoteCallback callback) { + void getHistoricalOps(int uid, @Nullable String packageName, @Nullable String attributionTag, + @Nullable String[] opNames, @OpHistoryFlags int historyFlags, + @HistoricalOpsRequestFilter int filter, long beginTimeMillis, long endTimeMillis, + @OpFlags int flags, @NonNull RemoteCallback callback) { if (!isApiEnabled()) { callback.sendResult(new Bundle()); return; @@ -392,6 +408,8 @@ final class HistoricalRegistry { endTimeMillis = currentTimeMillis; } + final Bundle payload = new Bundle(); + // Argument times are based off epoch start while our internal store is // based off now, so take this into account. final long inMemoryAdjBeginTimeMillis = Math.max(currentTimeMillis - endTimeMillis, 0); @@ -399,55 +417,63 @@ final class HistoricalRegistry { final HistoricalOps result = new HistoricalOps(inMemoryAdjBeginTimeMillis, inMemoryAdjEndTimeMillis); - synchronized (mOnDiskLock) { - final List<HistoricalOps> pendingWrites; - final HistoricalOps currentOps; - boolean collectOpsFromDisk; - - synchronized (mInMemoryLock) { - if (!isPersistenceInitializedMLocked()) { - Slog.e(LOG_TAG, "Interaction before persistence initialized"); - callback.sendResult(new Bundle()); - return; - } - - currentOps = getUpdatedPendingHistoricalOpsMLocked(currentTimeMillis); - if (!(inMemoryAdjBeginTimeMillis >= currentOps.getEndTimeMillis() - || inMemoryAdjEndTimeMillis <= currentOps.getBeginTimeMillis())) { - // Some of the current batch falls into the query, so extract that. - final HistoricalOps currentOpsCopy = new HistoricalOps(currentOps); - currentOpsCopy.filter(uid, packageName, attributionTag, opNames, filter, - inMemoryAdjBeginTimeMillis, inMemoryAdjEndTimeMillis); - result.merge(currentOpsCopy); - } - pendingWrites = new ArrayList<>(mPendingWrites); - mPendingWrites.clear(); - collectOpsFromDisk = inMemoryAdjEndTimeMillis > currentOps.getEndTimeMillis(); - } + if ((historyFlags & HISTORY_FLAG_DISCRETE) != 0) { + mDiscreteRegistry.getHistoricalDiscreteOps(result, beginTimeMillis, endTimeMillis, + filter, uid, packageName, opNames, attributionTag, flags); + } - // If the query was only for in-memory state - done. - if (collectOpsFromDisk) { - // If there is a write in flight we need to force it now - persistPendingHistory(pendingWrites); - // Collect persisted state. - final long onDiskAndInMemoryOffsetMillis = currentTimeMillis - - mNextPersistDueTimeMillis + mBaseSnapshotInterval; - final long onDiskAdjBeginTimeMillis = Math.max(inMemoryAdjBeginTimeMillis - - onDiskAndInMemoryOffsetMillis, 0); - final long onDiskAdjEndTimeMillis = Math.max(inMemoryAdjEndTimeMillis - - onDiskAndInMemoryOffsetMillis, 0); - mPersistence.collectHistoricalOpsDLocked(result, uid, packageName, attributionTag, - opNames, filter, onDiskAdjBeginTimeMillis, onDiskAdjEndTimeMillis, flags); - } + if ((historyFlags & HISTORY_FLAG_AGGREGATE) != 0) { + synchronized (mOnDiskLock) { + final List<HistoricalOps> pendingWrites; + final HistoricalOps currentOps; + boolean collectOpsFromDisk; - // Rebase the result time to be since epoch. - result.setBeginAndEndTime(beginTimeMillis, endTimeMillis); + synchronized (mInMemoryLock) { + if (!isPersistenceInitializedMLocked()) { + Slog.e(LOG_TAG, "Interaction before persistence initialized"); + callback.sendResult(new Bundle()); + return; + } - // Send back the result. - final Bundle payload = new Bundle(); - payload.putParcelable(AppOpsManager.KEY_HISTORICAL_OPS, result); - callback.sendResult(payload); - } + currentOps = getUpdatedPendingHistoricalOpsMLocked(currentTimeMillis); + if (!(inMemoryAdjBeginTimeMillis >= currentOps.getEndTimeMillis() + || inMemoryAdjEndTimeMillis <= currentOps.getBeginTimeMillis())) { + // Some of the current batch falls into the query, so extract that. + final HistoricalOps currentOpsCopy = new HistoricalOps(currentOps); + currentOpsCopy.filter(uid, packageName, attributionTag, opNames, + historyFlags, filter, inMemoryAdjBeginTimeMillis, + inMemoryAdjEndTimeMillis); + result.merge(currentOpsCopy); + } + pendingWrites = new ArrayList<>(mPendingWrites); + mPendingWrites.clear(); + collectOpsFromDisk = inMemoryAdjEndTimeMillis > currentOps.getEndTimeMillis(); + } + + // If the query was only for in-memory state - done. + if (collectOpsFromDisk) { + // If there is a write in flight we need to force it now + persistPendingHistory(pendingWrites); + // Collect persisted state. + final long onDiskAndInMemoryOffsetMillis = currentTimeMillis + - mNextPersistDueTimeMillis + mBaseSnapshotInterval; + final long onDiskAdjBeginTimeMillis = Math.max(inMemoryAdjBeginTimeMillis + - onDiskAndInMemoryOffsetMillis, 0); + final long onDiskAdjEndTimeMillis = Math.max(inMemoryAdjEndTimeMillis + - onDiskAndInMemoryOffsetMillis, 0); + mPersistence.collectHistoricalOpsDLocked(result, uid, packageName, + attributionTag, + opNames, filter, onDiskAdjBeginTimeMillis, onDiskAdjEndTimeMillis, + flags); + } + } + } + // Rebase the result time to be since epoch. + result.setBeginAndEndTime(beginTimeMillis, endTimeMillis); + + // Send back the result. + payload.putParcelable(AppOpsManager.KEY_HISTORICAL_OPS, result); + callback.sendResult(payload); } void incrementOpAccessedCount(int op, int uid, @NonNull String packageName, @@ -692,6 +718,7 @@ final class HistoricalRegistry { } persistPendingHistory(pendingWrites); } + mDiscreteRegistry.writeAndClearAccessHistory(); } private void persistPendingHistory(@NonNull List<HistoricalOps> pendingWrites) { |