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

Commit a8bb9d25 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

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

parents 3b0450d4 62ec27e9
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