Add APIs for querying broadcast response stats.

Bug: 206518114
Test: atest tests/tests/app.usage/src/android/app/usage/cts/UsageStatsTest.java
CTS-Coverage-Bug: 206518114
Change-Id: Idcefc143215a372a7bd786fcb2208a7c26f2f7e2
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 9002b46..ee18478 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2161,6 +2161,18 @@
 
 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();
@@ -2216,11 +2228,13 @@
   }
 
   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 0000000..5992841
--- /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 0000000..5acc3dda
--- /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 170d766..6f8fea1 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 @@
     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 d7e197e..f9f3476 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -18,6 +18,7 @@
 
 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.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;
@@ -1383,4 +1385,65 @@
             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 0000000..ceb79c1
--- /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 0000000..aa01f31
--- /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 559eb38..e28839e 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_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.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 @@
     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 @@
     }
 
     @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 @@
         mHandler = new H(BackgroundThread.get().getLooper());
 
         mAppStandby = mInjector.getAppStandbyController(getContext());
+        mResponseStatsTracker = new BroadcastResponseStatsTracker();
 
         mAppTimeLimit = new AppTimeLimitController(getContext(),
                 new AppTimeLimitController.TimeLimitCallbackListener() {
@@ -315,6 +324,9 @@
         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 @@
             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 @@
         }
     }
 
+    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 @@
                         }
                         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 @@
             idpw.println();
 
             mAppTimeLimit.dump(null, pw);
+
+            idpw.println();
+            mResponseStatsTracker.dump(idpw);
         }
 
         mAppStandby.dumpUsers(idpw, userIds, pkgs);
@@ -2645,6 +2679,60 @@
                         / 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 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 @@
                 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 0000000..819644846
--- /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 0000000..ac2a320
--- /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);
+        }
+    }
+}