Merge "Add APIs for querying broadcast response stats."

This commit is contained in:
Sudheer Shanka
2022-01-27 09:38:37 +00:00
committed by Android (Google) Code Review
10 changed files with 985 additions and 0 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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];
}
};
}

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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
+ "}";
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}