diff options
| author | 2022-01-27 09:38:37 +0000 | |
|---|---|---|
| committer | 2022-01-27 09:38:37 +0000 | |
| commit | 077c836e99fb7adeb2ed33cfbaec973a31dbcfc6 (patch) | |
| tree | dc58637fb86a3bb8eb1169a814a7ee3173acebb1 | |
| parent | 96a16c6cd5d4ae44ac516c2f11573a3635d5e958 (diff) | |
| parent | 7ce974c2b2889af0ff1096fedcc94b907168a1d3 (diff) | |
Merge "Add APIs for querying broadcast response stats."
10 files changed, 985 insertions, 0 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 6fc1ecbce1c8..18a3f863cb7e 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2252,6 +2252,18 @@ package android.app.time { package android.app.usage { + public final class BroadcastResponseStats implements android.os.Parcelable { + ctor public BroadcastResponseStats(@NonNull String); + method public int describeContents(); + method @IntRange(from=0) public int getBroadcastsDispatchedCount(); + method @IntRange(from=0) public int getNotificationsCancelledCount(); + method @IntRange(from=0) public int getNotificationsPostedCount(); + method @IntRange(from=0) public int getNotificationsUpdatedCount(); + method @NonNull public String getPackageName(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.usage.BroadcastResponseStats> CREATOR; + } + public final class CacheQuotaHint implements android.os.Parcelable { ctor public CacheQuotaHint(@NonNull android.app.usage.CacheQuotaHint.Builder); method public int describeContents(); @@ -2307,11 +2319,13 @@ package android.app.usage { } public final class UsageStatsManager { + method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void clearBroadcastResponseStats(@NonNull String, @IntRange(from=1) long); method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public int getAppStandbyBucket(String); method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public java.util.Map<java.lang.String,java.lang.Integer> getAppStandbyBuckets(); method @RequiresPermission(allOf={android.Manifest.permission.INTERACT_ACROSS_USERS, android.Manifest.permission.PACKAGE_USAGE_STATS}) public long getLastTimeAnyComponentUsed(@NonNull String); method public int getUsageSource(); method @RequiresPermission(android.Manifest.permission.BIND_CARRIER_SERVICES) public void onCarrierPrivilegedAppsChanged(); + method @NonNull @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public android.app.usage.BroadcastResponseStats queryBroadcastResponseStats(@NonNull String, @IntRange(from=1) long); method @RequiresPermission(allOf={android.Manifest.permission.SUSPEND_APPS, android.Manifest.permission.OBSERVE_APP_USAGE}) public void registerAppUsageLimitObserver(int, @NonNull String[], @NonNull java.time.Duration, @NonNull java.time.Duration, @Nullable android.app.PendingIntent); method @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE) public void registerAppUsageObserver(int, @NonNull String[], long, @NonNull java.util.concurrent.TimeUnit, @NonNull android.app.PendingIntent); method @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE) public void registerUsageSessionObserver(int, @NonNull String[], @NonNull java.time.Duration, @NonNull java.time.Duration, @NonNull android.app.PendingIntent, @Nullable android.app.PendingIntent); diff --git a/core/java/android/app/usage/BroadcastResponseStats.aidl b/core/java/android/app/usage/BroadcastResponseStats.aidl new file mode 100644 index 000000000000..599284162629 --- /dev/null +++ b/core/java/android/app/usage/BroadcastResponseStats.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 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.usage; + +parcelable BroadcastResponseStats;
\ No newline at end of file diff --git a/core/java/android/app/usage/BroadcastResponseStats.java b/core/java/android/app/usage/BroadcastResponseStats.java new file mode 100644 index 000000000000..5acc3dda9e48 --- /dev/null +++ b/core/java/android/app/usage/BroadcastResponseStats.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2022 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.usage; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.app.BroadcastOptions; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Class containing a collection of stats related to response events started from an app + * after receiving a broadcast. + * + * @hide + */ +@SystemApi +public final class BroadcastResponseStats implements Parcelable { + private final String mPackageName; + private int mBroadcastsDispatchedCount; + private int mNotificationsPostedCount; + private int mNotificationsUpdatedCount; + private int mNotificationsCancelledCount; + + public BroadcastResponseStats(@NonNull String packageName) { + mPackageName = packageName; + } + + private BroadcastResponseStats(@NonNull Parcel in) { + mPackageName = in.readString8(); + mBroadcastsDispatchedCount = in.readInt(); + mNotificationsPostedCount = in.readInt(); + mNotificationsUpdatedCount = in.readInt(); + mNotificationsCancelledCount = in.readInt(); + } + + /** + * @return the name of the package that the stats in this object correspond to. + */ + @NonNull + public String getPackageName() { + return mPackageName; + } + + /** + * Returns the total number of broadcasts that were dispatched to the app by the caller. + * + * <b> Note that the returned count will only include the broadcasts that the caller explicitly + * requested to record using + * {@link BroadcastOptions#recordResponseEventWhileInBackground(long)}. + * + * @return the total number of broadcasts that were dispatched to the app. + */ + @IntRange(from = 0) + public int getBroadcastsDispatchedCount() { + return mBroadcastsDispatchedCount; + } + + /** + * Returns the total number of notifications posted by the app soon after receiving a + * broadcast. + * + * <b> Note that the returned count will only include the notifications that correspond to the + * broadcasts that the caller explicitly requested to record using + * {@link BroadcastOptions#recordResponseEventWhileInBackground(long)}. + * + * @return the total number of notifications posted by the app soon after receiving + * a broadcast. + */ + @IntRange(from = 0) + public int getNotificationsPostedCount() { + return mNotificationsPostedCount; + } + + /** + * Returns the total number of notifications updated by the app soon after receiving a + * broadcast. + * + * <b> Note that the returned count will only include the notifications that correspond to the + * broadcasts that the caller explicitly requested to record using + * {@link BroadcastOptions#recordResponseEventWhileInBackground(long)}. + * + * @return the total number of notifications updated by the app soon after receiving + * a broadcast. + */ + @IntRange(from = 0) + public int getNotificationsUpdatedCount() { + return mNotificationsUpdatedCount; + } + + /** + * Returns the total number of notifications cancelled by the app soon after receiving a + * broadcast. + * + * <b> Note that the returned count will only include the notifications that correspond to the + * broadcasts that the caller explicitly requested to record using + * {@link BroadcastOptions#recordResponseEventWhileInBackground(long)}. + * + * @return the total number of notifications cancelled by the app soon after receiving + * a broadcast. + */ + @IntRange(from = 0) + public int getNotificationsCancelledCount() { + return mNotificationsCancelledCount; + } + + /** @hide */ + public void incrementBroadcastsDispatchedCount(@IntRange(from = 0) int count) { + mBroadcastsDispatchedCount += count; + } + + /** @hide */ + public void incrementNotificationsPostedCount(@IntRange(from = 0) int count) { + mNotificationsPostedCount += count; + } + + /** @hide */ + public void incrementNotificationsUpdatedCount(@IntRange(from = 0) int count) { + mNotificationsUpdatedCount += count; + } + + /** @hide */ + public void incrementNotificationsCancelledCount(@IntRange(from = 0) int count) { + mNotificationsCancelledCount += count; + } + + /** @hide */ + public void addCounts(@NonNull BroadcastResponseStats stats) { + incrementBroadcastsDispatchedCount(stats.getBroadcastsDispatchedCount()); + incrementNotificationsPostedCount(stats.getNotificationsPostedCount()); + incrementNotificationsUpdatedCount(stats.getNotificationsUpdatedCount()); + incrementNotificationsCancelledCount(stats.getNotificationsCancelledCount()); + } + + @Override + public @NonNull String toString() { + return "stats {" + + "broadcastsSent=" + mBroadcastsDispatchedCount + + ",notificationsPosted=" + mNotificationsPostedCount + + ",notificationsUpdated=" + mNotificationsUpdatedCount + + ",notificationsCancelled=" + mNotificationsCancelledCount + + "}"; + } + + @Override + public @ContentsFlags int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, @WriteFlags int flags) { + dest.writeString8(mPackageName); + dest.writeInt(mBroadcastsDispatchedCount); + dest.writeInt(mNotificationsPostedCount); + dest.writeInt(mNotificationsUpdatedCount); + dest.writeInt(mNotificationsCancelledCount); + } + + public static final @NonNull Creator<BroadcastResponseStats> CREATOR = + new Creator<BroadcastResponseStats>() { + @Override + public @NonNull BroadcastResponseStats createFromParcel(@NonNull Parcel source) { + return new BroadcastResponseStats(source); + } + + @Override + public @NonNull BroadcastResponseStats[] newArray(int size) { + return new BroadcastResponseStats[size]; + } + }; +} diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl index 170d766c794c..6f8fea15f200 100644 --- a/core/java/android/app/usage/IUsageStatsManager.aidl +++ b/core/java/android/app/usage/IUsageStatsManager.aidl @@ -17,6 +17,7 @@ package android.app.usage; import android.app.PendingIntent; +import android.app.usage.BroadcastResponseStats; import android.app.usage.UsageEvents; import android.content.pm.ParceledListSlice; @@ -71,4 +72,10 @@ interface IUsageStatsManager { int getUsageSource(); void forceUsageSourceSettingRead(); long getLastTimeAnyComponentUsed(String packageName, String callingPackage); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)") + BroadcastResponseStats queryBroadcastResponseStats( + String packageName, long id, String callingPackage, int userId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)") + void clearBroadcastResponseStats(String packageName, long id, String callingPackage, + int userId); } diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index 33efa01ace90..b81c62d29076 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -18,6 +18,7 @@ package android.app.usage; import android.annotation.CurrentTimeMillisLong; import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -26,6 +27,7 @@ import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UserHandleAware; import android.app.Activity; +import android.app.BroadcastOptions; import android.app.PendingIntent; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; @@ -1390,4 +1392,65 @@ public final class UsageStatsManager { throw re.rethrowFromSystemServer(); } } + + /** + * Returns the broadcast response stats since the last boot corresponding to + * {@code packageName} and {@code id}. + * + * <p>Broadcast response stats will include the aggregated data of what actions an app took upon + * receiving a broadcast. This data will consider the broadcasts that the caller sent to + * {@code packageName} and explicitly requested to record the response events using + * {@link BroadcastOptions#recordResponseEventWhileInBackground(long)}. + * + * @param packageName The name of the package that the caller wants to query for. + * @param id The ID corresponding to the broadcasts that the caller wants to query for. This is + * the ID the caller specifies when requesting a broadcast response event to be + * recorded using {@link BroadcastOptions#recordResponseEventWhileInBackground(long)}. + * + * @return the broadcast response stats corresponding to {@code packageName} and {@code id}. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) + @UserHandleAware + @NonNull + public BroadcastResponseStats queryBroadcastResponseStats( + @NonNull String packageName, @IntRange(from = 1) long id) { + try { + return mService.queryBroadcastResponseStats(packageName, id, + mContext.getOpPackageName(), mContext.getUserId()); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Clears the broadcast response stats corresponding to {@code packageName} and {@code id}. + * + * When a caller uses this API, stats related to the events occurring till that point will be + * cleared and subsequent calls to {@link #queryBroadcastResponseStats(String, long)} will + * return stats related to events occurring after this. + * + * @param packageName The name of the package that the caller wants to clear the data for. + * @param id The ID corresponding to the broadcasts that the caller wants to clear the data for. + * This is the ID the caller specifies when requesting a broadcast response event + * to be recorded using + * {@link BroadcastOptions#recordResponseEventWhileInBackground(long)}. + * + * @see #queryBroadcastResponseStats(String, long) + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) + @UserHandleAware + public void clearBroadcastResponseStats(@NonNull String packageName, + @IntRange(from = 1) long id) { + try { + mService.clearBroadcastResponseStats(packageName, id, + mContext.getOpPackageName(), mContext.getUserId()); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } } diff --git a/services/usage/java/com/android/server/usage/BroadcastEvent.java b/services/usage/java/com/android/server/usage/BroadcastEvent.java new file mode 100644 index 000000000000..ceb79c107d30 --- /dev/null +++ b/services/usage/java/com/android/server/usage/BroadcastEvent.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 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.usage; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; + +import java.util.Objects; + +/** + * Contains the data needed to identify a broadcast event. + */ +class BroadcastEvent { + private int mSourceUid; + private String mTargetPackage; + private int mTargetUserId; + private long mIdForResponseEvent; + + BroadcastEvent(int sourceUid, @NonNull String targetPackage, @UserIdInt int targetUserId, + long idForResponseEvent) { + mSourceUid = sourceUid; + mTargetPackage = targetPackage; + mTargetUserId = targetUserId; + mIdForResponseEvent = idForResponseEvent; + } + + public int getSourceUid() { + return mSourceUid; + } + + public @NonNull String getTargetPackage() { + return mTargetPackage; + } + + public @UserIdInt int getTargetUserId() { + return mTargetUserId; + } + + public long getIdForResponseEvent() { + return mIdForResponseEvent; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !(obj instanceof BroadcastEvent)) { + return false; + } + final BroadcastEvent other = (BroadcastEvent) obj; + return this.mSourceUid == other.mSourceUid + && this.mIdForResponseEvent == other.mIdForResponseEvent + && this.mTargetUserId == other.mTargetUserId + && this.mTargetPackage.equals(other.mTargetPackage); + } + + @Override + public int hashCode() { + return Objects.hash(mSourceUid, mTargetPackage, mTargetUserId, + mIdForResponseEvent); + } + + @Override + public @NonNull String toString() { + return "BroadcastEvent {" + + "srcUid=" + mSourceUid + + ",tgtPkg=" + mTargetPackage + + ",tgtUser=" + mTargetUserId + + ",id=" + mIdForResponseEvent + + "}"; + } +} diff --git a/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java b/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java new file mode 100644 index 000000000000..aa01f3152bb4 --- /dev/null +++ b/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2022 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.usage; + +import android.annotation.ElapsedRealtimeLong; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.usage.BroadcastResponseStats; +import android.os.UserHandle; +import android.util.LongSparseArray; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.IndentingPrintWriter; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +class BroadcastResponseStatsTracker { + private static final String TAG = "ResponseStatsTracker"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"NOTIFICATION_EVENT"}, value = { + NOTIFICATION_EVENT_POSTED, + NOTIFICATION_EVENT_UPDATED, + NOTIFICATION_EVENT_CANCELLED + }) + public @interface NotificationEvent {} + + private static final int NOTIFICATION_EVENT_POSTED = 0; + private static final int NOTIFICATION_EVENT_UPDATED = 1; + private static final int NOTIFICATION_EVENT_CANCELLED = 2; + + private final Object mLock = new Object(); + + /** + * Contains the mapping of user -> UserBroadcastEvents data. + */ + @GuardedBy("mLock") + private SparseArray<UserBroadcastEvents> mUserBroadcastEvents = new SparseArray<>(); + + /** + * Contains the mapping of sourceUid -> {targetUser -> UserBroadcastResponseStats} data. + * Here sourceUid refers to the uid that sent a broadcast and targetUser is the user that the + * broadcast was directed to. + */ + @GuardedBy("mLock") + private SparseArray<SparseArray<UserBroadcastResponseStats>> mUserResponseStats = + new SparseArray<>(); + + // TODO (206518114): Move all callbacks handling to a handler thread. + void reportBroadcastDispatchEvent(int sourceUid, @NonNull String targetPackage, + UserHandle targetUser, long idForResponseEvent, + @ElapsedRealtimeLong long timestampMs) { + synchronized (mLock) { + final LongSparseArray<BroadcastEvent> broadcastEvents = + getOrCreateBroadcastEventsLocked(targetPackage, targetUser); + final BroadcastEvent broadcastEvent = new BroadcastEvent( + sourceUid, targetPackage, targetUser.getIdentifier(), idForResponseEvent); + broadcastEvents.append(timestampMs, broadcastEvent); + final BroadcastResponseStats responseStats = + getOrCreateBroadcastResponseStats(broadcastEvent); + responseStats.incrementBroadcastsDispatchedCount(1); + } + } + + void reportNotificationPosted(@NonNull String packageName, UserHandle user, + @ElapsedRealtimeLong long timestampMs) { + reportNotificationEvent(NOTIFICATION_EVENT_POSTED, packageName, user, timestampMs); + } + + void reportNotificationUpdated(@NonNull String packageName, UserHandle user, + @ElapsedRealtimeLong long timestampMs) { + reportNotificationEvent(NOTIFICATION_EVENT_UPDATED, packageName, user, timestampMs); + + } + + void reportNotificationCancelled(@NonNull String packageName, UserHandle user, + @ElapsedRealtimeLong long timestampMs) { + reportNotificationEvent(NOTIFICATION_EVENT_CANCELLED, packageName, user, timestampMs); + } + + private void reportNotificationEvent(@NotificationEvent int event, + @NonNull String packageName, UserHandle user, @ElapsedRealtimeLong long timestampMs) { + // TODO (206518114): Store last N events to dump for debugging purposes. + synchronized (mLock) { + final LongSparseArray<BroadcastEvent> broadcastEvents = + getBroadcastEventsLocked(packageName, user); + if (broadcastEvents == null) { + return; + } + // TODO (206518114): Add LongSparseArray.removeAtRange() + for (int i = broadcastEvents.size() - 1; i >= 0; --i) { + final long dispatchTimestampMs = broadcastEvents.keyAt(i); + final long elapsedDurationMs = timestampMs - dispatchTimestampMs; + if (elapsedDurationMs <= 0) { + continue; + } + if (dispatchTimestampMs >= timestampMs) { + continue; + } + // TODO (206518114): Make the constant configurable. + if (elapsedDurationMs <= 2 * 60 * 1000) { + final BroadcastEvent broadcastEvent = broadcastEvents.valueAt(i); + final BroadcastResponseStats responseStats = + getBroadcastResponseStats(broadcastEvent); + if (responseStats == null) { + continue; + } + switch (event) { + case NOTIFICATION_EVENT_POSTED: + responseStats.incrementNotificationsPostedCount(1); + break; + case NOTIFICATION_EVENT_UPDATED: + responseStats.incrementNotificationsUpdatedCount(1); + break; + case NOTIFICATION_EVENT_CANCELLED: + responseStats.incrementNotificationsCancelledCount(1); + break; + default: + Slog.wtf(TAG, "Unknown event: " + event); + } + } + broadcastEvents.removeAt(i); + } + } + } + + @NonNull BroadcastResponseStats queryBroadcastResponseStats(int callingUid, + @NonNull String packageName, long id, @UserIdInt int userId) { + final BroadcastResponseStats aggregatedResponseStats = + new BroadcastResponseStats(packageName); + synchronized (mLock) { + final SparseArray<UserBroadcastResponseStats> responseStatsForCaller = + mUserResponseStats.get(callingUid); + if (responseStatsForCaller == null) { + return aggregatedResponseStats; + } + final UserBroadcastResponseStats responseStatsForUser = + responseStatsForCaller.get(userId); + if (responseStatsForUser == null) { + return aggregatedResponseStats; + } + responseStatsForUser.aggregateBroadcastResponseStats(aggregatedResponseStats, + packageName, id); + } + return aggregatedResponseStats; + } + + void clearBroadcastResponseStats(int callingUid, @NonNull String packageName, long id, + @UserIdInt int userId) { + synchronized (mLock) { + final SparseArray<UserBroadcastResponseStats> responseStatsForCaller = + mUserResponseStats.get(callingUid); + if (responseStatsForCaller == null) { + return; + } + final UserBroadcastResponseStats responseStatsForUser = + responseStatsForCaller.get(userId); + if (responseStatsForUser == null) { + return; + } + responseStatsForUser.clearBroadcastResponseStats(packageName, id); + } + } + + void onUserRemoved(@UserIdInt int userId) { + synchronized (mLock) { + mUserBroadcastEvents.remove(userId); + + for (int i = mUserResponseStats.size() - 1; i >= 0; --i) { + mUserResponseStats.valueAt(i).remove(userId); + } + } + } + + void onPackageRemoved(@NonNull String packageName, @UserIdInt int userId) { + synchronized (mLock) { + final UserBroadcastEvents userBroadcastEvents = mUserBroadcastEvents.get(userId); + if (userBroadcastEvents != null) { + userBroadcastEvents.onPackageRemoved(packageName); + } + + for (int i = mUserResponseStats.size() - 1; i >= 0; --i) { + final UserBroadcastResponseStats userResponseStats = + mUserResponseStats.valueAt(i).get(userId); + if (userResponseStats != null) { + userResponseStats.onPackageRemoved(packageName); + } + } + } + } + + void onUidRemoved(int uid) { + synchronized (mLock) { + for (int i = mUserBroadcastEvents.size() - 1; i >= 0; --i) { + mUserBroadcastEvents.valueAt(i).onUidRemoved(uid); + } + + mUserResponseStats.remove(uid); + } + } + + @GuardedBy("mLock") + @Nullable + private LongSparseArray<BroadcastEvent> getBroadcastEventsLocked( + @NonNull String packageName, UserHandle user) { + final UserBroadcastEvents userBroadcastEvents = mUserBroadcastEvents.get( + user.getIdentifier()); + if (userBroadcastEvents == null) { + return null; + } + return userBroadcastEvents.getBroadcastEvents(packageName); + } + + @GuardedBy("mLock") + @NonNull + private LongSparseArray<BroadcastEvent> getOrCreateBroadcastEventsLocked( + @NonNull String packageName, UserHandle user) { + UserBroadcastEvents userBroadcastEvents = mUserBroadcastEvents.get(user.getIdentifier()); + if (userBroadcastEvents == null) { + userBroadcastEvents = new UserBroadcastEvents(); + mUserBroadcastEvents.put(user.getIdentifier(), userBroadcastEvents); + } + return userBroadcastEvents.getOrCreateBroadcastEvents(packageName); + } + + @GuardedBy("mLock") + @Nullable + private BroadcastResponseStats getBroadcastResponseStats( + @NonNull BroadcastEvent broadcastEvent) { + final int sourceUid = broadcastEvent.getSourceUid(); + final SparseArray<UserBroadcastResponseStats> responseStatsForUid = + mUserResponseStats.get(sourceUid); + return getBroadcastResponseStats(responseStatsForUid, broadcastEvent); + } + + @GuardedBy("mLock") + @Nullable + private BroadcastResponseStats getBroadcastResponseStats( + @Nullable SparseArray<UserBroadcastResponseStats> responseStatsForUid, + @NonNull BroadcastEvent broadcastEvent) { + if (responseStatsForUid == null) { + return null; + } + final UserBroadcastResponseStats userResponseStats = responseStatsForUid.get( + broadcastEvent.getTargetUserId()); + if (userResponseStats == null) { + return null; + } + return userResponseStats.getBroadcastResponseStats(broadcastEvent); + } + + @GuardedBy("mLock") + @NonNull + private BroadcastResponseStats getOrCreateBroadcastResponseStats( + @NonNull BroadcastEvent broadcastEvent) { + final int sourceUid = broadcastEvent.getSourceUid(); + SparseArray<UserBroadcastResponseStats> userResponseStatsForUid = + mUserResponseStats.get(sourceUid); + if (userResponseStatsForUid == null) { + userResponseStatsForUid = new SparseArray<>(); + mUserResponseStats.put(sourceUid, userResponseStatsForUid); + } + UserBroadcastResponseStats userResponseStats = userResponseStatsForUid.get( + broadcastEvent.getTargetUserId()); + if (userResponseStats == null) { + userResponseStats = new UserBroadcastResponseStats(); + userResponseStatsForUid.put(broadcastEvent.getTargetUserId(), userResponseStats); + } + return userResponseStats.getOrCreateBroadcastResponseStats(broadcastEvent); + } + + void dump(@NonNull IndentingPrintWriter ipw) { + ipw.println("Broadcast response stats:"); + ipw.increaseIndent(); + + synchronized (mLock) { + dumpBroadcastEventsLocked(ipw); + ipw.println(); + dumpResponseStatsLocked(ipw); + } + + ipw.decreaseIndent(); + } + + @GuardedBy("mLock") + private void dumpBroadcastEventsLocked(@NonNull IndentingPrintWriter ipw) { + ipw.println("Broadcast events:"); + ipw.increaseIndent(); + for (int i = 0; i < mUserBroadcastEvents.size(); ++i) { + final int userId = mUserBroadcastEvents.keyAt(i); + final UserBroadcastEvents userBroadcastEvents = mUserBroadcastEvents.valueAt(i); + ipw.println("User " + userId + ":"); + ipw.increaseIndent(); + userBroadcastEvents.dump(ipw); + ipw.decreaseIndent(); + } + ipw.decreaseIndent(); + } + + @GuardedBy("mLock") + private void dumpResponseStatsLocked(@NonNull IndentingPrintWriter ipw) { + ipw.println("Response stats:"); + ipw.increaseIndent(); + for (int i = 0; i < mUserResponseStats.size(); ++i) { + final int sourceUid = mUserResponseStats.keyAt(i); + final SparseArray<UserBroadcastResponseStats> userBroadcastResponseStats = + mUserResponseStats.valueAt(i); + ipw.println("Uid " + sourceUid + ":"); + ipw.increaseIndent(); + for (int j = 0; j < userBroadcastResponseStats.size(); ++j) { + final int userId = userBroadcastResponseStats.keyAt(j); + final UserBroadcastResponseStats broadcastResponseStats = + userBroadcastResponseStats.valueAt(j); + ipw.println("User " + userId + ":"); + ipw.increaseIndent(); + broadcastResponseStats.dump(ipw); + ipw.decreaseIndent(); + } + ipw.decreaseIndent(); + } + ipw.decreaseIndent(); + } +} + diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 559eb388fe57..e28839efa4bc 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -29,13 +29,17 @@ import static android.app.usage.UsageEvents.Event.USER_STOPPED; import static android.app.usage.UsageEvents.Event.USER_UNLOCKED; import static android.app.usage.UsageStatsManager.USAGE_SOURCE_CURRENT_ACTIVITY; import static android.app.usage.UsageStatsManager.USAGE_SOURCE_TASK_ROOT_ACTIVITY; +import static android.content.Intent.ACTION_UID_REMOVED; +import static android.content.Intent.EXTRA_UID; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import android.Manifest; import android.annotation.CurrentTimeMillisLong; import android.annotation.ElapsedRealtimeLong; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; @@ -44,6 +48,7 @@ import android.app.PendingIntent; import android.app.admin.DevicePolicyManagerInternal; import android.app.usage.AppLaunchEstimateInfo; import android.app.usage.AppStandbyInfo; +import android.app.usage.BroadcastResponseStats; import android.app.usage.ConfigurationStats; import android.app.usage.EventStats; import android.app.usage.IUsageStatsManager; @@ -217,6 +222,8 @@ public class UsageStatsService extends SystemService implements private final CopyOnWriteArraySet<UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener> mEstimatedLaunchTimeChangedListeners = new CopyOnWriteArraySet<>(); + private BroadcastResponseStatsTracker mResponseStatsTracker; + private static class ActivityData { private final String mTaskRootPackage; private final String mTaskRootClass; @@ -263,6 +270,7 @@ public class UsageStatsService extends SystemService implements } @Override + @SuppressLint("AndroidFrameworkRequiresPermission") public void onStart() { mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE); @@ -271,6 +279,7 @@ public class UsageStatsService extends SystemService implements mHandler = new H(BackgroundThread.get().getLooper()); mAppStandby = mInjector.getAppStandbyController(getContext()); + mResponseStatsTracker = new BroadcastResponseStatsTracker(); mAppTimeLimit = new AppTimeLimitController(getContext(), new AppTimeLimitController.TimeLimitCallbackListener() { @@ -315,6 +324,9 @@ public class UsageStatsService extends SystemService implements getContext().registerReceiverAsUser(new UserActionsReceiver(), UserHandle.ALL, filter, null, mHandler); + getContext().registerReceiverAsUser(new UidRemovedReceiver(), UserHandle.ALL, + new IntentFilter(ACTION_UID_REMOVED), null, mHandler); + mRealTimeSnapshot = SystemClock.elapsedRealtime(); mSystemTimeSnapshot = System.currentTimeMillis(); @@ -536,6 +548,7 @@ public class UsageStatsService extends SystemService implements if (Intent.ACTION_USER_REMOVED.equals(action)) { if (userId >= 0) { mHandler.obtainMessage(MSG_REMOVE_USER, userId, 0).sendToTarget(); + mResponseStatsTracker.onUserRemoved(userId); } } else if (Intent.ACTION_USER_STARTED.equals(action)) { if (userId >= 0) { @@ -545,6 +558,20 @@ public class UsageStatsService extends SystemService implements } } + private class UidRemovedReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final int uid = intent.getIntExtra(EXTRA_UID, -1); + if (uid == -1) { + return; + } + + synchronized (mLock) { + mResponseStatsTracker.onUidRemoved(uid); + } + } + } + private final IUidObserver mUidObserver = new IUidObserver.Stub() { @Override public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) { @@ -1780,6 +1807,10 @@ public class UsageStatsService extends SystemService implements } return; } + } else if ("broadcast-response-stats".equals(arg)) { + synchronized (mLock) { + mResponseStatsTracker.dump(idpw); + } } else if (arg != null && !arg.startsWith("-")) { // Anything else that doesn't start with '-' is a pkg to filter pkgs.add(arg); @@ -1813,6 +1844,9 @@ public class UsageStatsService extends SystemService implements idpw.println(); mAppTimeLimit.dump(null, pw); + + idpw.println(); + mResponseStatsTracker.dump(idpw); } mAppStandby.dumpUsers(idpw, userIds, pkgs); @@ -2645,6 +2679,60 @@ public class UsageStatsService extends SystemService implements / TimeUnit.DAYS.toMillis(1) * TimeUnit.DAYS.toMillis(1); } } + + @Override + @NonNull + public BroadcastResponseStats queryBroadcastResponseStats( + @NonNull String packageName, + @IntRange(from = 1) long id, + @NonNull String callingPackage, + @UserIdInt int userId) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(callingPackage); + // TODO: Move to Preconditions utility class + if (id <= 0) { + throw new IllegalArgumentException("id needs to be >0"); + } + + final int callingUid = Binder.getCallingUid(); + if (!hasPermission(callingPackage)) { + throw new SecurityException( + "Caller does not have the permission needed to call this API; " + + "callingPackage=" + callingPackage + + ", callingUid=" + callingUid); + } + userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), callingUid, + userId, false /* allowAll */, false /* requireFull */, + "queryBroadcastResponseStats" /* name */, callingPackage); + return mResponseStatsTracker.queryBroadcastResponseStats( + callingUid, packageName, id, userId); + } + + @Override + public void clearBroadcastResponseStats( + @NonNull String packageName, + @IntRange(from = 1) long id, + @NonNull String callingPackage, + @UserIdInt int userId) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(callingPackage); + if (id <= 0) { + throw new IllegalArgumentException("id needs to be >0"); + } + + final int callingUid = Binder.getCallingUid(); + if (!hasPermission(callingPackage)) { + throw new SecurityException( + "Caller does not have the permission needed to call this API; " + + "callingPackage=" + callingPackage + + ", callingUid=" + callingUid); + } + userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), callingUid, + userId, false /* allowAll */, false /* requireFull */, + "clearBroadcastResponseStats" /* name */, callingPackage); + mResponseStatsTracker.clearBroadcastResponseStats(callingUid, + packageName, id, userId); + } } void registerAppUsageObserver(int callingUid, int observerId, String[] packages, @@ -2953,21 +3041,26 @@ public class UsageStatsService extends SystemService implements public void reportBroadcastDispatched(int sourceUid, @NonNull String targetPackage, @NonNull UserHandle targetUser, long idForResponseEvent, @ElapsedRealtimeLong long timestampMs) { + mResponseStatsTracker.reportBroadcastDispatchEvent(sourceUid, targetPackage, + targetUser, idForResponseEvent, timestampMs); } @Override public void reportNotificationPosted(@NonNull String packageName, @NonNull UserHandle user, @ElapsedRealtimeLong long timestampMs) { + mResponseStatsTracker.reportNotificationPosted(packageName, user, timestampMs); } @Override public void reportNotificationUpdated(@NonNull String packageName, @NonNull UserHandle user, @ElapsedRealtimeLong long timestampMs) { + mResponseStatsTracker.reportNotificationUpdated(packageName, user, timestampMs); } @Override public void reportNotificationRemoved(@NonNull String packageName, @NonNull UserHandle user, @ElapsedRealtimeLong long timestampMs) { + mResponseStatsTracker.reportNotificationCancelled(packageName, user, timestampMs); } } @@ -2980,6 +3073,7 @@ public class UsageStatsService extends SystemService implements mHandler.obtainMessage(MSG_PACKAGE_REMOVED, changingUserId, 0, packageName) .sendToTarget(); } + mResponseStatsTracker.onPackageRemoved(packageName, UserHandle.getUserId(uid)); super.onPackageRemoved(packageName, uid); } } diff --git a/services/usage/java/com/android/server/usage/UserBroadcastEvents.java b/services/usage/java/com/android/server/usage/UserBroadcastEvents.java new file mode 100644 index 000000000000..81964484696a --- /dev/null +++ b/services/usage/java/com/android/server/usage/UserBroadcastEvents.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 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.usage; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.util.LongSparseArray; +import android.util.TimeUtils; + +import com.android.internal.util.IndentingPrintWriter; + +class UserBroadcastEvents { + /** + * Contains the mapping of targetPackage -> {BroadcastEvent} data. + * Here targetPackage refers to the package receiving the broadcast and BroadcastEvent objects + * corresponding to each broadcast it is receiving. + */ + private ArrayMap<String, LongSparseArray<BroadcastEvent>> mBroadcastEvents = new ArrayMap(); + + @Nullable LongSparseArray<BroadcastEvent> getBroadcastEvents(@NonNull String packageName) { + return mBroadcastEvents.get(packageName); + } + + @NonNull LongSparseArray<BroadcastEvent> getOrCreateBroadcastEvents( + @NonNull String packageName) { + LongSparseArray<BroadcastEvent> broadcastEvents = mBroadcastEvents.get(packageName); + if (broadcastEvents == null) { + broadcastEvents = new LongSparseArray<>(); + mBroadcastEvents.put(packageName, broadcastEvents); + } + return broadcastEvents; + } + + void onPackageRemoved(@NonNull String packageName) { + mBroadcastEvents.remove(packageName); + } + + void onUidRemoved(int uid) { + for (int i = mBroadcastEvents.size() - 1; i >= 0; --i) { + final LongSparseArray<BroadcastEvent> broadcastEvents = mBroadcastEvents.valueAt(i); + for (int j = broadcastEvents.size() - 1; j >= 0; --j) { + if (broadcastEvents.valueAt(j).getSourceUid() == uid) { + broadcastEvents.removeAt(j); + } + } + } + } + + void dump(@NonNull IndentingPrintWriter ipw) { + for (int i = 0; i < mBroadcastEvents.size(); ++i) { + final String packageName = mBroadcastEvents.keyAt(i); + final LongSparseArray<BroadcastEvent> broadcastEvents = mBroadcastEvents.valueAt(i); + ipw.println(packageName + ":"); + ipw.increaseIndent(); + if (broadcastEvents.size() == 0) { + ipw.println("<empty>"); + } else { + for (int j = 0; j < broadcastEvents.size(); ++j) { + final long timestampMs = broadcastEvents.keyAt(j); + final BroadcastEvent broadcastEvent = broadcastEvents.valueAt(j); + TimeUtils.formatDuration(timestampMs, ipw); + ipw.print(": "); + ipw.println(broadcastEvent); + } + } + ipw.decreaseIndent(); + } + } +} diff --git a/services/usage/java/com/android/server/usage/UserBroadcastResponseStats.java b/services/usage/java/com/android/server/usage/UserBroadcastResponseStats.java new file mode 100644 index 000000000000..ac2a320e4995 --- /dev/null +++ b/services/usage/java/com/android/server/usage/UserBroadcastResponseStats.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 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.usage; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.usage.BroadcastResponseStats; +import android.util.ArrayMap; + +import com.android.internal.util.IndentingPrintWriter; + +class UserBroadcastResponseStats { + /** + * Contains the mapping of a BroadcastEvent type to it's aggregated stats. + */ + private ArrayMap<BroadcastEvent, BroadcastResponseStats> mResponseStats = + new ArrayMap<>(); + + @Nullable BroadcastResponseStats getBroadcastResponseStats( + BroadcastEvent broadcastEvent) { + return mResponseStats.get(broadcastEvent); + } + + @NonNull BroadcastResponseStats getOrCreateBroadcastResponseStats( + BroadcastEvent broadcastEvent) { + BroadcastResponseStats responseStats = mResponseStats.get(broadcastEvent); + if (responseStats == null) { + responseStats = new BroadcastResponseStats(broadcastEvent.getTargetPackage()); + mResponseStats.put(broadcastEvent, responseStats); + } + return responseStats; + } + + void aggregateBroadcastResponseStats( + @NonNull BroadcastResponseStats responseStats, + @NonNull String packageName, long id) { + for (int i = mResponseStats.size() - 1; i >= 0; --i) { + final BroadcastEvent broadcastEvent = mResponseStats.keyAt(i); + if (broadcastEvent.getIdForResponseEvent() == id + && broadcastEvent.getTargetPackage().equals(packageName)) { + responseStats.addCounts(mResponseStats.valueAt(i)); + } + } + } + + void clearBroadcastResponseStats(@NonNull String packageName, long id) { + for (int i = mResponseStats.size() - 1; i >= 0; --i) { + final BroadcastEvent broadcastEvent = mResponseStats.keyAt(i); + if (broadcastEvent.getIdForResponseEvent() == id + && broadcastEvent.getTargetPackage().equals(packageName)) { + mResponseStats.removeAt(i); + } + } + } + + void onPackageRemoved(@NonNull String packageName) { + for (int i = mResponseStats.size() - 1; i >= 0; --i) { + if (mResponseStats.keyAt(i).getTargetPackage().equals(packageName)) { + mResponseStats.removeAt(i); + } + } + } + + void dump(@NonNull IndentingPrintWriter ipw) { + for (int i = 0; i < mResponseStats.size(); ++i) { + final BroadcastEvent broadcastEvent = mResponseStats.keyAt(i); + final BroadcastResponseStats responseStats = mResponseStats.valueAt(i); + ipw.print(broadcastEvent); + ipw.print(" -> "); + ipw.println(responseStats); + } + } +} |