Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0071ccea authored by Amith Yamasani's avatar Amith Yamasani Committed by android-build-merger
Browse files

Merge "App Time Limits API in UsageStats" into pi-dev

am: a8bb9d25

Change-Id: I77b6656d38701b76444c64be1d75c7b86e70e3bb
parents 12c26fc5 a8bb9d25
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -729,9 +729,14 @@ package android.app.usage {
  public final class UsageStatsManager {
    method public int getAppStandbyBucket(java.lang.String);
    method public java.util.Map<java.lang.String, java.lang.Integer> getAppStandbyBuckets();
    method public void registerAppUsageObserver(int, java.lang.String[], long, java.util.concurrent.TimeUnit, android.app.PendingIntent);
    method public void setAppStandbyBucket(java.lang.String, int);
    method public void setAppStandbyBuckets(java.util.Map<java.lang.String, java.lang.Integer>);
    method public void unregisterAppUsageObserver(int);
    method public void whitelistAppTemporarily(java.lang.String, long, android.os.UserHandle);
    field public static final java.lang.String EXTRA_OBSERVER_ID = "android.app.usage.extra.OBSERVER_ID";
    field public static final java.lang.String EXTRA_TIME_LIMIT = "android.app.usage.extra.TIME_LIMIT";
    field public static final java.lang.String EXTRA_TIME_USED = "android.app.usage.extra.TIME_USED";
    field public static final int STANDBY_BUCKET_EXEMPTED = 5; // 0x5
    field public static final int STANDBY_BUCKET_NEVER = 50; // 0x32
  }
+4 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.app.usage;

import android.app.PendingIntent;
import android.app.usage.UsageEvents;
import android.content.pm.ParceledListSlice;

@@ -43,4 +44,7 @@ interface IUsageStatsManager {
    void setAppStandbyBucket(String packageName, int bucket, int userId);
    ParceledListSlice getAppStandbyBuckets(String callingPackage, int userId);
    void setAppStandbyBuckets(in ParceledListSlice appBuckets, int userId);
    void registerAppUsageObserver(int observerId, in String[] packages, long timeLimitMs,
            in PendingIntent callback, String callingPackage);
    void unregisterAppUsageObserver(int observerId, String callingPackage);
}
+74 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.annotation.IntDef;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.os.RemoteException;
@@ -32,6 +33,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Provides access to device usage history and statistics. Usage data is aggregated into
@@ -179,6 +181,31 @@ public final class UsageStatsManager {
    @Retention(RetentionPolicy.SOURCE)
    public @interface StandbyBuckets {}

    /**
     * Observer id of the registered observer for the group of packages that reached the usage
     * time limit. Included as an extra in the PendingIntent that was registered.
     * @hide
     */
    @SystemApi
    public static final String EXTRA_OBSERVER_ID = "android.app.usage.extra.OBSERVER_ID";

    /**
     * Original time limit in milliseconds specified by the registered observer for the group of
     * packages that reached the usage time limit. Included as an extra in the PendingIntent that
     * was registered.
     * @hide
     */
    @SystemApi
    public static final String EXTRA_TIME_LIMIT = "android.app.usage.extra.TIME_LIMIT";

    /**
     * Actual usage time in milliseconds for the group of packages that reached the specified time
     * limit. Included as an extra in the PendingIntent that was registered.
     * @hide
     */
    @SystemApi
    public static final String EXTRA_TIME_USED = "android.app.usage.extra.TIME_USED";

    private static final UsageEvents sEmptyResults = new UsageEvents();

    private final Context mContext;
@@ -470,6 +497,53 @@ public final class UsageStatsManager {
        }
    }

    /**
     * @hide
     * Register an app usage limit observer that receives a callback on the provided intent when
     * the sum of usages of apps in the packages array exceeds the timeLimit specified. The
     * observer will automatically be unregistered when the time limit is reached and the intent
     * is delivered.
     * @param observerId A unique id associated with the group of apps to be monitored. There can
     *                  be multiple groups with common packages and different time limits.
     * @param packages The list of packages to observe for foreground activity time. Must include
     *                 at least one package.
     * @param timeLimit The total time the set of apps can be in the foreground before the
     *                  callbackIntent is delivered. Must be greater than 0.
     * @param timeUnit The unit for time specified in timeLimit.
     * @param callbackIntent The PendingIntent that will be dispatched when the time limit is
     *                       exceeded by the group of apps. The delivered Intent will also contain
     *                       the extras {@link #EXTRA_OBSERVER_ID}, {@link #EXTRA_TIME_LIMIT} and
     *                       {@link #EXTRA_TIME_USED}.
     * @throws SecurityException if the caller doesn't have the PACKAGE_USAGE_STATS permission.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
    public void registerAppUsageObserver(int observerId, String[] packages, long timeLimit,
            TimeUnit timeUnit, PendingIntent callbackIntent) {
        try {
            mService.registerAppUsageObserver(observerId, packages, timeUnit.toMillis(timeLimit),
                    callbackIntent, mContext.getOpPackageName());
        } catch (RemoteException e) {
        }
    }

    /**
     * @hide
     * Unregister the app usage observer specified by the observerId. This will only apply to any
     * observer registered by this application. Unregistering an observer that was already
     * unregistered or never registered will have no effect.
     * @param observerId The id of the observer that was previously registered.
     * @throws SecurityException if the caller doesn't have the PACKAGE_USAGE_STATS permission.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
    public void unregisterAppUsageObserver(int observerId) {
        try {
            mService.unregisterAppUsageObserver(observerId, mContext.getOpPackageName());
        } catch (RemoteException e) {
        }
    }

    /** @hide */
    public static String reasonToString(int standbyReason) {
        StringBuilder sb = new StringBuilder();
+256 −0
Original line number Diff line number Diff line
/*
 * 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.app.PendingIntent;
import android.os.HandlerThread;
import android.os.Looper;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@RunWith(AndroidJUnit4.class)
@MediumTest
public class AppTimeLimitControllerTests {

    private static final String PKG_SOC1 = "package.soc1";
    private static final String PKG_SOC2 = "package.soc2";
    private static final String PKG_GAME1 = "package.game1";
    private static final String PKG_GAME2 = "package.game2";
    private static final String PKG_PROD = "package.prod";

    private static final int UID = 10100;
    private static final int USER_ID = 10;
    private static final int OBS_ID1 = 1;
    private static final int OBS_ID2 = 2;
    private static final int OBS_ID3 = 3;

    private static final long TIME_30_MIN = 30 * 60_1000L;
    private static final long TIME_10_MIN = 10 * 60_1000L;

    private static final String[] GROUP1 = {
            PKG_SOC1, PKG_GAME1, PKG_PROD
    };

    private static final String[] GROUP_SOC = {
            PKG_SOC1, PKG_SOC2
    };

    private static final String[] GROUP_GAME = {
            PKG_GAME1, PKG_GAME2
    };

    private final CountDownLatch mCountDownLatch = new CountDownLatch(1);

    private AppTimeLimitController mController;

    private HandlerThread mThread;

    private long mUptimeMillis;

    AppTimeLimitController.OnLimitReachedListener mListener
            = new AppTimeLimitController.OnLimitReachedListener() {

        @Override
        public void onLimitReached(int observerId, int userId, long timeLimit, long timeElapsed,
                PendingIntent callbackIntent) {
            mCountDownLatch.countDown();
        }
    };

    class MyAppTimeLimitController extends AppTimeLimitController {
        MyAppTimeLimitController(AppTimeLimitController.OnLimitReachedListener listener,
                Looper looper) {
            super(listener, looper);
        }

        @Override
        protected long getUptimeMillis() {
            return mUptimeMillis;
        }
    }

    @Before
    public void setUp() {
        mThread = new HandlerThread("Test");
        mThread.start();
        mController = new MyAppTimeLimitController(mListener, mThread.getLooper());
    }

    @After
    public void tearDown() {
        mThread.quit();
    }

    /** Verify observer is added */
    @Test
    public void testAddObserver() {
        addObserver(OBS_ID1, GROUP1, TIME_30_MIN);
        assertTrue("Observer wasn't added", hasObserver(OBS_ID1));
        addObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN);
        assertTrue("Observer wasn't added", hasObserver(OBS_ID2));
        assertTrue("Observer wasn't added", hasObserver(OBS_ID1));
    }

    /** Verify observer is removed */
    @Test
    public void testRemoveObserver() {
        addObserver(OBS_ID1, GROUP1, TIME_30_MIN);
        assertTrue("Observer wasn't added", hasObserver(OBS_ID1));
        mController.removeObserver(UID, OBS_ID1, USER_ID);
        assertFalse("Observer wasn't removed", hasObserver(OBS_ID1));
    }

    /** Re-adding an observer should result in only one copy */
    @Test
    public void testObserverReAdd() {
        addObserver(OBS_ID1, GROUP1, TIME_30_MIN);
        assertTrue("Observer wasn't added", hasObserver(OBS_ID1));
        addObserver(OBS_ID1, GROUP1, TIME_10_MIN);
        assertTrue("Observer wasn't added",
                mController.getObserverGroup(OBS_ID1, USER_ID).timeLimit == TIME_10_MIN);
        mController.removeObserver(UID, OBS_ID1, USER_ID);
        assertFalse("Observer wasn't removed", hasObserver(OBS_ID1));
    }

    /** Verify that usage across different apps within a group are added up */
    @Test
    public void testAccumulation() throws Exception {
        setTime(0L);
        addObserver(OBS_ID1, GROUP1, TIME_30_MIN);
        moveToForeground(PKG_SOC1);
        // Add 10 mins
        setTime(TIME_10_MIN);
        moveToBackground(PKG_SOC1);

        long timeRemaining = mController.getObserverGroup(OBS_ID1, USER_ID).timeRemaining;
        assertEquals(TIME_10_MIN * 2, timeRemaining);

        moveToForeground(PKG_SOC1);
        setTime(TIME_10_MIN * 2);
        moveToBackground(PKG_SOC1);

        timeRemaining = mController.getObserverGroup(OBS_ID1, USER_ID).timeRemaining;
        assertEquals(TIME_10_MIN, timeRemaining);

        setTime(TIME_30_MIN);

        assertFalse(mCountDownLatch.await(100L, TimeUnit.MILLISECONDS));

        // Add a different package in the group
        moveToForeground(PKG_GAME1);
        setTime(TIME_30_MIN + TIME_10_MIN);
        moveToBackground(PKG_GAME1);

        assertEquals(0, mController.getObserverGroup(OBS_ID1, USER_ID).timeRemaining);
        assertTrue(mCountDownLatch.await(100L, TimeUnit.MILLISECONDS));
    }

    /** Verify that time limit does not get triggered due to a different app */
    @Test
    public void testTimeoutOtherApp() throws Exception {
        setTime(0L);
        addObserver(OBS_ID1, GROUP1, 4_000L);
        moveToForeground(PKG_SOC2);
        assertFalse(mCountDownLatch.await(6_000L, TimeUnit.MILLISECONDS));
        setTime(6_000L);
        moveToBackground(PKG_SOC2);
        assertFalse(mCountDownLatch.await(100L, TimeUnit.MILLISECONDS));
    }

    /** Verify the timeout message is delivered at the right time */
    @Test
    public void testTimeout() throws Exception {
        setTime(0L);
        addObserver(OBS_ID1, GROUP1, 4_000L);
        moveToForeground(PKG_SOC1);
        setTime(6_000L);
        assertTrue(mCountDownLatch.await(6_000L, TimeUnit.MILLISECONDS));
        moveToBackground(PKG_SOC1);
        // Verify that the observer was removed
        assertFalse(hasObserver(OBS_ID1));
    }

    /** If an app was already running, make sure it is partially counted towards the time limit */
    @Test
    public void testAlreadyRunning() throws Exception {
        setTime(TIME_10_MIN);
        moveToForeground(PKG_GAME1);
        setTime(TIME_30_MIN);
        addObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN);
        setTime(TIME_30_MIN + TIME_10_MIN);
        moveToBackground(PKG_GAME1);
        assertFalse(mCountDownLatch.await(1000L, TimeUnit.MILLISECONDS));

        moveToForeground(PKG_GAME2);
        setTime(TIME_30_MIN + TIME_30_MIN);
        moveToBackground(PKG_GAME2);
        assertTrue(mCountDownLatch.await(1000L, TimeUnit.MILLISECONDS));
        // Verify that the observer was removed
        assertFalse(hasObserver(OBS_ID2));
    }

    /** If watched app is already running, verify the timeout callback happens at the right time */
    @Test
    public void testAlreadyRunningTimeout() throws Exception {
        setTime(0);
        moveToForeground(PKG_SOC1);
        setTime(TIME_10_MIN);
        // 10 second time limit
        addObserver(OBS_ID1, GROUP_SOC, 10_000L);
        setTime(TIME_10_MIN + 5_000L);
        // Shouldn't call back in 6 seconds
        assertFalse(mCountDownLatch.await(6_000L, TimeUnit.MILLISECONDS));
        setTime(TIME_10_MIN + 10_000L);
        // Should call back by 11 seconds (6 earlier + 5 now)
        assertTrue(mCountDownLatch.await(5_000L, TimeUnit.MILLISECONDS));
        // Verify that the observer was removed
        assertFalse(hasObserver(OBS_ID1));
    }

    private void moveToForeground(String packageName) {
        mController.moveToForeground(packageName, "class", USER_ID);
    }

    private void moveToBackground(String packageName) {
        mController.moveToBackground(packageName, "class", USER_ID);
    }

    private void addObserver(int observerId, String[] packages, long timeLimit) {
        mController.addObserver(UID, observerId, packages, timeLimit, null, USER_ID);
    }

    /** Is there still an observer by that id */
    private boolean hasObserver(int observerId) {
        return mController.getObserverGroup(observerId, USER_ID) != null;
    }

    private void setTime(long time) {
        mUptimeMillis = time;
    }
}
+464 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading