diff options
author | Varun Shah <varunshah@google.com> | 2019-01-11 15:50:54 -0800 |
---|---|---|
committer | Varun Shah <varunshah@google.com> | 2019-01-23 20:30:25 -0800 |
commit | 2546cef56cdf8767c4bb600251aed8c15dd6a7ae (patch) | |
tree | e7e029bf221c31709f9f24791a4565c527051dda | |
parent | 203445c85380a750a10cbbd2a57a0d87f382922e (diff) |
Added APIs for App Usage Limits.
Added a new AppUsageLimit group observer which follows the same pattern as
other UsageGroups. This specific observer allows the launcher to query
for the AppUsageLimit, available via the new LauncherApps API below. The
observer can be registered and unregistered via the respective new APIs in
UsageStats.
LauncherApps has a new API which allows it to get the AppUsageLimit for
a specified package and user, initally set via the API in UsageStats.
This new API allows the launcher to query specifics about the limit such
as how much usage time the limit has, and how much total usage time is
remaining.
Bug: 117409586
Test: atest FrameworksServicesTests:AppTimeLimitControllerTests
Test: atest android.app.usage.cts.UsageStatsTest#testObserveUsagePermissionForRegisterObserver
Test: atest android.app.usage.cts.UsageStatsTest#testObserveUsagePermissionForUnregisterObserver
Test: manual (mmma frameworks/base/tests/UsageStatsTest/)
Change-Id: Ifaffab629409e9191e40404a949c8df70bd3f7cb
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; } } |