blob: 7d5be8ea9a79fcb09ff40e3d7f90b29e05dbd2e3 [file] [log] [blame]
/*
* 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.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager.ProcessState;
import android.app.role.OnRoleHoldersChangedListener;
import android.app.role.RoleManager;
import android.app.usage.BroadcastResponseStats;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.permission.PermissionManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.LongArrayQueue;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.IndentingPrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
class BroadcastResponseStatsTracker {
static final String TAG = "ResponseStatsTracker";
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"NOTIFICATION_EVENT_TYPE_"}, value = {
NOTIFICATION_EVENT_TYPE_POSTED,
NOTIFICATION_EVENT_TYPE_UPDATED,
NOTIFICATION_EVENT_TYPE_CANCELLED
})
public @interface NotificationEventType {}
static final int NOTIFICATION_EVENT_TYPE_POSTED = 0;
static final int NOTIFICATION_EVENT_TYPE_UPDATED = 1;
static final int NOTIFICATION_EVENT_TYPE_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<>();
/**
* Cache of package names holding exempted roles.
*
* Contains the mapping of userId -> {roleName -> <packages>} data.
*/
// TODO: Use SparseArrayMap to simplify the logic.
@GuardedBy("mLock")
private SparseArray<ArrayMap<String, List<String>>> mExemptedRoleHoldersCache =
new SparseArray<>();
private final OnRoleHoldersChangedListener mRoleHoldersChangedListener =
this::onRoleHoldersChanged;
private AppStandbyInternal mAppStandby;
private BroadcastResponseStatsLogger mLogger;
private RoleManager mRoleManager;
BroadcastResponseStatsTracker(@NonNull AppStandbyInternal appStandby) {
mAppStandby = appStandby;
mLogger = new BroadcastResponseStatsLogger();
}
void onSystemServicesReady(Context context) {
mRoleManager = context.getSystemService(RoleManager.class);
mRoleManager.addOnRoleHoldersChangedListenerAsUser(BackgroundThread.getExecutor(),
mRoleHoldersChangedListener, UserHandle.ALL);
}
// TODO (206518114): Move all callbacks handling to a handler thread.
void reportBroadcastDispatchEvent(int sourceUid, @NonNull String targetPackage,
UserHandle targetUser, long idForResponseEvent,
@ElapsedRealtimeLong long timestampMs, @ProcessState int targetUidProcState) {
mLogger.logBroadcastDispatchEvent(sourceUid, targetPackage, targetUser,
idForResponseEvent, timestampMs, targetUidProcState);
if (targetUidProcState <= mAppStandby.getBroadcastResponseFgThresholdState()) {
// No need to track the broadcast response stats while the target app is
// in the foreground.
return;
}
if (doesPackageHoldExemptedRole(targetPackage, targetUser)) {
// Package holds an exempted role, so no need to track the broadcast response stats.
return;
}
if (doesPackageHoldExemptedPermission(targetPackage, targetUser)) {
// Package holds an exempted permission, so no need to track the broadcast response
// stats
return;
}
synchronized (mLock) {
final ArraySet<BroadcastEvent> broadcastEvents =
getOrCreateBroadcastEventsLocked(targetPackage, targetUser);
final BroadcastEvent broadcastEvent = getOrCreateBroadcastEvent(broadcastEvents,
sourceUid, targetPackage, targetUser.getIdentifier(), idForResponseEvent);
broadcastEvent.addTimestampMs(timestampMs);
// Delete any old broadcast event related data so that we don't keep accumulating them.
recordAndPruneOldBroadcastDispatchTimestamps(broadcastEvent);
}
}
void reportNotificationPosted(@NonNull String packageName, UserHandle user,
@ElapsedRealtimeLong long timestampMs) {
reportNotificationEvent(NOTIFICATION_EVENT_TYPE_POSTED, packageName, user, timestampMs);
}
void reportNotificationUpdated(@NonNull String packageName, UserHandle user,
@ElapsedRealtimeLong long timestampMs) {
reportNotificationEvent(NOTIFICATION_EVENT_TYPE_UPDATED, packageName, user, timestampMs);
}
void reportNotificationCancelled(@NonNull String packageName, UserHandle user,
@ElapsedRealtimeLong long timestampMs) {
reportNotificationEvent(NOTIFICATION_EVENT_TYPE_CANCELLED, packageName, user, timestampMs);
}
private void reportNotificationEvent(@NotificationEventType int event,
@NonNull String packageName, UserHandle user, @ElapsedRealtimeLong long timestampMs) {
mLogger.logNotificationEvent(event, packageName, user, timestampMs);
synchronized (mLock) {
final ArraySet<BroadcastEvent> broadcastEvents =
getBroadcastEventsLocked(packageName, user);
if (broadcastEvents == null) {
return;
}
final long broadcastResponseWindowDurationMs =
mAppStandby.getBroadcastResponseWindowDurationMs();
final long broadcastsSessionWithResponseDurationMs =
mAppStandby.getBroadcastSessionsWithResponseDurationMs();
final boolean recordAllBroadcastsSessionsWithinResponseWindow =
mAppStandby.shouldNoteResponseEventForAllBroadcastSessions();
for (int i = broadcastEvents.size() - 1; i >= 0; --i) {
final BroadcastEvent broadcastEvent = broadcastEvents.valueAt(i);
recordAndPruneOldBroadcastDispatchTimestamps(broadcastEvent);
final LongArrayQueue dispatchTimestampsMs = broadcastEvent.getTimestampsMs();
long broadcastsSessionEndTimestampMs = 0;
// We only need to look at the broadcast events that occurred before
// this notification related event.
while (dispatchTimestampsMs.size() > 0
&& dispatchTimestampsMs.peekFirst() < timestampMs) {
final long dispatchTimestampMs = dispatchTimestampsMs.peekFirst();
final long elapsedDurationMs = timestampMs - dispatchTimestampMs;
// Only increment the counts if the broadcast was sent not too long ago, as
// decided by 'broadcastResponseWindowDurationMs' and is part of a new session.
// That is, it occurred 'broadcastsSessionWithResponseDurationMs' after the
// previously handled broadcast event which is represented by
// 'broadcastsSessionEndTimestampMs'.
if (elapsedDurationMs <= broadcastResponseWindowDurationMs
&& dispatchTimestampMs >= broadcastsSessionEndTimestampMs) {
if (broadcastsSessionEndTimestampMs != 0
&& !recordAllBroadcastsSessionsWithinResponseWindow) {
break;
}
final BroadcastResponseStats responseStats =
getOrCreateBroadcastResponseStats(broadcastEvent);
responseStats.incrementBroadcastsDispatchedCount(1);
broadcastsSessionEndTimestampMs = dispatchTimestampMs
+ broadcastsSessionWithResponseDurationMs;
switch (event) {
case NOTIFICATION_EVENT_TYPE_POSTED:
responseStats.incrementNotificationsPostedCount(1);
break;
case NOTIFICATION_EVENT_TYPE_UPDATED:
responseStats.incrementNotificationsUpdatedCount(1);
break;
case NOTIFICATION_EVENT_TYPE_CANCELLED:
responseStats.incrementNotificationsCancelledCount(1);
break;
default:
Slog.wtf(TAG, "Unknown event: " + event);
}
}
dispatchTimestampsMs.removeFirst();
}
if (dispatchTimestampsMs.size() == 0) {
broadcastEvents.removeAt(i);
}
}
}
}
@GuardedBy("mLock")
private void recordAndPruneOldBroadcastDispatchTimestamps(BroadcastEvent broadcastEvent) {
final LongArrayQueue timestampsMs = broadcastEvent.getTimestampsMs();
final long broadcastResponseWindowDurationMs =
mAppStandby.getBroadcastResponseWindowDurationMs();
final long broadcastsSessionDurationMs =
mAppStandby.getBroadcastSessionsDurationMs();
final long nowElapsedMs = SystemClock.elapsedRealtime();
long broadcastsSessionEndTimestampMs = 0;
while (timestampsMs.size() > 0
&& timestampsMs.peekFirst() < (nowElapsedMs - broadcastResponseWindowDurationMs)) {
final long eventTimestampMs = timestampsMs.peekFirst();
if (eventTimestampMs >= broadcastsSessionEndTimestampMs) {
final BroadcastResponseStats responseStats =
getOrCreateBroadcastResponseStats(broadcastEvent);
responseStats.incrementBroadcastsDispatchedCount(1);
broadcastsSessionEndTimestampMs = eventTimestampMs + broadcastsSessionDurationMs;
}
timestampsMs.removeFirst();
}
}
@NonNull List<BroadcastResponseStats> queryBroadcastResponseStats(int callingUid,
@Nullable String packageName, @IntRange(from = 0) long id, @UserIdInt int userId) {
final List<BroadcastResponseStats> broadcastResponseStatsList = new ArrayList<>();
synchronized (mLock) {
final SparseArray<UserBroadcastResponseStats> responseStatsForCaller =
mUserResponseStats.get(callingUid);
if (responseStatsForCaller == null) {
return broadcastResponseStatsList;
}
final UserBroadcastResponseStats responseStatsForUser =
responseStatsForCaller.get(userId);
if (responseStatsForUser == null) {
return broadcastResponseStatsList;
}
responseStatsForUser.populateAllBroadcastResponseStats(
broadcastResponseStatsList, packageName, id);
}
return broadcastResponseStatsList;
}
void clearBroadcastResponseStats(int callingUid, @Nullable 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 clearBroadcastEvents(int callingUid, @UserIdInt int userId) {
synchronized (mLock) {
final UserBroadcastEvents userBroadcastEvents = mUserBroadcastEvents.get(userId);
if (userBroadcastEvents == null) {
return;
}
userBroadcastEvents.clear(callingUid);
}
}
boolean doesPackageHoldExemptedRole(@NonNull String packageName, @NonNull UserHandle user) {
final List<String> exemptedRoles = mAppStandby.getBroadcastResponseExemptedRoles();
synchronized (mLock) {
for (int i = exemptedRoles.size() - 1; i >= 0; --i) {
final String roleName = exemptedRoles.get(i);
final List<String> roleHolders = getRoleHoldersLocked(roleName, user);
if (CollectionUtils.contains(roleHolders, packageName)) {
return true;
}
}
}
return false;
}
boolean doesPackageHoldExemptedPermission(@NonNull String packageName,
@NonNull UserHandle user) {
final List<String> exemptedPermissions = mAppStandby
.getBroadcastResponseExemptedPermissions();
for (int i = exemptedPermissions.size() - 1; i >= 0; --i) {
final String permissionName = exemptedPermissions.get(i);
if (PermissionManager.checkPackageNamePermission(permissionName, packageName,
user.getIdentifier()) == PackageManager.PERMISSION_GRANTED) {
return true;
}
}
return false;
}
@GuardedBy("mLock")
@Nullable
private List<String> getRoleHoldersLocked(@NonNull String roleName, @NonNull UserHandle user) {
ArrayMap<String, List<String>> roleHoldersForUser = mExemptedRoleHoldersCache.get(
user.getIdentifier());
if (roleHoldersForUser == null) {
roleHoldersForUser = new ArrayMap<>();
mExemptedRoleHoldersCache.put(user.getIdentifier(), roleHoldersForUser);
}
List<String> roleHolders = roleHoldersForUser.get(roleName);
if (roleHolders == null && mRoleManager != null) {
roleHolders = mRoleManager.getRoleHoldersAsUser(roleName, user);
roleHoldersForUser.put(roleName, roleHolders);
}
return roleHolders;
}
private void onRoleHoldersChanged(@NonNull String roleName, @NonNull UserHandle user) {
synchronized (mLock) {
final ArrayMap<String, List<String>> roleHoldersForUser =
mExemptedRoleHoldersCache.get(user.getIdentifier());
if (roleHoldersForUser == null) {
return;
}
roleHoldersForUser.remove(roleName);
}
}
void onUserRemoved(@UserIdInt int userId) {
synchronized (mLock) {
mUserBroadcastEvents.remove(userId);
for (int i = mUserResponseStats.size() - 1; i >= 0; --i) {
mUserResponseStats.valueAt(i).remove(userId);
}
mExemptedRoleHoldersCache.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 ArraySet<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 ArraySet<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(
@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);
}
private static BroadcastEvent getOrCreateBroadcastEvent(
ArraySet<BroadcastEvent> broadcastEvents,
int sourceUid, String targetPackage, int targetUserId, long idForResponseEvent) {
final BroadcastEvent broadcastEvent = new BroadcastEvent(
sourceUid, targetPackage, targetUserId, idForResponseEvent);
final int index = broadcastEvents.indexOf(broadcastEvent);
if (index >= 0) {
return broadcastEvents.valueAt(index);
} else {
broadcastEvents.add(broadcastEvent);
return broadcastEvent;
}
}
void dump(@NonNull IndentingPrintWriter ipw) {
ipw.println("Broadcast response stats:");
ipw.increaseIndent();
synchronized (mLock) {
dumpBroadcastEventsLocked(ipw);
ipw.println();
dumpResponseStatsLocked(ipw);
ipw.println();
dumpRoleHoldersLocked(ipw);
ipw.println();
mLogger.dumpLogs(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();
}
@GuardedBy("mLock")
private void dumpRoleHoldersLocked(@NonNull IndentingPrintWriter ipw) {
ipw.println("Role holders:");
ipw.increaseIndent();
for (int userIdx = 0; userIdx < mExemptedRoleHoldersCache.size(); ++userIdx) {
final int userId = mExemptedRoleHoldersCache.keyAt(userIdx);
final ArrayMap<String, List<String>> roleHoldersForUser =
mExemptedRoleHoldersCache.valueAt(userIdx);
ipw.println("User " + userId + ":");
ipw.increaseIndent();
for (int roleIdx = 0; roleIdx < roleHoldersForUser.size(); ++roleIdx) {
final String roleName = roleHoldersForUser.keyAt(roleIdx);
final List<String> holders = roleHoldersForUser.valueAt(roleIdx);
ipw.print(roleName + ": ");
for (int holderIdx = 0; holderIdx < holders.size(); ++holderIdx) {
if (holderIdx > 0) {
ipw.print(", ");
}
ipw.print(holders.get(holderIdx));
}
ipw.println();
}
ipw.decreaseIndent();
}
ipw.decreaseIndent();
}
}