All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.server.usage.AppTimeLimitController Maven / Gradle / Ivy

/**
 * 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.AlarmManager;
import android.app.PendingIntent;
import android.app.usage.UsageStatsManagerInternal;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.io.PrintWriter;
import java.lang.ref.WeakReference;
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 final Context mContext;

    private AlarmManager mAlarmManager;

    private TimeLimitCallbackListener mListener;

    private static final long MAX_OBSERVER_PER_UID = 1000;

    private static final long ONE_MINUTE = 60_000L;

    private static final Integer ONE = new Integer(1);

    /** Collection of data for each user that has reported usage */
    @GuardedBy("mLock")
    private final SparseArray mUsers = new SparseArray<>();

    /**
     * Collection of data for each app that is registering observers
     * WARNING: Entries are currently not removed, based on the assumption there are a small
     *          fixed number of apps on device that can register observers.
     */
    @GuardedBy("mLock")
    private final SparseArray mObserverApps = new SparseArray<>();

    private class UserData {
        /** userId of the user */
        private @UserIdInt
        int userId;

        /** Count of the currently active entities */
        public final ArrayMap currentlyActive = new ArrayMap<>();

        /** Map from entity name for quick lookup */
        public final ArrayMap> observedMap = new ArrayMap<>();

        private UserData(@UserIdInt int userId) {
            this.userId = userId;
        }

        @GuardedBy("mLock")
        boolean isActive(String[] entities) {
            // TODO: Consider using a bloom filter here if number of actives becomes large
            final int size = entities.length;
            for (int i = 0; i < size; i++) {
                if (currentlyActive.containsKey(entities[i])) {
                    return true;
                }
            }
            return false;
        }

        @GuardedBy("mLock")
        void addUsageGroup(UsageGroup group) {
            final int size = group.mObserved.length;
            for (int i = 0; i < size; i++) {
                ArrayList list = observedMap.get(group.mObserved[i]);
                if (list == null) {
                    list = new ArrayList<>();
                    observedMap.put(group.mObserved[i], list);
                }
                list.add(group);
            }
        }

        @GuardedBy("mLock")
        void removeUsageGroup(UsageGroup group) {
            final int size = group.mObserved.length;
            for (int i = 0; i < size; i++) {
                final String observed = group.mObserved[i];
                final ArrayList list = observedMap.get(observed);
                if (list != null) {
                    list.remove(group);
                    if (list.isEmpty()) {
                        // No more observers for this observed entity, remove from map
                        observedMap.remove(observed);
                    }
                }
            }
        }

        @GuardedBy("mLock")
        void dump(PrintWriter pw) {
            pw.print(" userId=");
            pw.println(userId);
            pw.print(" Currently Active:");
            final int nActive = currentlyActive.size();
            for (int i = 0; i < nActive; i++) {
                pw.print(currentlyActive.keyAt(i));
                pw.print(", ");
            }
            pw.println();
            pw.print(" Observed Entities:");
            final int nEntities = observedMap.size();
            for (int i = 0; i < nEntities; i++) {
                pw.print(observedMap.keyAt(i));
                pw.print(", ");
            }
            pw.println();
        }
    }


    private class ObserverAppData {
        /** uid of the observing app */
        private int uid;

        /** Map of observerId to details of the time limit group */
        SparseArray appUsageGroups = new SparseArray<>();

        /** Map of observerId to details of the time limit group */
        SparseArray sessionUsageGroups = new SparseArray<>();

        /** Map of observerId to details of the app usage limit group */
        SparseArray appUsageLimitGroups = new SparseArray<>();

        private ObserverAppData(int uid) {
            this.uid = uid;
        }

        @GuardedBy("mLock")
        void removeAppUsageGroup(int observerId) {
            appUsageGroups.remove(observerId);
        }

        @GuardedBy("mLock")
        void removeSessionUsageGroup(int observerId) {
            sessionUsageGroups.remove(observerId);
        }

        @GuardedBy("mLock")
        void removeAppUsageLimitGroup(int observerId) {
            appUsageLimitGroups.remove(observerId);
        }

        @GuardedBy("mLock")
        void dump(PrintWriter pw) {
            pw.print(" uid=");
            pw.println(uid);
            pw.println("    App Usage Groups:");
            final int nAppUsageGroups = appUsageGroups.size();
            for (int i = 0; i < nAppUsageGroups; i++) {
                appUsageGroups.valueAt(i).dump(pw);
                pw.println();
            }
            pw.println("    Session Usage Groups:");
            final int nSessionUsageGroups = sessionUsageGroups.size();
            for (int i = 0; i < nSessionUsageGroups; i++) {
                sessionUsageGroups.valueAt(i).dump(pw);
                pw.println();
            }
            pw.println("    App Usage Limit Groups:");
            final int nAppUsageLimitGroups = appUsageLimitGroups.size();
            for (int i = 0; i < nAppUsageLimitGroups; i++) {
                appUsageLimitGroups.valueAt(i).dump(pw);
                pw.println();
            }
        }
    }

    /**
     * Listener interface for being informed when an app group's time limit is reached.
     */
    public interface TimeLimitCallbackListener {
        /**
         * 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);

        /**
         * Session ended for a group, keyed by the observerId, after limit was reached.
         *
         * @param observerId     The observerId of the group whose limit was reached
         * @param userId         The userId
         * @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 onSessionEnd(int observerId, @UserIdInt int userId, long timeElapsed,
                PendingIntent callbackIntent);
    }

    abstract class UsageGroup {
        protected int mObserverId;
        protected String[] mObserved;
        protected long mTimeLimitMs;
        protected long mUsageTimeMs;
        protected int mActives;
        protected long mLastKnownUsageTimeMs;
        protected long mLastUsageEndTimeMs;
        protected WeakReference mUserRef;
        protected WeakReference mObserverAppRef;
        protected PendingIntent mLimitReachedCallback;

        UsageGroup(UserData user, ObserverAppData observerApp, int observerId, String[] observed,
                long timeLimitMs, PendingIntent limitReachedCallback) {
            mUserRef = new WeakReference<>(user);
            mObserverAppRef = new WeakReference<>(observerApp);
            mObserverId = observerId;
            mObserved = observed;
            mTimeLimitMs = timeLimitMs;
            mLimitReachedCallback = limitReachedCallback;
        }

        @GuardedBy("mLock")
        public long getTimeLimitMs() { return mTimeLimitMs; }

        @GuardedBy("mLock")
        public long getUsageTimeMs() { return mUsageTimeMs; }

        @GuardedBy("mLock")
        public void remove() {
            UserData user = mUserRef.get();
            if (user != null) {
                user.removeUsageGroup(this);
            }
            // Clear the callback, so any racy inflight message will do nothing
            mLimitReachedCallback = null;
        }

        @GuardedBy("mLock")
        void noteUsageStart(long startTimeMs) {
            noteUsageStart(startTimeMs, startTimeMs);
        }

        @GuardedBy("mLock")
        void noteUsageStart(long startTimeMs, long currentTimeMs) {
            if (mActives++ == 0) {
                // If last known usage ended after the start of this usage, there is overlap
                // between the last usage session and this one. Avoid double counting by only
                // counting from the end of the last session. This has a rare side effect that some
                // usage will not be accounted for if the previous session started and stopped
                // within this current usage.
                startTimeMs = mLastUsageEndTimeMs > startTimeMs ? mLastUsageEndTimeMs : startTimeMs;
                mLastKnownUsageTimeMs = startTimeMs;
                final long timeRemaining =
                        mTimeLimitMs - mUsageTimeMs - currentTimeMs + startTimeMs;
                if (timeRemaining > 0) {
                    if (DEBUG) {
                        Slog.d(TAG, "Posting timeout for " + mObserverId + " for "
                                + timeRemaining + "ms");
                    }
                    postCheckTimeoutLocked(this, timeRemaining);
                }
            } else {
                if (mActives > mObserved.length) {
                    // Try to get to a valid state and log the issue
                    mActives = mObserved.length;
                    final UserData user = mUserRef.get();
                    if (user == null) return;
                    final Object[] array = user.currentlyActive.keySet().toArray();
                    Slog.e(TAG,
                            "Too many noted usage starts! Observed entities: " + Arrays.toString(
                                    mObserved) + "   Active Entities: " + Arrays.toString(array));
                }
            }
        }

        @GuardedBy("mLock")
        void noteUsageStop(long stopTimeMs) {
            if (--mActives == 0) {
                final boolean limitNotCrossed = mUsageTimeMs < mTimeLimitMs;
                mUsageTimeMs += stopTimeMs - mLastKnownUsageTimeMs;

                mLastUsageEndTimeMs = stopTimeMs;
                if (limitNotCrossed && mUsageTimeMs >= mTimeLimitMs) {
                    // Crossed the limit
                    if (DEBUG) Slog.d(TAG, "MTB informing group obs=" + mObserverId);
                    postInformLimitReachedListenerLocked(this);
                }
                cancelCheckTimeoutLocked(this);
            } else {
                if (mActives < 0) {
                    // Try to get to a valid state and log the issue
                    mActives = 0;
                    final UserData user = mUserRef.get();
                    if (user == null) return;
                    final Object[] array = user.currentlyActive.keySet().toArray();
                    Slog.e(TAG,
                            "Too many noted usage stops! Observed entities: " + Arrays.toString(
                                    mObserved) + "   Active Entities: " + Arrays.toString(array));
                }
            }
        }

        @GuardedBy("mLock")
        void checkTimeout(long currentTimeMs) {
            final UserData user = mUserRef.get();
            if (user == null) return;

            long timeRemainingMs = mTimeLimitMs - mUsageTimeMs;

            if (DEBUG) Slog.d(TAG, "checkTimeout timeRemaining=" + timeRemainingMs);

            // Already reached the limit, no need to report again
            if (timeRemainingMs <= 0) return;

            if (DEBUG) {
                Slog.d(TAG, "checkTimeout");
            }

            // Double check that at least one entity in this group is currently active
            if (user.isActive(mObserved)) {
                if (DEBUG) {
                    Slog.d(TAG, "checkTimeout group is active");
                }
                final long timeUsedMs = currentTimeMs - mLastKnownUsageTimeMs;
                if (timeRemainingMs <= timeUsedMs) {
                    if (DEBUG) Slog.d(TAG, "checkTimeout : Time limit reached");
                    // Hit the limit, set timeRemaining to zero to avoid checking again
                    mUsageTimeMs += timeUsedMs;
                    mLastKnownUsageTimeMs = currentTimeMs;
                    AppTimeLimitController.this.postInformLimitReachedListenerLocked(this);
                } else {
                    if (DEBUG) Slog.d(TAG, "checkTimeout : Some more time remaining");
                    AppTimeLimitController.this.postCheckTimeoutLocked(this,
                            timeRemainingMs - timeUsedMs);
                }
            }
        }

        @GuardedBy("mLock")
        public void onLimitReached() {
            UserData user = mUserRef.get();
            if (user == null) return;
            if (mListener != null) {
                mListener.onLimitReached(mObserverId, user.userId, mTimeLimitMs, mUsageTimeMs,
                        mLimitReachedCallback);
            }
        }

        @GuardedBy("mLock")
        void dump(PrintWriter pw) {
            pw.print("        Group id=");
            pw.print(mObserverId);
            pw.print(" timeLimit=");
            pw.print(mTimeLimitMs);
            pw.print(" used=");
            pw.print(mUsageTimeMs);
            pw.print(" lastKnownUsage=");
            pw.print(mLastKnownUsageTimeMs);
            pw.print(" mActives=");
            pw.print(mActives);
            pw.print(" observed=");
            pw.print(Arrays.toString(mObserved));
        }
    }

    class AppUsageGroup extends UsageGroup {
        public AppUsageGroup(UserData user, ObserverAppData observerApp, int observerId,
                String[] observed, long timeLimitMs, PendingIntent limitReachedCallback) {
            super(user, observerApp, observerId, observed, timeLimitMs, limitReachedCallback);
        }

        @Override
        @GuardedBy("mLock")
        public void remove() {
            super.remove();
            ObserverAppData observerApp = mObserverAppRef.get();
            if (observerApp != null) {
                observerApp.removeAppUsageGroup(mObserverId);
            }
        }

        @Override
        @GuardedBy("mLock")
        public void onLimitReached() {
            super.onLimitReached();
            // Unregister since the limit has been met and observer was informed.
            remove();
        }
    }

    class SessionUsageGroup extends UsageGroup implements AlarmManager.OnAlarmListener {
        private long mNewSessionThresholdMs;
        private PendingIntent mSessionEndCallback;

        public SessionUsageGroup(UserData user, ObserverAppData observerApp, int observerId,
                String[] observed, long timeLimitMs, PendingIntent limitReachedCallback,
                long newSessionThresholdMs, PendingIntent sessionEndCallback) {
            super(user, observerApp, observerId, observed, timeLimitMs, limitReachedCallback);
            this.mNewSessionThresholdMs = newSessionThresholdMs;
            this.mSessionEndCallback = sessionEndCallback;
        }

        @Override
        @GuardedBy("mLock")
        public void remove() {
            super.remove();
            ObserverAppData observerApp = mObserverAppRef.get();
            if (observerApp != null) {
                observerApp.removeSessionUsageGroup(mObserverId);
            }
            // Clear the callback, so any racy inflight messages will do nothing
            mSessionEndCallback = null;
        }

        @Override
        @GuardedBy("mLock")
        public void noteUsageStart(long startTimeMs, long currentTimeMs) {
            if (mActives == 0) {
                if (startTimeMs - mLastUsageEndTimeMs > mNewSessionThresholdMs) {
                    // New session has started, clear usage time.
                    mUsageTimeMs = 0;
                }
                getAlarmManager().cancel(this);
            }
            super.noteUsageStart(startTimeMs, currentTimeMs);
        }

        @Override
        @GuardedBy("mLock")
        public void noteUsageStop(long stopTimeMs) {
            super.noteUsageStop(stopTimeMs);
            if (mActives == 0) {
                if (mUsageTimeMs >= mTimeLimitMs) {
                    // Usage has ended. Schedule the session end callback to be triggered once
                    // the new session threshold has been reached
                    getAlarmManager().setExact(AlarmManager.ELAPSED_REALTIME,
                            getElapsedRealtime() + mNewSessionThresholdMs, TAG, this, mHandler);
                }
            }
        }

        @GuardedBy("mLock")
        public void onSessionEnd() {
            UserData user = mUserRef.get();
            if (user == null) return;
            if (mListener != null) {
                mListener.onSessionEnd(mObserverId,
                                       user.userId,
                                       mUsageTimeMs,
                                       mSessionEndCallback);
            }
        }

        @Override
        public void onAlarm() {
            synchronized (mLock) {
                onSessionEnd();
            }
        }

        @Override
        @GuardedBy("mLock")
        void dump(PrintWriter pw) {
            super.dump(pw);
            pw.print(" lastUsageEndTime=");
            pw.print(mLastUsageEndTimeMs);
            pw.print(" newSessionThreshold=");
            pw.print(mNewSessionThresholdMs);
        }
    }

    class AppUsageLimitGroup extends UsageGroup {
        public AppUsageLimitGroup(UserData user, ObserverAppData observerApp, int observerId,
                String[] observed, long timeLimitMs, long timeUsedMs,
                PendingIntent limitReachedCallback) {
            super(user, observerApp, observerId, observed, timeLimitMs, limitReachedCallback);
            mUsageTimeMs = timeUsedMs;
        }

        @Override
        @GuardedBy("mLock")
        public void remove() {
            super.remove();
            ObserverAppData observerApp = mObserverAppRef.get();
            if (observerApp != null) {
                observerApp.removeAppUsageLimitGroup(mObserverId);
            }
        }

        @GuardedBy("mLock")
        long getTotaUsageLimit() {
            return mTimeLimitMs;
        }

        @GuardedBy("mLock")
        long getUsageRemaining() {
            // If there is currently an active session, account for its usage
            if (mActives > 0) {
                return mTimeLimitMs - mUsageTimeMs - (getElapsedRealtime() - mLastKnownUsageTimeMs);
            } else {
                return mTimeLimitMs - mUsageTimeMs;
            }
        }
    }

    private class MyHandler extends Handler {
        static final int MSG_CHECK_TIMEOUT = 1;
        static final int MSG_INFORM_LIMIT_REACHED_LISTENER = 2;

        MyHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_CHECK_TIMEOUT:
                    synchronized (mLock) {
                        ((UsageGroup) msg.obj).checkTimeout(getElapsedRealtime());
                    }
                    break;
                case MSG_INFORM_LIMIT_REACHED_LISTENER:
                    synchronized (mLock) {
                        ((UsageGroup) msg.obj).onLimitReached();
                    }
                    break;
                default:
                    super.handleMessage(msg);
                    break;
            }
        }
    }

    public AppTimeLimitController(Context context, TimeLimitCallbackListener listener,
            Looper looper) {
        mContext = context;
        mHandler = new MyHandler(looper);
        mListener = listener;
    }

    /** Overrideable by a test */
    @VisibleForTesting
    protected AlarmManager getAlarmManager() {
        if (mAlarmManager == null) {
            mAlarmManager = mContext.getSystemService(AlarmManager.class);
        }
        return mAlarmManager;
    }

    /** Overrideable by a test */
    @VisibleForTesting
    protected long getElapsedRealtime() {
        return SystemClock.elapsedRealtime();
    }

    /** Overrideable for testing purposes */
    @VisibleForTesting
    protected long getAppUsageObserverPerUidLimit() {
        return MAX_OBSERVER_PER_UID;
    }

    /** Overrideable for testing purposes */
    @VisibleForTesting
    protected long getUsageSessionObserverPerUidLimit() {
        return MAX_OBSERVER_PER_UID;
    }

    /** Overrideable for testing purposes */
    @VisibleForTesting
    protected long getAppUsageLimitObserverPerUidLimit() {
        return MAX_OBSERVER_PER_UID;
    }

    /** Overrideable for testing purposes */
    @VisibleForTesting
    protected long getMinTimeLimit() {
        return ONE_MINUTE;
    }

    @VisibleForTesting
    AppUsageGroup getAppUsageGroup(int observerAppUid, int observerId) {
        synchronized (mLock) {
            return getOrCreateObserverAppDataLocked(observerAppUid).appUsageGroups.get(observerId);
        }
    }

    @VisibleForTesting
    SessionUsageGroup getSessionUsageGroup(int observerAppUid, int observerId) {
        synchronized (mLock) {
            return getOrCreateObserverAppDataLocked(observerAppUid).sessionUsageGroups.get(
                    observerId);
        }
    }

    @VisibleForTesting
    AppUsageLimitGroup getAppUsageLimitGroup(int observerAppUid, int observerId) {
        synchronized (mLock) {
            return getOrCreateObserverAppDataLocked(observerAppUid).appUsageLimitGroups.get(
                    observerId);
        }
    }

    /**
     * Returns an object describing the app usage limit for the given package which was set via
     * {@link #addAppUsageLimitObserver).
     * If there are multiple limits that apply to the package, the one with the smallest
     * time remaining will be returned.
     */
    public UsageStatsManagerInternal.AppUsageLimitData getAppUsageLimit(
            String packageName, UserHandle user) {
        synchronized (mLock) {
            final UserData userData = getOrCreateUserDataLocked(user.getIdentifier());
            if (userData == null) {
                return null;
            }

            final ArrayList usageGroups = userData.observedMap.get(packageName);
            if (usageGroups == null || usageGroups.isEmpty()) {
                return null;
            }

            final ArraySet usageLimitGroups = new ArraySet<>();
            for (int i = 0; i < usageGroups.size(); i++) {
                if (usageGroups.get(i) instanceof AppUsageLimitGroup) {
                    final AppUsageLimitGroup group = (AppUsageLimitGroup) usageGroups.get(i);
                    for (int j = 0; j < group.mObserved.length; j++) {
                        if (group.mObserved[j].equals(packageName)) {
                            usageLimitGroups.add(group);
                            break;
                        }
                    }
                }
            }
            if (usageLimitGroups.isEmpty()) {
                return null;
            }

            AppUsageLimitGroup smallestGroup = usageLimitGroups.valueAt(0);
            for (int i = 1; i < usageLimitGroups.size(); i++) {
                final AppUsageLimitGroup otherGroup = usageLimitGroups.valueAt(i);
                if (otherGroup.getUsageRemaining() < smallestGroup.getUsageRemaining()) {
                    smallestGroup = otherGroup;
                }
            }
            return new UsageStatsManagerInternal.AppUsageLimitData(
                    smallestGroup.getTotaUsageLimit(), smallestGroup.getUsageRemaining());
        }
    }

    /** Returns an existing UserData object for the given userId, or creates one */
    @GuardedBy("mLock")
    private UserData getOrCreateUserDataLocked(int userId) {
        UserData userData = mUsers.get(userId);
        if (userData == null) {
            userData = new UserData(userId);
            mUsers.put(userId, userData);
        }
        return userData;
    }

    /** Returns an existing ObserverAppData object for the given uid, or creates one */
    @GuardedBy("mLock")
    private ObserverAppData getOrCreateObserverAppDataLocked(int uid) {
        ObserverAppData appData = mObserverApps.get(uid);
        if (appData == null) {
            appData = new ObserverAppData(uid);
            mObserverApps.put(uid, appData);
        }
        return appData;
    }

    /** Clean up data if user is removed */
    public void onUserRemoved(int userId) {
        synchronized (mLock) {
            // TODO: Remove any inflight delayed messages
            mUsers.remove(userId);
        }
    }

    /**
     * Check if group has any currently active entities.
     */
    @GuardedBy("mLock")
    private void noteActiveLocked(UserData user, UsageGroup group, long currentTimeMs) {
        // TODO: Consider using a bloom filter here if number of actives becomes large
        final int size = group.mObserved.length;
        for (int i = 0; i < size; i++) {
            if (user.currentlyActive.containsKey(group.mObserved[i])) {
                // Entity is currently active. Start group's usage.
                group.noteUsageStart(currentTimeMs);
            }
        }
    }

    /**
     * Registers an app usage observer with the given details.
     * Existing app usage observer with the same observerId will be removed.
     */
    public void addAppUsageObserver(int requestingUid, int observerId, String[] observed,
            long timeLimit, PendingIntent callbackIntent, @UserIdInt int userId) {
        if (timeLimit < getMinTimeLimit()) {
            throw new IllegalArgumentException("Time limit must be >= " + getMinTimeLimit());
        }
        synchronized (mLock) {
            UserData user = getOrCreateUserDataLocked(userId);
            ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid);
            AppUsageGroup group = observerApp.appUsageGroups.get(observerId);
            if (group != null) {
                // Remove previous app usage group associated with observerId
                group.remove();
            }

            final int observerIdCount = observerApp.appUsageGroups.size();
            if (observerIdCount >= getAppUsageObserverPerUidLimit()) {
                throw new IllegalStateException(
                        "Too many app usage observers added by uid " + requestingUid);
            }
            group = new AppUsageGroup(user, observerApp, observerId, observed, timeLimit,
                    callbackIntent);
            observerApp.appUsageGroups.append(observerId, group);

            if (DEBUG) {
                Slog.d(TAG, "addObserver " + observed + " for " + timeLimit);
            }

            user.addUsageGroup(group);
            noteActiveLocked(user, group, getElapsedRealtime());
        }
    }

    /**
     * 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 removeAppUsageObserver(int requestingUid, int observerId, @UserIdInt int userId) {
        synchronized (mLock) {
            final ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid);
            final AppUsageGroup group = observerApp.appUsageGroups.get(observerId);
            if (group != null) {
                // Remove previous app usage group associated with observerId
                group.remove();
            }
        }
    }


    /**
     * Registers a usage session observer with the given details.
     * Existing usage session observer with the same observerId will be removed.
     */
    public void addUsageSessionObserver(int requestingUid, int observerId, String[] observed,
            long timeLimit, long sessionThresholdTime,
            PendingIntent limitReachedCallbackIntent, PendingIntent sessionEndCallbackIntent,
            @UserIdInt int userId) {
        if (timeLimit < getMinTimeLimit()) {
            throw new IllegalArgumentException("Time limit must be >= " + getMinTimeLimit());
        }
        synchronized (mLock) {
            UserData user = getOrCreateUserDataLocked(userId);
            ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid);
            SessionUsageGroup group = observerApp.sessionUsageGroups.get(observerId);
            if (group != null) {
                // Remove previous session usage group associated with observerId
                group.remove();
            }

            final int observerIdCount = observerApp.sessionUsageGroups.size();
            if (observerIdCount >= getUsageSessionObserverPerUidLimit()) {
                throw new IllegalStateException(
                        "Too many app usage observers added by uid " + requestingUid);
            }
            group = new SessionUsageGroup(user, observerApp, observerId, observed, timeLimit,
                    limitReachedCallbackIntent, sessionThresholdTime, sessionEndCallbackIntent);
            observerApp.sessionUsageGroups.append(observerId, group);

            user.addUsageGroup(group);
            noteActiveLocked(user, group, getElapsedRealtime());
        }
    }

    /**
     * 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 removeUsageSessionObserver(int requestingUid, int observerId,
            @UserIdInt int userId) {
        synchronized (mLock) {
            final ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid);
            final SessionUsageGroup group = observerApp.sessionUsageGroups.get(observerId);
            if (group != null) {
                // Remove previous app usage group associated with observerId
                group.remove();
            }
        }
    }

    /**
     * Registers an app usage limit observer with the given details.
     * Existing app usage limit observer with the same observerId will be removed.
     */
    public void addAppUsageLimitObserver(int requestingUid, int observerId, String[] observed,
            long timeLimit, long timeUsed, PendingIntent callbackIntent,
            @UserIdInt int userId) {
        if (timeLimit < getMinTimeLimit()) {
            throw new IllegalArgumentException("Time limit must be >= " + getMinTimeLimit());
        }
        synchronized (mLock) {
            UserData user = getOrCreateUserDataLocked(userId);
            ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid);
            AppUsageLimitGroup group = observerApp.appUsageLimitGroups.get(observerId);
            if (group != null) {
                // Remove previous app usage group associated with observerId
                group.remove();
            }

            final int observerIdCount = observerApp.appUsageLimitGroups.size();
            if (observerIdCount >= getAppUsageLimitObserverPerUidLimit()) {
                throw new IllegalStateException(
                        "Too many app usage observers added by uid " + requestingUid);
            }
            group = new AppUsageLimitGroup(user, observerApp, observerId, observed, timeLimit,
                    timeUsed, timeUsed >= timeLimit ? null : callbackIntent);
            observerApp.appUsageLimitGroups.append(observerId, group);

            if (DEBUG) {
                Slog.d(TAG, "addObserver " + observed + " for " + timeLimit);
            }

            user.addUsageGroup(group);
            noteActiveLocked(user, group, getElapsedRealtime());
        }
    }

    /**
     * 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 removeAppUsageLimitObserver(int requestingUid, int observerId,
            @UserIdInt int userId) {
        synchronized (mLock) {
            final ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid);
            final AppUsageLimitGroup group = observerApp.appUsageLimitGroups.get(observerId);
            if (group != null) {
                // Remove previous app usage group associated with observerId
                group.remove();
            }
        }
    }

    /**
     * Called when an entity becomes active.
     *
     * @param name      The entity that became active
     * @param userId    The user
     * @param timeAgoMs Time since usage was started
     */
    public void noteUsageStart(String name, int userId, long timeAgoMs)
            throws IllegalArgumentException {
        synchronized (mLock) {
            UserData user = getOrCreateUserDataLocked(userId);
            if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became active");

            final int index = user.currentlyActive.indexOfKey(name);
            if (index >= 0) {
                final Integer count = user.currentlyActive.valueAt(index);
                if (count != null) {
                    // There are multiple instances of this entity. Just increment the count.
                    user.currentlyActive.setValueAt(index, count + 1);
                    return;
                }
            }
            final long currentTime = getElapsedRealtime();

            user.currentlyActive.put(name, ONE);

            ArrayList groups = user.observedMap.get(name);
            if (groups == null) return;

            final int size = groups.size();
            for (int i = 0; i < size; i++) {
                UsageGroup group = groups.get(i);
                group.noteUsageStart(currentTime - timeAgoMs, currentTime);
            }
        }
    }

    /**
     * Called when an entity becomes active.
     *
     * @param name   The entity that became active
     * @param userId The user
     */
    public void noteUsageStart(String name, int userId) throws IllegalArgumentException {
        noteUsageStart(name, userId, 0);
    }

    /**
     * Called when an entity becomes inactive.
     *
     * @param name   The entity that became inactive
     * @param userId The user
     */
    public void noteUsageStop(String name, int userId) throws IllegalArgumentException {
        synchronized (mLock) {
            UserData user = getOrCreateUserDataLocked(userId);
            if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became inactive");

            final int index = user.currentlyActive.indexOfKey(name);
            if (index < 0) {
                throw new IllegalArgumentException(
                        "Unable to stop usage for " + name + ", not in use");
            }

            final Integer count = user.currentlyActive.valueAt(index);
            if (!count.equals(ONE)) {
                // There are multiple instances of this entity. Just decrement the count.
                user.currentlyActive.setValueAt(index, count - 1);
                return;
            }

            user.currentlyActive.removeAt(index);
            final long currentTime = getElapsedRealtime();

            // Check if any of the groups need to watch for this entity
            ArrayList groups = user.observedMap.get(name);
            if (groups == null) return;

            final int size = groups.size();
            for (int i = 0; i < size; i++) {
                UsageGroup group = groups.get(i);
                group.noteUsageStop(currentTime);
            }

        }
    }

    @GuardedBy("mLock")
    private void postInformLimitReachedListenerLocked(UsageGroup group) {
        mHandler.sendMessage(mHandler.obtainMessage(MyHandler.MSG_INFORM_LIMIT_REACHED_LISTENER,
                group));
    }

    @GuardedBy("mLock")
    private void postCheckTimeoutLocked(UsageGroup group, long timeout) {
        mHandler.sendMessageDelayed(mHandler.obtainMessage(MyHandler.MSG_CHECK_TIMEOUT, group),
                timeout);
    }

    @GuardedBy("mLock")
    private void cancelCheckTimeoutLocked(UsageGroup group) {
        mHandler.removeMessages(MyHandler.MSG_CHECK_TIMEOUT, group);
    }

    void dump(String[] args, PrintWriter pw) {
        if (args != null) {
            for (int i = 0; i < args.length; i++) {
                String arg = args[i];
                if ("actives".equals(arg)) {
                    synchronized (mLock) {
                        final int nUsers = mUsers.size();
                        for (int user = 0; user < nUsers; user++) {
                            final ArrayMap actives =
                                    mUsers.valueAt(user).currentlyActive;
                            final int nActive = actives.size();
                            for (int active = 0; active < nActive; active++) {
                                pw.println(actives.keyAt(active));
                            }
                        }
                    }
                    return;
                }
            }
        }

        synchronized (mLock) {
            pw.println("\n  App Time Limits");
            final int nUsers = mUsers.size();
            for (int i = 0; i < nUsers; i++) {
                pw.print("   User ");
                mUsers.valueAt(i).dump(pw);
            }
            pw.println();
            final int nObserverApps = mObserverApps.size();
            for (int i = 0; i < nObserverApps; i++) {
                pw.print("   Observer App ");
                mObserverApps.valueAt(i).dump(pw);
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy