diff options
16 files changed, 940 insertions, 9 deletions
diff --git a/api/current.txt b/api/current.txt index 0ea7ecc8d6b1..98fd348ece66 100644 --- a/api/current.txt +++ b/api/current.txt @@ -11210,6 +11210,7 @@ package android.content.pm { public class LauncherApps { method public java.util.List<android.content.pm.LauncherActivityInfo> getActivityList(String, android.os.UserHandle); + method @Nullable public android.content.pm.LauncherApps.AppUsageLimit getAppUsageLimit(String, android.os.UserHandle); method public android.content.pm.ApplicationInfo getApplicationInfo(@NonNull String, int, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.pm.LauncherApps.PinItemRequest getPinItemRequest(android.content.Intent); method public java.util.List<android.os.UserHandle> getProfiles(); @@ -11237,6 +11238,14 @@ package android.content.pm { field public static final String EXTRA_PIN_ITEM_REQUEST = "android.content.pm.extra.PIN_ITEM_REQUEST"; } + public static final class LauncherApps.AppUsageLimit implements android.os.Parcelable { + method public int describeContents(); + method public long getTotalUsageLimit(); + method public long getUsageRemaining(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.content.pm.LauncherApps.AppUsageLimit> CREATOR; + } + public abstract static class LauncherApps.Callback { ctor public LauncherApps.Callback(); method public abstract void onPackageAdded(String, android.os.UserHandle); diff --git a/api/system-current.txt b/api/system-current.txt index 15d6ab7b63fc..27e9a38e379f 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -1112,6 +1112,7 @@ package android.app.usage { method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public int getAppStandbyBucket(String); method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public java.util.Map<java.lang.String,java.lang.Integer> getAppStandbyBuckets(); method public int getUsageSource(); + method @RequiresPermission(allOf={android.Manifest.permission.SUSPEND_APPS, android.Manifest.permission.OBSERVE_APP_USAGE}) public void registerAppUsageLimitObserver(int, @NonNull String[], long, @NonNull java.util.concurrent.TimeUnit, @NonNull android.app.PendingIntent); method @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE) public void registerAppUsageObserver(int, @NonNull String[], long, @NonNull java.util.concurrent.TimeUnit, @NonNull android.app.PendingIntent); method @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE) public void registerUsageSessionObserver(int, @NonNull String[], long, @NonNull java.util.concurrent.TimeUnit, long, @NonNull java.util.concurrent.TimeUnit, @NonNull android.app.PendingIntent, @Nullable android.app.PendingIntent); method public void reportUsageStart(@NonNull android.app.Activity, @NonNull String); @@ -1119,6 +1120,7 @@ package android.app.usage { method public void reportUsageStop(@NonNull android.app.Activity, @NonNull String); method @RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE) public void setAppStandbyBucket(String, int); method @RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE) public void setAppStandbyBuckets(java.util.Map<java.lang.String,java.lang.Integer>); + method @RequiresPermission(allOf={android.Manifest.permission.SUSPEND_APPS, android.Manifest.permission.OBSERVE_APP_USAGE}) public void unregisterAppUsageLimitObserver(int); method @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE) public void unregisterAppUsageObserver(int); method @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE) public void unregisterUsageSessionObserver(int); method @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST) public void whitelistAppTemporarily(String, long, android.os.UserHandle); diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl index d2934b9f5a21..b1500c193820 100644 --- a/core/java/android/app/usage/IUsageStatsManager.aidl +++ b/core/java/android/app/usage/IUsageStatsManager.aidl @@ -55,6 +55,9 @@ interface IUsageStatsManager { long sessionThresholdTimeMs, in PendingIntent limitReachedCallbackIntent, in PendingIntent sessionEndCallbackIntent, String callingPackage); void unregisterUsageSessionObserver(int sessionObserverId, String callingPackage); + void registerAppUsageLimitObserver(int observerId, in String[] packages, long timeLimitMs, + in PendingIntent callback, String callingPackage); + void unregisterAppUsageLimitObserver(int observerId, String callingPackage); void reportUsageStart(in IBinder activity, String token, String callingPackage); void reportPastUsageStart(in IBinder activity, String token, long timeAgoMs, String callingPackage); diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index d2de8872c1bd..51397a243420 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -619,7 +619,7 @@ public final class UsageStatsManager { * @param timeLimit The total time the set of apps can be in the foreground before the * callbackIntent is delivered. Must be at least one minute. * @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null. - * @param callbackIntent The PendingIntent that will be dispatched when the time limit is + * @param callbackIntent The PendingIntent that will be dispatched when the usage 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}. Cannot be null. @@ -682,14 +682,14 @@ public final class UsageStatsManager { * @param sessionThresholdTimeUnit The unit for time specified in {@code sessionThreshold}. * Cannot be null. * @param limitReachedCallbackIntent The {@link 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 + * usage 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}. Cannot be null. * @param sessionEndCallbackIntent The {@link PendingIntent} that will be dispatched when the - * session has ended after the time limit has been exceeded. The - * session is considered at its end after the {@code observed} - * usage has stopped and an additional {@code + * session has ended after the usage limit has been exceeded. + * The session is considered at its end after the {@code + * observed} usage has stopped and an additional {@code * sessionThresholdTime} has passed. The delivered Intent will * also contain the extras {@link #EXTRA_OBSERVER_ID} and {@link * #EXTRA_TIME_USED}. Can be null. @@ -736,6 +736,74 @@ public final class UsageStatsManager { } /** + * Register a usage limit observer that receives a callback on the provided intent when the + * sum of usages of apps and tokens in the provided {@code observedEntities} array exceeds the + * {@code timeLimit} specified. The structure of a token is a {@link String} with the reporting + * package's name and a token that the calling app will use, separated by the forward slash + * character. Example: com.reporting.package/5OM3*0P4QU3-7OK3N + * <p> + * Registering an {@code observerId} that was already registered will override the previous one. + * No more than 1000 unique {@code observerId} may be registered by a single uid + * at any one time. + * A limit may be unregistered via {@link #unregisterAppUsageLimitObserver} + * <p> + * This method is similar to {@link #registerAppUsageObserver}, but the usage limit set here + * will be visible to the launcher so that it can report the limit to the user and how much + * of it is remaining. + * @see android.content.pm.LauncherApps#getAppUsageLimit + * + * @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 observedEntities The list of packages and token to observe for usage time. Cannot be + * null and must include at least one package or token. + * @param timeLimit The total time the set of apps can be in the foreground before the + * callbackIntent is delivered. Must be at least one minute. + * @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null. + * @param callbackIntent The PendingIntent that will be dispatched when the usage 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}. Cannot be null. + * @throws SecurityException if the caller doesn't have both SUSPEND_APPS and OBSERVE_APP_USAGE + * permissions. + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.SUSPEND_APPS, + android.Manifest.permission.OBSERVE_APP_USAGE}) + public void registerAppUsageLimitObserver(int observerId, @NonNull String[] observedEntities, + long timeLimit, @NonNull TimeUnit timeUnit, @NonNull PendingIntent callbackIntent) { + try { + mService.registerAppUsageLimitObserver(observerId, observedEntities, + timeUnit.toMillis(timeLimit), callbackIntent, mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Unregister the app usage limit observer specified by the {@code 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 both SUSPEND_APPS and OBSERVE_APP_USAGE + * permissions. + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.SUSPEND_APPS, + android.Manifest.permission.OBSERVE_APP_USAGE}) + public void unregisterAppUsageLimitObserver(int observerId) { + try { + mService.unregisterAppUsageLimitObserver(observerId, mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Report usage associated with a particular {@code token} has started. Tokens are app defined * strings used to represent usage of in-app features. Apps with the {@link * android.Manifest.permission#OBSERVE_APP_USAGE} permission can register time limit observers @@ -743,6 +811,7 @@ public final class UsageStatsManager { * and usage will be considered stopped if the activity stops or crashes. * @see #registerAppUsageObserver * @see #registerUsageSessionObserver + * @see #registerAppUsageLimitObserver * * @param activity The activity {@code token} is associated with. * @param token The token to report usage against. @@ -766,6 +835,7 @@ public final class UsageStatsManager { * {@code activity} and usage will be considered stopped if the activity stops or crashes. * @see #registerAppUsageObserver * @see #registerUsageSessionObserver + * @see #registerAppUsageLimitObserver * * @param activity The activity {@code token} is associated with. * @param token The token to report usage against. diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java index d2d0cf9ca90b..3d3c03ae3daa 100644 --- a/core/java/android/app/usage/UsageStatsManagerInternal.java +++ b/core/java/android/app/usage/UsageStatsManagerInternal.java @@ -20,6 +20,7 @@ import android.annotation.UserIdInt; import android.app.usage.UsageStatsManager.StandbyBuckets; import android.content.ComponentName; import android.content.res.Configuration; +import android.os.UserHandle; import java.util.List; import java.util.Set; @@ -270,4 +271,40 @@ public abstract class UsageStatsManagerInternal { * @param userId which user the app is associated with */ public abstract void reportExemptedSyncStart(String packageName, @UserIdInt int userId); + + /** + * Returns an object describing the app usage limit for the given package which was set via + * {@link UsageStatsManager#registerAppUsageLimitObserver}. + * If there are multiple limits that apply to the package, the one with the smallest + * time remaining will be returned. + * + * @param packageName name of the package whose app usage limit will be returned + * @param user the user associated with the limit + * @return an {@link AppUsageLimitData} object describing the app time limit containing + * the given package, with the smallest time remaining. + */ + public abstract AppUsageLimitData getAppUsageLimit(String packageName, UserHandle user); + + /** A class which is used to share the usage limit data for an app or a group of apps. */ + public static class AppUsageLimitData { + private final boolean mGroupLimit; + private final long mTotalUsageLimit; + private final long mUsageRemaining; + + public AppUsageLimitData(boolean groupLimit, long totalUsageLimit, long usageRemaining) { + this.mGroupLimit = groupLimit; + this.mTotalUsageLimit = totalUsageLimit; + this.mUsageRemaining = usageRemaining; + } + + public boolean isGroupLimit() { + return mGroupLimit; + } + public long getTotalUsageLimit() { + return mTotalUsageLimit; + } + public long getUsageRemaining() { + return mUsageRemaining; + } + } } diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index db2b6fd235d3..d1bc37791d40 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -23,6 +23,7 @@ import android.content.IntentSender; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.IOnAppsChangedListener; +import android.content.pm.LauncherApps; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; @@ -56,6 +57,9 @@ interface ILauncherApps { ApplicationInfo getApplicationInfo( String callingPackage, String packageName, int flags, in UserHandle user); + LauncherApps.AppUsageLimit getAppUsageLimit(String callingPackage, String packageName, + in UserHandle user); + ParceledListSlice getShortcuts(String callingPackage, long changedSince, String packageName, in List shortcutIds, in ComponentName componentName, int flags, in UserHandle user); void pinShortcuts(String callingPackage, String packageName, in List<String> shortcutIds, diff --git a/core/java/android/content/pm/LauncherApps.aidl b/core/java/android/content/pm/LauncherApps.aidl new file mode 100644 index 000000000000..1d98ad1abd32 --- /dev/null +++ b/core/java/android/content/pm/LauncherApps.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2019, 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 android.content.pm; + +parcelable LauncherApps.AppUsageLimit; diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 766c5660012b..89630e15972e 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -758,6 +758,27 @@ public class LauncherApps { } /** + * Returns an object describing the app usage limit for the given package. + * If there are multiple limits that apply to the package, the one with the smallest + * time remaining will be returned. + * + * @param packageName name of the package whose app usage limit will be returned + * @param user the user of the package + * + * @return an {@link AppUsageLimit} object describing the app time limit containing + * the given package with the smallest time remaining, or {@code null} if none exist. + * @throws SecurityException when the caller is not the active launcher. + */ + @Nullable + public LauncherApps.AppUsageLimit getAppUsageLimit(String packageName, UserHandle user) { + try { + return mService.getAppUsageLimit(mContext.getPackageName(), packageName, user); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** * Checks if the activity exists and it enabled for a profile. * * @param component The activity to check. @@ -1632,4 +1653,86 @@ public class LauncherApps { return 0; } } + + /** + * A class that encapsulates information about the usage limit set for an app or + * a group of apps. + * + * <p>The launcher can query specifics about the usage limit such as if it is a group limit, + * how much usage time the limit has, and how much of the total usage time is remaining + * via the APIs available in this class. + * + * @see #getAppUsageLimit(String, UserHandle) + */ + public static final class AppUsageLimit implements Parcelable { + private final boolean mGroupLimit; + private final long mTotalUsageLimit; + private final long mUsageRemaining; + + /** @hide */ + public AppUsageLimit(boolean groupLimit, long totalUsageLimit, long usageRemaining) { + this.mGroupLimit = groupLimit; + this.mTotalUsageLimit = totalUsageLimit; + this.mUsageRemaining = usageRemaining; + } + + /** + * Returns whether this limit refers to a group of apps. + * + * @return {@code TRUE} if the limit refers to a group of apps, {@code FALSE} otherwise. + * @hide + */ + public boolean isGroupLimit() { + return mGroupLimit; + } + + /** + * Returns the total usage limit in milliseconds set for an app or a group of apps. + * + * @return the total usage limit in milliseconds + */ + public long getTotalUsageLimit() { + return mTotalUsageLimit; + } + + /** + * Returns the usage remaining in milliseconds for an app or the group of apps + * this limit refers to. + * + * @return the usage remaining in milliseconds + */ + public long getUsageRemaining() { + return mUsageRemaining; + } + + private AppUsageLimit(Parcel source) { + mGroupLimit = source.readBoolean(); + mTotalUsageLimit = source.readLong(); + mUsageRemaining = source.readLong(); + } + + public static final Creator<AppUsageLimit> CREATOR = new Creator<AppUsageLimit>() { + @Override + public AppUsageLimit createFromParcel(Parcel source) { + return new AppUsageLimit(source); + } + + @Override + public AppUsageLimit[] newArray(int size) { + return new AppUsageLimit[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBoolean(mGroupLimit); + dest.writeLong(mTotalUsageLimit); + dest.writeLong(mUsageRemaining); + } + } } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index a33f14bab4b1..d0ef4f1523d4 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -25,6 +25,7 @@ import android.app.AppGlobals; import android.app.IApplicationThread; import android.app.PendingIntent; import android.app.admin.DevicePolicyManager; +import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -135,6 +136,7 @@ public class LauncherAppsService extends SystemService { private final Context mContext; private final UserManager mUm; private final UserManagerInternal mUserManagerInternal; + private final UsageStatsManagerInternal mUsageStatsManagerInternal; private final ActivityManagerInternal mActivityManagerInternal; private final ActivityTaskManagerInternal mActivityTaskManagerInternal; private final ShortcutServiceInternal mShortcutServiceInternal; @@ -156,6 +158,8 @@ public class LauncherAppsService extends SystemService { mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE); mUserManagerInternal = Preconditions.checkNotNull( LocalServices.getService(UserManagerInternal.class)); + mUsageStatsManagerInternal = Preconditions.checkNotNull( + LocalServices.getService(UsageStatsManagerInternal.class)); mActivityManagerInternal = Preconditions.checkNotNull( LocalServices.getService(ActivityManagerInternal.class)); mActivityTaskManagerInternal = Preconditions.checkNotNull( @@ -671,6 +675,30 @@ public class LauncherAppsService extends SystemService { } } + @Override + public LauncherApps.AppUsageLimit getAppUsageLimit(String callingPackage, + String packageName, UserHandle user) { + verifyCallingPackage(callingPackage); + if (!canAccessProfile(user.getIdentifier(), "Cannot access usage limit")) { + return null; + } + + final PackageManagerInternal pmi = + LocalServices.getService(PackageManagerInternal.class); + final ComponentName cn = pmi.getDefaultHomeActivity(user.getIdentifier()); + if (!cn.getPackageName().equals(callingPackage)) { + throw new SecurityException("Caller is not the active launcher"); + } + + final UsageStatsManagerInternal.AppUsageLimitData data = + mUsageStatsManagerInternal.getAppUsageLimit(packageName, user); + if (data == null) { + return null; + } + return new LauncherApps.AppUsageLimit( + data.isGroupLimit(), data.getTotalUsageLimit(), data.getUsageRemaining()); + } + private void ensureShortcutPermission(@NonNull String callingPackage) { verifyCallingPackage(callingPackage); if (!mShortcutServiceInternal.hasShortcutHostPermission(getCallingUserId(), diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index bc1f7981258d..6845f15f6a28 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -43,6 +43,7 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.IUidObserver; import android.app.Person; +import android.app.admin.DevicePolicyManager; import android.app.usage.UsageStatsManagerInternal; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; @@ -143,8 +144,10 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { switch (name) { case Context.USER_SERVICE: return mMockUserManager; + case Context.DEVICE_POLICY_SERVICE: + return mMockDevicePolicyManager; } - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("Couldn't find system service: " + name); } @Override @@ -610,6 +613,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected PackageManager mMockPackageManager; protected PackageManagerInternal mMockPackageManagerInternal; protected UserManager mMockUserManager; + protected DevicePolicyManager mMockDevicePolicyManager; protected UserManagerInternal mMockUserManagerInternal; protected UsageStatsManagerInternal mMockUsageStatsManagerInternal; protected ActivityManagerInternal mMockActivityManagerInternal; @@ -750,6 +754,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { mMockPackageManager = mock(PackageManager.class); mMockPackageManagerInternal = mock(PackageManagerInternal.class); mMockUserManager = mock(UserManager.class); + mMockDevicePolicyManager = mock(DevicePolicyManager.class); mMockUserManagerInternal = mock(UserManagerInternal.class); mMockUsageStatsManagerInternal = mock(UsageStatsManagerInternal.class); mMockActivityManagerInternal = mock(ActivityManagerInternal.class); diff --git a/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java index b348aeef802e..5d69bbdcf0c7 100644 --- a/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java @@ -18,12 +18,15 @@ package com.android.server.usage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import android.app.PendingIntent; +import android.app.usage.UsageStatsManagerInternal; import android.os.HandlerThread; import android.os.Looper; +import android.os.UserHandle; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; @@ -64,7 +67,7 @@ public class AppTimeLimitControllerTests { private static final long TIME_30_MIN = 30 * 60_000L; private static final long TIME_10_MIN = 10 * 60_000L; - private static final long TIME_1_MIN = 10 * 60_000L; + private static final long TIME_1_MIN = 1 * 60_000L; private static final long MAX_OBSERVER_PER_UID = 10; private static final long MIN_TIME_LIMIT = 4_000L; @@ -128,6 +131,11 @@ public class AppTimeLimitControllerTests { } @Override + protected long getAppUsageLimitObserverPerUidLimit() { + return MAX_OBSERVER_PER_UID; + } + + @Override protected long getMinTimeLimit() { return MIN_TIME_LIMIT; } @@ -164,6 +172,16 @@ public class AppTimeLimitControllerTests { assertTrue("Observer wasn't added", hasUsageSessionObserver(UID, OBS_ID2)); } + /** Verify app usage limit observer is added */ + @Test + public void testAppUsageLimitObserver_AddObserver() { + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + assertTrue("Observer wasn't added", hasAppUsageLimitObserver(UID, OBS_ID1)); + addAppUsageLimitObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN); + assertTrue("Observer wasn't added", hasAppUsageLimitObserver(UID, OBS_ID2)); + assertTrue("Observer wasn't added", hasAppUsageLimitObserver(UID, OBS_ID1)); + } + /** Verify app usage observer is removed */ @Test public void testAppUsageObserver_RemoveObserver() { @@ -182,6 +200,15 @@ public class AppTimeLimitControllerTests { assertFalse("Observer wasn't removed", hasUsageSessionObserver(UID, OBS_ID1)); } + /** Verify app usage limit observer is removed */ + @Test + public void testAppUsageLimitObserver_RemoveObserver() { + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + assertTrue("Observer wasn't added", hasAppUsageLimitObserver(UID, OBS_ID1)); + mController.removeAppUsageLimitObserver(UID, OBS_ID1, USER_ID); + assertFalse("Observer wasn't removed", hasAppUsageLimitObserver(UID, OBS_ID1)); + } + /** Verify nothing happens when a nonexistent app usage observer is removed */ @Test public void testAppUsageObserver_RemoveMissingObserver() { @@ -218,6 +245,24 @@ public class AppTimeLimitControllerTests { assertFalse("Observer should not exist", hasUsageSessionObserver(UID, OBS_ID1)); } + /** Verify nothing happens when a nonexistent app usage limit observer is removed */ + @Test + public void testAppUsageLimitObserver_RemoveMissingObserver() { + assertFalse("Observer should not exist", hasAppUsageLimitObserver(UID, OBS_ID1)); + try { + mController.removeAppUsageLimitObserver(UID, OBS_ID1, USER_ID); + } catch (Exception e) { + StringWriter sw = new StringWriter(); + sw.write("Hit exception trying to remove nonexistent observer:\n"); + sw.write(e.toString()); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + sw.write("\nTest Failed!"); + fail(sw.toString()); + } + assertFalse("Observer should not exist", hasAppUsageLimitObserver(UID, OBS_ID1)); + } + /** Re-adding an observer should result in only one copy */ @Test public void testAppUsageObserver_ObserverReAdd() { @@ -242,22 +287,39 @@ public class AppTimeLimitControllerTests { assertFalse("Observer wasn't removed", hasUsageSessionObserver(UID, OBS_ID1)); } + /** Re-adding an observer should result in only one copy */ + @Test + public void testAppUsageLimitObserver_ObserverReAdd() { + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + assertTrue("Observer wasn't added", hasAppUsageLimitObserver(UID, OBS_ID1)); + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_10_MIN); + assertTrue("Observer wasn't added", + getAppUsageLimitObserver(UID, OBS_ID1).getTimeLimitMs() == TIME_10_MIN); + mController.removeAppUsageLimitObserver(UID, OBS_ID1, USER_ID); + assertFalse("Observer wasn't removed", hasAppUsageLimitObserver(UID, OBS_ID1)); + } + /** Different type observers can be registered to the same observerId value */ @Test public void testAllObservers_ExclusiveObserverIds() { addAppUsageObserver(OBS_ID1, GROUP1, TIME_10_MIN); addUsageSessionObserver(OBS_ID1, GROUP1, TIME_30_MIN, TIME_1_MIN); + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_10_MIN); assertTrue("Observer wasn't added", hasAppUsageObserver(UID, OBS_ID1)); assertTrue("Observer wasn't added", hasUsageSessionObserver(UID, OBS_ID1)); + assertTrue("Observer wasn't added", hasAppUsageLimitObserver(UID, OBS_ID1)); AppTimeLimitController.UsageGroup appUsageGroup = mController.getAppUsageGroup(UID, OBS_ID1); AppTimeLimitController.UsageGroup sessionUsageGroup = mController.getSessionUsageGroup(UID, OBS_ID1); + AppTimeLimitController.UsageGroup appUsageLimitGroup = getAppUsageLimitObserver( + UID, OBS_ID1); // Verify data still intact assertEquals(TIME_10_MIN, appUsageGroup.getTimeLimitMs()); assertEquals(TIME_30_MIN, sessionUsageGroup.getTimeLimitMs()); + assertEquals(TIME_10_MIN, appUsageLimitGroup.getTimeLimitMs()); } /** Verify that usage across different apps within a group are added up */ @@ -299,7 +361,7 @@ public class AppTimeLimitControllerTests { @Test public void testUsageSessionObserver_Accumulation() throws Exception { setTime(0L); - addUsageSessionObserver(OBS_ID1, GROUP1, TIME_30_MIN, TIME_1_MIN); + addUsageSessionObserver(OBS_ID1, GROUP1, TIME_30_MIN, TIME_10_MIN); startUsage(PKG_SOC1); // Add 10 mins setTime(TIME_10_MIN); @@ -330,6 +392,41 @@ public class AppTimeLimitControllerTests { assertTrue(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS)); } + /** Verify that usage across different apps within a group are added up */ + @Test + public void testAppUsageLimitObserver_Accumulation() throws Exception { + setTime(0L); + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + startUsage(PKG_SOC1); + // Add 10 mins + setTime(TIME_10_MIN); + stopUsage(PKG_SOC1); + + AppTimeLimitController.UsageGroup group = getAppUsageLimitObserver(UID, OBS_ID1); + + long timeRemaining = group.getTimeLimitMs() - group.getUsageTimeMs(); + assertEquals(TIME_10_MIN * 2, timeRemaining); + + startUsage(PKG_SOC1); + setTime(TIME_10_MIN * 2); + stopUsage(PKG_SOC1); + + timeRemaining = group.getTimeLimitMs() - group.getUsageTimeMs(); + assertEquals(TIME_10_MIN, timeRemaining); + + setTime(TIME_30_MIN); + + assertFalse(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS)); + + // Add a different package in the group + startUsage(PKG_GAME1); + setTime(TIME_30_MIN + TIME_10_MIN); + stopUsage(PKG_GAME1); + + assertEquals(0, group.getTimeLimitMs() - group.getUsageTimeMs()); + assertTrue(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS)); + } + /** Verify that time limit does not get triggered due to a different app */ @Test public void testAppUsageObserver_TimeoutOtherApp() throws Exception { @@ -355,6 +452,18 @@ public class AppTimeLimitControllerTests { } + /** Verify that time limit does not get triggered due to a different app */ + @Test + public void testAppUsageLimitObserver_TimeoutOtherApp() throws Exception { + setTime(0L); + addAppUsageLimitObserver(OBS_ID1, GROUP1, 4_000L); + startUsage(PKG_SOC2); + assertFalse(mLimitReachedLatch.await(6_000L, TimeUnit.MILLISECONDS)); + setTime(6_000L); + stopUsage(PKG_SOC2); + assertFalse(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS)); + } + /** Verify the timeout message is delivered at the right time */ @Test public void testAppUsageObserver_Timeout() throws Exception { @@ -385,6 +494,19 @@ public class AppTimeLimitControllerTests { assertTrue(hasUsageSessionObserver(UID, OBS_ID1)); } + /** Verify the timeout message is delivered at the right time */ + @Test + public void testAppUsageLimitObserver_Timeout() throws Exception { + setTime(0L); + addAppUsageLimitObserver(OBS_ID1, GROUP1, 4_000L); + startUsage(PKG_SOC1); + setTime(6_000L); + assertTrue(mLimitReachedLatch.await(6_000L, TimeUnit.MILLISECONDS)); + stopUsage(PKG_SOC1); + // Verify that the observer was not removed + assertTrue(hasAppUsageLimitObserver(UID, OBS_ID1)); + } + /** If an app was already running, make sure it is partially counted towards the time limit */ @Test public void testAppUsageObserver_AlreadyRunning() throws Exception { @@ -423,6 +545,25 @@ public class AppTimeLimitControllerTests { assertTrue(hasUsageSessionObserver(UID, OBS_ID2)); } + /** If an app was already running, make sure it is partially counted towards the time limit */ + @Test + public void testAppUsageLimitObserver_AlreadyRunning() throws Exception { + setTime(TIME_10_MIN); + startUsage(PKG_GAME1); + setTime(TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN); + setTime(TIME_30_MIN + TIME_10_MIN); + stopUsage(PKG_GAME1); + assertFalse(mLimitReachedLatch.await(1_000L, TimeUnit.MILLISECONDS)); + + startUsage(PKG_GAME2); + setTime(TIME_30_MIN + TIME_30_MIN); + stopUsage(PKG_GAME2); + assertTrue(mLimitReachedLatch.await(1_000L, TimeUnit.MILLISECONDS)); + // Verify that the observer was not removed + assertTrue(hasAppUsageLimitObserver(UID, OBS_ID2)); + } + /** If watched app is already running, verify the timeout callback happens at the right time */ @Test public void testAppUsageObserver_AlreadyRunningTimeout() throws Exception { @@ -464,6 +605,24 @@ public class AppTimeLimitControllerTests { assertTrue(hasUsageSessionObserver(UID, OBS_ID1)); } + /** If watched app is already running, verify the timeout callback happens at the right time */ + @Test + public void testAppUsageLimitObserver_AlreadyRunningTimeout() throws Exception { + setTime(0); + startUsage(PKG_SOC1); + setTime(TIME_10_MIN); + // 10 second time limit + addAppUsageLimitObserver(OBS_ID1, GROUP_SOC, 10_000L); + setTime(TIME_10_MIN + 5_000L); + // Shouldn't call back in 6 seconds + assertFalse(mLimitReachedLatch.await(6_000L, TimeUnit.MILLISECONDS)); + setTime(TIME_10_MIN + 10_000L); + // Should call back by 11 seconds (6 earlier + 5 now) + assertTrue(mLimitReachedLatch.await(5_000L, TimeUnit.MILLISECONDS)); + // Verify that the observer was not removed + assertTrue(hasAppUsageLimitObserver(UID, OBS_ID1)); + } + /** * Verify that App Time Limit Controller will limit the number of observerIds for app usage * observers @@ -525,6 +684,37 @@ public class AppTimeLimitControllerTests { assertTrue("Should have caused an IllegalStateException", receivedException); } + /** + * Verify that App Time Limit Controller will limit the number of observerIds for app usage + * limit observers + */ + @Test + public void testAppUsageLimitObserver_MaxObserverLimit() throws Exception { + boolean receivedException = false; + int ANOTHER_UID = UID + 1; + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID2, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID3, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID4, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID5, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID6, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID7, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID8, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID9, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID10, GROUP1, TIME_30_MIN); + // Readding an observer should not cause an IllegalStateException + addAppUsageLimitObserver(OBS_ID5, GROUP1, TIME_30_MIN); + // Adding an observer for a different uid shouldn't cause an IllegalStateException + mController.addAppUsageLimitObserver( + ANOTHER_UID, OBS_ID11, GROUP1, TIME_30_MIN, null, USER_ID); + try { + addAppUsageLimitObserver(OBS_ID11, GROUP1, TIME_30_MIN); + } catch (IllegalStateException ise) { + receivedException = true; + } + assertTrue("Should have caused an IllegalStateException", receivedException); + } + /** Verify that addAppUsageObserver minimum time limit is one minute */ @Test public void testAppUsageObserver_MinimumTimeLimit() throws Exception { @@ -553,6 +743,20 @@ public class AppTimeLimitControllerTests { assertTrue("Should have caused an IllegalArgumentException", receivedException); } + /** Verify that addAppUsageLimitObserver minimum time limit is one minute */ + @Test + public void testAppUsageLimitObserver_MinimumTimeLimit() throws Exception { + boolean receivedException = false; + // adding an observer with a one minute time limit should not cause an exception + addAppUsageLimitObserver(OBS_ID1, GROUP1, MIN_TIME_LIMIT); + try { + addAppUsageLimitObserver(OBS_ID1, GROUP1, MIN_TIME_LIMIT - 1); + } catch (IllegalArgumentException iae) { + receivedException = true; + } + assertTrue("Should have caused an IllegalArgumentException", receivedException); + } + /** Verify that concurrent usage from multiple apps in the same group will counted correctly */ @Test public void testAppUsageObserver_ConcurrentUsage() throws Exception { @@ -599,6 +803,29 @@ public class AppTimeLimitControllerTests { assertTrue(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS)); } + /** Verify that concurrent usage from multiple apps in the same group will counted correctly */ + @Test + public void testAppUsageLimitObserver_ConcurrentUsage() throws Exception { + setTime(0L); + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + AppTimeLimitController.UsageGroup group = getAppUsageLimitObserver(UID, OBS_ID1); + startUsage(PKG_SOC1); + // Add 10 mins + setTime(TIME_10_MIN); + + // Add a different package in the group will first package is still in use + startUsage(PKG_GAME1); + setTime(TIME_10_MIN * 2); + // Stop first package usage + stopUsage(PKG_SOC1); + + setTime(TIME_30_MIN); + stopUsage(PKG_GAME1); + + assertEquals(TIME_30_MIN, group.getUsageTimeMs()); + assertTrue(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS)); + } + /** Verify that a session will continue if usage starts again within the session threshold */ @Test public void testUsageSessionObserver_ContinueSession() throws Exception { @@ -737,6 +964,97 @@ public class AppTimeLimitControllerTests { assertFalse(hasAppUsageObserver(UID, OBS_ID1)); } + /** Verify app usage limit observer added correctly reports it being a group limit */ + @Test + public void testAppUsageLimitObserver_IsGroupLimit() { + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + AppTimeLimitController.AppUsageLimitGroup group = getAppUsageLimitObserver(UID, OBS_ID1); + assertNotNull("Observer wasn't added", group); + assertTrue("Observer didn't correctly report being a group limit", + group.isGroupLimit()); + } + + /** Verify app usage limit observer added correctly reports it being not a group limit */ + @Test + public void testAppUsageLimitObserver_IsNotGroupLimit() { + addAppUsageLimitObserver(OBS_ID1, new String[]{PKG_PROD}, TIME_30_MIN); + AppTimeLimitController.AppUsageLimitGroup group = getAppUsageLimitObserver(UID, OBS_ID1); + assertNotNull("Observer wasn't added", group); + assertFalse("Observer didn't correctly report not being a group limit", + group.isGroupLimit()); + } + + /** Verify app usage limit observer added correctly reports its total usage limit */ + @Test + public void testAppUsageLimitObserver_GetTotalUsageLimit() { + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + AppTimeLimitController.AppUsageLimitGroup group = getAppUsageLimitObserver(UID, OBS_ID1); + assertNotNull("Observer wasn't added", group); + assertEquals("Observer didn't correctly report total usage limit", + TIME_30_MIN, group.getTotaUsageLimit()); + } + + /** Verify app usage limit observer added correctly reports its total usage limit */ + @Test + public void testAppUsageLimitObserver_GetUsageRemaining() { + setTime(0L); + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + startUsage(PKG_SOC1); + setTime(TIME_10_MIN); + stopUsage(PKG_SOC1); + AppTimeLimitController.AppUsageLimitGroup group = getAppUsageLimitObserver(UID, OBS_ID1); + assertNotNull("Observer wasn't added", group); + assertEquals("Observer didn't correctly report total usage limit", + TIME_10_MIN * 2, group.getUsageRemaining()); + } + + /** Verify the app usage limit observer with the smallest usage limit remaining is returned + * when querying the getAppUsageLimit API. + */ + @Test + public void testAppUsageLimitObserver_GetAppUsageLimit() { + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID2, GROUP_SOC, TIME_10_MIN); + UsageStatsManagerInternal.AppUsageLimitData group = getAppUsageLimit(PKG_SOC1); + assertEquals("Observer with the smallest usage limit remaining wasn't returned", + TIME_10_MIN, group.getTotalUsageLimit()); + } + + /** Verify the app usage limit observer with the smallest usage limit remaining is returned + * when querying the getAppUsageLimit API. + */ + @Test + public void testAppUsageLimitObserver_GetAppUsageLimitUsed() { + setTime(0L); + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID2, GROUP_SOC, TIME_10_MIN); + startUsage(PKG_GAME1); + setTime(TIME_10_MIN * 2 + TIME_1_MIN); + stopUsage(PKG_GAME1); + // PKG_GAME1 is only in GROUP1 but since we're querying for PCK_SOC1 which is + // in both groups, GROUP1 should be returned since it has a smaller time remaining + UsageStatsManagerInternal.AppUsageLimitData group = getAppUsageLimit(PKG_SOC1); + assertEquals("Observer with the smallest usage limit remaining wasn't returned", + TIME_1_MIN * 9, group.getUsageRemaining()); + } + + /** Verify the app usage limit observer with the smallest usage limit remaining is returned + * when querying the getAppUsageLimit API. + */ + @Test + public void testAppUsageLimitObserver_GetAppUsageLimitAllUsed() { + setTime(0L); + addAppUsageLimitObserver(OBS_ID1, GROUP1, TIME_30_MIN); + addAppUsageLimitObserver(OBS_ID2, GROUP_SOC, TIME_10_MIN); + startUsage(PKG_SOC1); + setTime(TIME_10_MIN); + stopUsage(PKG_SOC1); + // GROUP_SOC should be returned since it should be completely used up (0ms remaining) + UsageStatsManagerInternal.AppUsageLimitData group = getAppUsageLimit(PKG_SOC1); + assertEquals("Observer with the smallest usage limit remaining wasn't returned", + 0L, group.getUsageRemaining()); + } + private void startUsage(String packageName) { mController.noteUsageStart(packageName, USER_ID); } @@ -759,6 +1077,10 @@ public class AppTimeLimitControllerTests { null, null, USER_ID); } + private void addAppUsageLimitObserver(int observerId, String[] packages, long timeLimit) { + mController.addAppUsageLimitObserver(UID, observerId, packages, timeLimit, null, USER_ID); + } + /** Is there still an app usage observer by that id */ private boolean hasAppUsageObserver(int uid, int observerId) { return mController.getAppUsageGroup(uid, observerId) != null; @@ -769,6 +1091,20 @@ public class AppTimeLimitControllerTests { return mController.getSessionUsageGroup(uid, observerId) != null; } + /** Is there still an app usage limit observer by that id */ + private boolean hasAppUsageLimitObserver(int uid, int observerId) { + return mController.getAppUsageLimitGroup(uid, observerId) != null; + } + + private AppTimeLimitController.AppUsageLimitGroup getAppUsageLimitObserver( + int uid, int observerId) { + return mController.getAppUsageLimitGroup(uid, observerId); + } + + private UsageStatsManagerInternal.AppUsageLimitData getAppUsageLimit(String packageName) { + return mController.getAppUsageLimit(packageName, UserHandle.of(USER_ID)); + } + private void setTime(long time) { mUptimeMillis = time; } diff --git a/services/usage/java/com/android/server/usage/AppTimeLimitController.java b/services/usage/java/com/android/server/usage/AppTimeLimitController.java index 2ed11fe92e15..fa472e2575f0 100644 --- a/services/usage/java/com/android/server/usage/AppTimeLimitController.java +++ b/services/usage/java/com/android/server/usage/AppTimeLimitController.java @@ -18,11 +18,14 @@ package com.android.server.usage; import android.annotation.UserIdInt; import android.app.PendingIntent; +import android.app.usage.UsageStatsManagerInternal; 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; @@ -163,6 +166,9 @@ public class AppTimeLimitController { /** Map of observerId to details of the time limit group */ SparseArray<SessionUsageGroup> sessionUsageGroups = new SparseArray<>(); + /** Map of observerId to details of the app usage limit group */ + SparseArray<AppUsageLimitGroup> appUsageLimitGroups = new SparseArray<>(); + private ObserverAppData(int uid) { this.uid = uid; } @@ -177,6 +183,10 @@ public class AppTimeLimitController { sessionUsageGroups.remove(observerId); } + @GuardedBy("mLock") + void removeAppUsageLimitGroup(int observerId) { + appUsageLimitGroups.remove(observerId); + } @GuardedBy("mLock") void dump(PrintWriter pw) { @@ -194,6 +204,12 @@ public class AppTimeLimitController { 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(); + } } } @@ -493,6 +509,54 @@ public class AppTimeLimitController { } } + class AppUsageLimitGroup extends UsageGroup { + private boolean mGroupLimit; + + public AppUsageLimitGroup(UserData user, ObserverAppData observerApp, int observerId, + String[] observed, long timeLimitMs, PendingIntent limitReachedCallback) { + super(user, observerApp, observerId, observed, timeLimitMs, limitReachedCallback); + mGroupLimit = observed.length > 1; + } + + @Override + @GuardedBy("mLock") + public void remove() { + super.remove(); + ObserverAppData observerApp = mObserverAppRef.get(); + if (observerApp != null) { + observerApp.removeAppUsageLimitGroup(mObserverId); + } + } + + @GuardedBy("mLock") + boolean isGroupLimit() { + return mGroupLimit; + } + + @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 - (getUptimeMillis() - mLastKnownUsageTimeMs); + } else { + return mTimeLimitMs - mUsageTimeMs; + } + } + + @Override + @GuardedBy("mLock") + void dump(PrintWriter pw) { + super.dump(pw); + pw.print(" groupLimit="); + pw.print(mGroupLimit); + } + } + private class MyHandler extends Handler { static final int MSG_CHECK_TIMEOUT = 1; @@ -553,6 +617,12 @@ public class AppTimeLimitController { /** Overrideable for testing purposes */ @VisibleForTesting + protected long getAppUsageLimitObserverPerUidLimit() { + return MAX_OBSERVER_PER_UID; + } + + /** Overrideable for testing purposes */ + @VisibleForTesting protected long getMinTimeLimit() { return ONE_MINUTE; } @@ -572,6 +642,61 @@ public class AppTimeLimitController { } } + @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<UsageGroup> usageGroups = userData.observedMap.get(packageName); + if (usageGroups == null || usageGroups.isEmpty()) { + return null; + } + + final ArraySet<AppUsageLimitGroup> 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.isGroupLimit(), + smallestGroup.getTotaUsageLimit(), smallestGroup.getUsageRemaining()); + } + } + /** Returns an existing UserData object for the given userId, or creates one */ @GuardedBy("mLock") private UserData getOrCreateUserDataLocked(int userId) { @@ -726,6 +851,61 @@ public class AppTimeLimitController { } /** + * 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, 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, + callbackIntent); + observerApp.appUsageLimitGroups.append(observerId, group); + + if (DEBUG) { + Slog.d(TAG, "addObserver " + observed + " for " + timeLimit); + } + + user.addUsageGroup(group); + noteActiveLocked(user, group, getUptimeMillis()); + } + } + + /** + * 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 diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 6ad698b39763..85939d498755 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -855,6 +855,22 @@ public class UsageStatsService extends SystemService implements == PackageManager.PERMISSION_GRANTED; } + private boolean hasPermissions(String callingPackage, String... permissions) { + final int callingUid = Binder.getCallingUid(); + if (callingUid == Process.SYSTEM_UID) { + // Caller is the system, so proceed. + return true; + } + + boolean hasPermissions = true; + final Context context = getContext(); + for (int i = 0; i < permissions.length; i++) { + hasPermissions = hasPermissions && (context.checkCallingPermission(permissions[i]) + == PackageManager.PERMISSION_GRANTED); + } + return hasPermissions; + } + private void checkCallerIsSystemOrSameApp(String pkg) { if (isCallingUidSystem()) { return; @@ -1346,6 +1362,51 @@ public class UsageStatsService extends SystemService implements } @Override + public void registerAppUsageLimitObserver(int observerId, String[] packages, + long timeLimitMs, PendingIntent callbackIntent, String callingPackage) { + if (!hasPermissions(callingPackage, + Manifest.permission.SUSPEND_APPS, Manifest.permission.OBSERVE_APP_USAGE)) { + throw new SecurityException("Caller doesn't have both SUSPEND_APPS and " + + "OBSERVE_APP_USAGE permissions"); + } + + if (packages == null || packages.length == 0) { + throw new IllegalArgumentException("Must specify at least one package"); + } + if (callbackIntent == null) { + throw new NullPointerException("callbackIntent can't be null"); + } + final int callingUid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(callingUid); + final long token = Binder.clearCallingIdentity(); + try { + UsageStatsService.this.registerAppUsageLimitObserver(callingUid, observerId, + packages, timeLimitMs, callbackIntent, userId); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void unregisterAppUsageLimitObserver(int observerId, String callingPackage) { + if (!hasPermissions(callingPackage, + Manifest.permission.SUSPEND_APPS, Manifest.permission.OBSERVE_APP_USAGE)) { + throw new SecurityException("Caller doesn't have both SUSPEND_APPS and " + + "OBSERVE_APP_USAGE permissions"); + } + + final int callingUid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(callingUid); + final long token = Binder.clearCallingIdentity(); + try { + UsageStatsService.this.unregisterAppUsageLimitObserver( + callingUid, observerId, userId); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override public void reportUsageStart(IBinder activity, String token, String callingPackage) { reportPastUsageStart(activity, token, 0, callingPackage); } @@ -1447,6 +1508,16 @@ public class UsageStatsService extends SystemService implements mAppTimeLimit.removeUsageSessionObserver(callingUid, sessionObserverId, userId); } + void registerAppUsageLimitObserver(int callingUid, int observerId, String[] packages, + long timeLimitMs, PendingIntent callbackIntent, int userId) { + mAppTimeLimit.addAppUsageLimitObserver(callingUid, observerId, packages, timeLimitMs, + callbackIntent, userId); + } + + void unregisterAppUsageLimitObserver(int callingUid, int observerId, int userId) { + mAppTimeLimit.removeAppUsageLimitObserver(callingUid, observerId, userId); + } + /** * This local service implementation is primarily used by ActivityManagerService. * ActivityManagerService will call these methods holding the 'am' lock, which means we @@ -1652,5 +1723,10 @@ public class UsageStatsService extends SystemService implements public void reportExemptedSyncStart(String packageName, int userId) { mAppStandby.postReportExemptedSyncStart(packageName, userId); } + + @Override + public AppUsageLimitData getAppUsageLimit(String packageName, UserHandle user) { + return mAppTimeLimit.getAppUsageLimit(packageName, user); + } } } diff --git a/tests/UsageStatsTest/AndroidManifest.xml b/tests/UsageStatsTest/AndroidManifest.xml index 4b1c1bd69920..fefd99394a87 100644 --- a/tests/UsageStatsTest/AndroidManifest.xml +++ b/tests/UsageStatsTest/AndroidManifest.xml @@ -11,6 +11,7 @@ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> <uses-permission android:name="android.permission.OBSERVE_APP_USAGE" /> + <uses-permission android:name="android.permission.SUSPEND_APPS" /> <application android:label="Usage Access Test"> <activity android:name=".UsageStatsActivity" diff --git a/tests/UsageStatsTest/res/menu/main.xml b/tests/UsageStatsTest/res/menu/main.xml index 612267c85b1b..272e0f4e1f54 100644 --- a/tests/UsageStatsTest/res/menu/main.xml +++ b/tests/UsageStatsTest/res/menu/main.xml @@ -6,4 +6,6 @@ android:title="Call isAppInactive()"/> <item android:id="@+id/set_app_limit" android:title="Set App Limit" /> + <item android:id="@+id/set_app_usage_limit" + android:title="Set App Usage Limit" /> </menu> diff --git a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java index 3c628f6e0013..0105893adf9e 100644 --- a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java +++ b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java @@ -21,6 +21,8 @@ import android.app.ListActivity; import android.app.PendingIntent; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -49,6 +51,8 @@ public class UsageStatsActivity extends ListActivity { private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14; private static final String EXTRA_KEY_TIMEOUT = "com.android.tests.usagestats.extra.TIMEOUT"; private UsageStatsManager mUsageStatsManager; + private ClipboardManager mClipboard; + private ClipData mClip; private Adapter mAdapter; private Comparator<UsageStats> mComparator = new Comparator<UsageStats>() { @Override @@ -61,6 +65,7 @@ public class UsageStatsActivity extends ListActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); + mClipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); mAdapter = new Adapter(); setListAdapter(mAdapter); Bundle extras = getIntent().getExtras(); @@ -98,6 +103,8 @@ public class UsageStatsActivity extends ListActivity { case R.id.set_app_limit: callSetAppLimit(); return true; + case R.id.set_app_usage_limit: + callSetAppUsageLimit(); default: return super.onOptionsItemSelected(item); } @@ -170,6 +177,40 @@ public class UsageStatsActivity extends ListActivity { builder.show(); } + private void callSetAppUsageLimit() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Enter package name"); + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setHint("com.android.tests.usagestats"); + builder.setView(input); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final String packageName = input.getText().toString().trim(); + if (!TextUtils.isEmpty(packageName)) { + String[] packages = packageName.split(","); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(UsageStatsActivity.this, UsageStatsActivity.class); + intent.setPackage(getPackageName()); + intent.putExtra(EXTRA_KEY_TIMEOUT, true); + mUsageStatsManager.registerAppUsageLimitObserver(1, packages, + 60, TimeUnit.SECONDS, PendingIntent.getActivity(UsageStatsActivity.this, + 1, intent, 0)); + } + } + }); + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + private void showInactive(String packageName) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage( @@ -232,6 +273,21 @@ public class UsageStatsActivity extends ListActivity { holder.packageName.setText(mStats.get(position).getPackageName()); holder.usageTime.setText(DateUtils.formatDuration( mStats.get(position).getTotalTimeInForeground())); + + //copy package name to the clipboard for convenience + holder.packageName.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + String text = holder.packageName.getText().toString(); + mClip = ClipData.newPlainText("package_name", text); + mClipboard.setPrimaryClip(mClip); + + Toast.makeText(getApplicationContext(), "package name copied to clipboard", + Toast.LENGTH_SHORT).show(); + return true; + } + }); + return convertView; } } |