blob: e2018511426153cfb20ad490e60c1a1dd8138516 [file] [log] [blame]
/**
* Copyright (C) 2018 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.UserIdInt;
import android.app.PendingIntent;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Monitors and informs of any app time limits exceeded. It must be informed when an app
* enters the foreground and exits. Used by UsageStatsService. Manages multiple users.
*
* Test: atest FrameworksServicesTests:AppTimeLimitControllerTests
* Test: manual: frameworks/base/tests/UsageStatsTest
*/
public class AppTimeLimitController {
private static final String TAG = "AppTimeLimitController";
private static final boolean DEBUG = false;
/** Lock class for this object */
private static class Lock {}
/** Lock object for the data in this class. */
private final Lock mLock = new Lock();
private final MyHandler mHandler;
private OnLimitReachedListener mListener;
@GuardedBy("mLock")
private final SparseArray<UserData> mUsers = new SparseArray<>();
private static class UserData {
/** userId of the user */
private @UserIdInt int userId;
/** The app that is currently in the foreground */
private String currentForegroundedPackage;
/** The time when the current app came to the foreground */
private long currentForegroundedTime;
/** Map from package name for quick lookup */
private ArrayMap<String, ArrayList<TimeLimitGroup>> packageMap = new ArrayMap<>();
/** Map of observerId to details of the time limit group */
private SparseArray<TimeLimitGroup> groups = new SparseArray<>();
private UserData(@UserIdInt int userId) {
this.userId = userId;
}
}
/**
* Listener interface for being informed when an app group's time limit is reached.
*/
public interface OnLimitReachedListener {
/**
* Time limit for a group, keyed by the observerId, has been reached.
* @param observerId The observerId of the group whose limit was reached
* @param userId The userId
* @param timeLimit The original time limit in milliseconds
* @param timeElapsed How much time was actually spent on apps in the group, in milliseconds
* @param callbackIntent The PendingIntent to send when the limit is reached
*/
public void onLimitReached(int observerId, @UserIdInt int userId, long timeLimit,
long timeElapsed, PendingIntent callbackIntent);
}
static class TimeLimitGroup {
int requestingUid;
int observerId;
String[] packages;
long timeLimit;
long timeRequested;
long timeRemaining;
PendingIntent callbackIntent;
String currentPackage;
long timeCurrentPackageStarted;
int userId;
}
private class MyHandler extends Handler {
static final int MSG_CHECK_TIMEOUT = 1;
static final int MSG_INFORM_LISTENER = 2;
MyHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_CHECK_TIMEOUT:
checkTimeout((TimeLimitGroup) msg.obj);
break;
case MSG_INFORM_LISTENER:
informListener((TimeLimitGroup) msg.obj);
break;
default:
super.handleMessage(msg);
break;
}
}
}
public AppTimeLimitController(OnLimitReachedListener listener, Looper looper) {
mHandler = new MyHandler(looper);
mListener = listener;
}
/** Overrideable by a test */
@VisibleForTesting
protected long getUptimeMillis() {
return SystemClock.uptimeMillis();
}
/** Returns an existing UserData object for the given userId, or creates one */
private UserData getOrCreateUserDataLocked(int userId) {
UserData userData = mUsers.get(userId);
if (userData == null) {
userData = new UserData(userId);
mUsers.put(userId, userData);
}
return userData;
}
/** Clean up data if user is removed */
public void onUserRemoved(int userId) {
synchronized (mLock) {
// TODO: Remove any inflight delayed messages
mUsers.remove(userId);
}
}
/**
* Registers an observer with the given details. Existing observer with the same observerId
* is removed.
*/
public void addObserver(int requestingUid, int observerId, String[] packages, long timeLimit,
PendingIntent callbackIntent, @UserIdInt int userId) {
synchronized (mLock) {
UserData user = getOrCreateUserDataLocked(userId);
removeObserverLocked(user, requestingUid, observerId);
TimeLimitGroup group = new TimeLimitGroup();
group.observerId = observerId;
group.callbackIntent = callbackIntent;
group.packages = packages;
group.timeLimit = timeLimit;
group.timeRemaining = group.timeLimit;
group.timeRequested = getUptimeMillis();
group.requestingUid = requestingUid;
group.timeCurrentPackageStarted = -1L;
group.userId = userId;
user.groups.append(observerId, group);
addGroupToPackageMapLocked(user, packages, group);
if (DEBUG) {
Slog.d(TAG, "addObserver " + packages + " for " + timeLimit);
}
// Handle the case where a target package is already in the foreground when observer
// is added.
if (user.currentForegroundedPackage != null && inPackageList(group.packages,
user.currentForegroundedPackage)) {
group.timeCurrentPackageStarted = group.timeRequested;
group.currentPackage = user.currentForegroundedPackage;
if (group.timeRemaining > 0) {
postCheckTimeoutLocked(group, group.timeRemaining);
}
}
}
}
/**
* Remove a registered observer by observerId and calling uid.
* @param requestingUid The calling uid
* @param observerId The unique observer id for this user
* @param userId The user id of the observer
*/
public void removeObserver(int requestingUid, int observerId, @UserIdInt int userId) {
synchronized (mLock) {
UserData user = getOrCreateUserDataLocked(userId);
removeObserverLocked(user, requestingUid, observerId);
}
}
@VisibleForTesting
TimeLimitGroup getObserverGroup(int observerId, int userId) {
synchronized (mLock) {
return getOrCreateUserDataLocked(userId).groups.get(observerId);
}
}
private static boolean inPackageList(String[] packages, String packageName) {
return ArrayUtils.contains(packages, packageName);
}
@GuardedBy("mLock")
private void removeObserverLocked(UserData user, int requestingUid, int observerId) {
TimeLimitGroup group = user.groups.get(observerId);
if (group != null && group.requestingUid == requestingUid) {
removeGroupFromPackageMapLocked(user, group);
user.groups.remove(observerId);
mHandler.removeMessages(MyHandler.MSG_CHECK_TIMEOUT, group);
}
}
/**
* Called when an app has moved to the foreground.
* @param packageName The app that is foregrounded
* @param className The className of the activity
* @param userId The user
*/
public void moveToForeground(String packageName, String className, int userId) {
synchronized (mLock) {
UserData user = getOrCreateUserDataLocked(userId);
if (DEBUG) Slog.d(TAG, "Setting mCurrentForegroundedPackage to " + packageName);
// Note the current foreground package
user.currentForegroundedPackage = packageName;
user.currentForegroundedTime = getUptimeMillis();
// Check if any of the groups need to watch for this package
maybeWatchForPackageLocked(user, packageName, user.currentForegroundedTime);
}
}
/**
* Called when an app is sent to the background.
*
* @param packageName
* @param className
* @param userId
*/
public void moveToBackground(String packageName, String className, int userId) {
synchronized (mLock) {
UserData user = getOrCreateUserDataLocked(userId);
if (!TextUtils.equals(user.currentForegroundedPackage, packageName)) {
Slog.w(TAG, "Eh? Last foregrounded package = " + user.currentForegroundedPackage
+ " and now backgrounded = " + packageName);
return;
}
final long stopTime = getUptimeMillis();
// Add up the usage time to all groups that contain the package
ArrayList<TimeLimitGroup> groups = user.packageMap.get(packageName);
if (groups != null) {
final int size = groups.size();
for (int i = 0; i < size; i++) {
final TimeLimitGroup group = groups.get(i);
// Don't continue to send
if (group.timeRemaining <= 0) continue;
final long startTime = Math.max(user.currentForegroundedTime,
group.timeRequested);
long diff = stopTime - startTime;
group.timeRemaining -= diff;
if (group.timeRemaining <= 0) {
if (DEBUG) Slog.d(TAG, "MTB informing group obs=" + group.observerId);
postInformListenerLocked(group);
}
// Reset indicators that observer was added when package was already fg
group.currentPackage = null;
group.timeCurrentPackageStarted = -1L;
mHandler.removeMessages(MyHandler.MSG_CHECK_TIMEOUT, group);
}
}
user.currentForegroundedPackage = null;
}
}
private void postInformListenerLocked(TimeLimitGroup group) {
mHandler.sendMessage(mHandler.obtainMessage(MyHandler.MSG_INFORM_LISTENER,
group));
}
/**
* Inform the observer and unregister it, as the limit has been reached.
* @param group the observed group
*/
private void informListener(TimeLimitGroup group) {
if (mListener != null) {
mListener.onLimitReached(group.observerId, group.userId, group.timeLimit,
group.timeLimit - group.timeRemaining, group.callbackIntent);
}
// Unregister since the limit has been met and observer was informed.
synchronized (mLock) {
UserData user = getOrCreateUserDataLocked(group.userId);
removeObserverLocked(user, group.requestingUid, group.observerId);
}
}
/** Check if any of the groups care about this package and set up delayed messages */
@GuardedBy("mLock")
private void maybeWatchForPackageLocked(UserData user, String packageName, long uptimeMillis) {
ArrayList<TimeLimitGroup> groups = user.packageMap.get(packageName);
if (groups == null) return;
final int size = groups.size();
for (int i = 0; i < size; i++) {
TimeLimitGroup group = groups.get(i);
if (group.timeRemaining > 0) {
group.timeCurrentPackageStarted = uptimeMillis;
group.currentPackage = packageName;
if (DEBUG) {
Slog.d(TAG, "Posting timeout for " + packageName + " for "
+ group.timeRemaining + "ms");
}
postCheckTimeoutLocked(group, group.timeRemaining);
}
}
}
private void addGroupToPackageMapLocked(UserData user, String[] packages,
TimeLimitGroup group) {
for (int i = 0; i < packages.length; i++) {
ArrayList<TimeLimitGroup> list = user.packageMap.get(packages[i]);
if (list == null) {
list = new ArrayList<>();
user.packageMap.put(packages[i], list);
}
list.add(group);
}
}
/**
* Remove the group reference from the package to group mapping, which is 1 to many.
* @param group The group to remove from the package map.
*/
private void removeGroupFromPackageMapLocked(UserData user, TimeLimitGroup group) {
final int mapSize = user.packageMap.size();
for (int i = 0; i < mapSize; i++) {
ArrayList<TimeLimitGroup> list = user.packageMap.valueAt(i);
list.remove(group);
}
}
private void postCheckTimeoutLocked(TimeLimitGroup group, long timeout) {
mHandler.sendMessageDelayed(mHandler.obtainMessage(MyHandler.MSG_CHECK_TIMEOUT, group),
timeout);
}
/**
* See if the given group has reached the timeout if the current foreground app is included
* and it exceeds the time remaining.
* @param group the group of packages to check
*/
void checkTimeout(TimeLimitGroup group) {
// For each package in the group, check if any of the currently foregrounded apps are adding
// up to hit the limit and inform the observer
synchronized (mLock) {
UserData user = getOrCreateUserDataLocked(group.userId);
// This group doesn't exist anymore, nothing to see here.
if (user.groups.get(group.observerId) != group) return;
if (DEBUG) Slog.d(TAG, "checkTimeout timeRemaining=" + group.timeRemaining);
// Already reached the limit, no need to report again
if (group.timeRemaining <= 0) return;
if (DEBUG) {
Slog.d(TAG, "checkTimeout foregroundedPackage="
+ user.currentForegroundedPackage);
}
if (inPackageList(group.packages, user.currentForegroundedPackage)) {
if (DEBUG) {
Slog.d(TAG, "checkTimeout package in foreground="
+ user.currentForegroundedPackage);
}
if (group.timeCurrentPackageStarted < 0) {
Slog.w(TAG, "startTime was not set correctly for " + group);
}
final long timeInForeground = getUptimeMillis() - group.timeCurrentPackageStarted;
if (group.timeRemaining <= timeInForeground) {
if (DEBUG) Slog.d(TAG, "checkTimeout : Time limit reached");
// Hit the limit, set timeRemaining to zero to avoid checking again
group.timeRemaining -= timeInForeground;
postInformListenerLocked(group);
// Reset
group.timeCurrentPackageStarted = -1L;
group.currentPackage = null;
} else {
if (DEBUG) Slog.d(TAG, "checkTimeout : Some more time remaining");
postCheckTimeoutLocked(group, group.timeRemaining - timeInForeground);
}
}
}
}
void dump(PrintWriter pw) {
synchronized (mLock) {
pw.println("\n App Time Limits");
int nUsers = mUsers.size();
for (int i = 0; i < nUsers; i++) {
UserData user = mUsers.valueAt(i);
pw.print(" User "); pw.println(user.userId);
int nGroups = user.groups.size();
for (int j = 0; j < nGroups; j++) {
TimeLimitGroup group = user.groups.valueAt(j);
pw.print(" Group id="); pw.print(group.observerId);
pw.print(" timeLimit="); pw.print(group.timeLimit);
pw.print(" remaining="); pw.print(group.timeRemaining);
pw.print(" currentPackage="); pw.print(group.currentPackage);
pw.print(" timeCurrentPkgStarted="); pw.print(group.timeCurrentPackageStarted);
pw.print(" packages="); pw.println(Arrays.toString(group.packages));
}
pw.println();
pw.print(" currentForegroundedPackage=");
pw.println(user.currentForegroundedPackage);
}
}
}
}