diff --git a/core/api/system-current.txt b/core/api/system-current.txt index fb8e0c7a57350..d0df0ae6515d8 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -83,6 +83,7 @@ package android { field public static final String CAPTURE_TV_INPUT = "android.permission.CAPTURE_TV_INPUT"; field public static final String CAPTURE_VOICE_COMMUNICATION_OUTPUT = "android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT"; field public static final String CHANGE_APP_IDLE_STATE = "android.permission.CHANGE_APP_IDLE_STATE"; + field public static final String CHANGE_APP_LAUNCH_TIME_ESTIMATE = "android.permission.CHANGE_APP_LAUNCH_TIME_ESTIMATE"; field public static final String CHANGE_DEVICE_IDLE_TEMP_WHITELIST = "android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST"; field public static final String CLEAR_APP_USER_DATA = "android.permission.CLEAR_APP_USER_DATA"; field public static final String COMPANION_APPROVE_WIFI_CONNECTIONS = "android.permission.COMPANION_APPROVE_WIFI_CONNECTIONS"; @@ -1923,6 +1924,8 @@ 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); + method @RequiresPermission(android.Manifest.permission.CHANGE_APP_LAUNCH_TIME_ESTIMATE) public void setEstimatedLaunchTime(@NonNull String, long); + method @RequiresPermission(android.Manifest.permission.CHANGE_APP_LAUNCH_TIME_ESTIMATE) public void setEstimatedLaunchTimes(@NonNull java.util.Map); 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); diff --git a/core/java/android/app/usage/AppLaunchEstimateInfo.java b/core/java/android/app/usage/AppLaunchEstimateInfo.java new file mode 100644 index 0000000000000..2085d00194436 --- /dev/null +++ b/core/java/android/app/usage/AppLaunchEstimateInfo.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 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.app.usage; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A pair of {package, estimated launch time} to denote the estimated launch time for a given + * package. + * Used as a vehicle of data across the binder IPC. + * + * @hide + */ +public final class AppLaunchEstimateInfo implements Parcelable { + + public final String packageName; + @CurrentTimeMillisLong + public final long estimatedLaunchTime; + + private AppLaunchEstimateInfo(Parcel in) { + packageName = in.readString(); + estimatedLaunchTime = in.readLong(); + } + + public AppLaunchEstimateInfo(String packageName, + @CurrentTimeMillisLong long estimatedLaunchTime) { + this.packageName = packageName; + this.estimatedLaunchTime = estimatedLaunchTime; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(packageName); + dest.writeLong(estimatedLaunchTime); + } + + @NonNull + public static final Creator CREATOR = + new Creator() { + @Override + public AppLaunchEstimateInfo createFromParcel(Parcel source) { + return new AppLaunchEstimateInfo(source); + } + + @Override + public AppLaunchEstimateInfo[] newArray(int size) { + return new AppLaunchEstimateInfo[size]; + } + }; +} diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl index 585eb61b6f573..170d766c794c7 100644 --- a/core/java/android/app/usage/IUsageStatsManager.aidl +++ b/core/java/android/app/usage/IUsageStatsManager.aidl @@ -51,6 +51,8 @@ interface IUsageStatsManager { void setAppStandbyBucket(String packageName, int bucket, int userId); ParceledListSlice getAppStandbyBuckets(String callingPackage, int userId); void setAppStandbyBuckets(in ParceledListSlice appBuckets, int userId); + void setEstimatedLaunchTime(String packageName, long estimatedLaunchTime, int userId); + void setEstimatedLaunchTimes(in ParceledListSlice appLaunchTimes, int userId); void registerAppUsageObserver(int observerId, in String[] packages, long timeLimitMs, in PendingIntent callback, String callingPackage); void unregisterAppUsageObserver(int observerId, String callingPackage); diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index ac7a318746828..3cbb24b586b79 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -16,6 +16,7 @@ package android.app.usage; +import android.annotation.CurrentTimeMillisLong; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -771,6 +772,72 @@ public final class UsageStatsManager { } } + /** + * Changes an app's estimated launch time. An app is considered "launched" when a user opens + * one of its {@link android.app.Activity Activities}. The provided time is persisted across + * reboots and is used unless 1) the time is more than a week in the future and the platform + * thinks the app will be launched sooner, 2) the estimated time has passed. Passing in + * {@link Long#MAX_VALUE} effectively clears the previously set launch time for the app. + * + * @param packageName The package name of the app to set the bucket for. + * @param estimatedLaunchTime The next time the app is expected to be launched. Units are in + * milliseconds since epoch (the same as + * {@link System#currentTimeMillis()}). + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.CHANGE_APP_LAUNCH_TIME_ESTIMATE) + public void setEstimatedLaunchTime(@NonNull String packageName, + @CurrentTimeMillisLong long estimatedLaunchTime) { + if (packageName == null) { + throw new NullPointerException("package name cannot be null"); + } + if (estimatedLaunchTime <= 0) { + throw new IllegalArgumentException("estimated launch time must be positive"); + } + try { + mService.setEstimatedLaunchTime(packageName, estimatedLaunchTime, mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Changes the estimated launch times for multiple apps at once. The map is keyed by the + * package name and the value is the estimated launch time. + * + * @param estimatedLaunchTimes A map of package name to estimated launch time. + * @see #setEstimatedLaunchTime(String, long) + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.CHANGE_APP_LAUNCH_TIME_ESTIMATE) + public void setEstimatedLaunchTimes(@NonNull Map estimatedLaunchTimes) { + if (estimatedLaunchTimes == null) { + throw new NullPointerException("estimatedLaunchTimes cannot be null"); + } + final List estimateList = + new ArrayList<>(estimatedLaunchTimes.size()); + for (Map.Entry estimateEntry : estimatedLaunchTimes.entrySet()) { + final String pkgName = estimateEntry.getKey(); + if (pkgName == null) { + throw new NullPointerException("package name cannot be null"); + } + final Long estimatedLaunchTime = estimateEntry.getValue(); + if (estimatedLaunchTime == null || estimatedLaunchTime <= 0) { + throw new IllegalArgumentException("estimated launch time must be positive"); + } + estimateList.add(new AppLaunchEstimateInfo(pkgName, estimatedLaunchTime)); + } + final ParceledListSlice slice = + new ParceledListSlice<>(estimateList); + try { + mService.setEstimatedLaunchTimes(slice, mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * @hide * Register an app usage limit observer that receives a callback on the provided intent when diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index aada7eb2df8f4..316ea3455c69d 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4880,6 +4880,11 @@ + + + diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index a32bd2dadc187..997c8832f4a9a 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -41,6 +41,7 @@ import android.app.AppOpsManager; import android.app.IUidObserver; import android.app.PendingIntent; import android.app.admin.DevicePolicyManagerInternal; +import android.app.usage.AppLaunchEstimateInfo; import android.app.usage.AppStandbyInfo; import android.app.usage.ConfigurationStats; import android.app.usage.EventStats; @@ -1554,6 +1555,52 @@ public class UsageStatsService extends SystemService implements } } + private void setEstimatedLaunchTime(int userId, String packageName, + @CurrentTimeMillisLong long estimatedLaunchTime) { + final long now = System.currentTimeMillis(); + if (estimatedLaunchTime <= now) { + if (DEBUG) { + Slog.w(TAG, "Ignoring new estimate for " + + userId + ":" + packageName + " because it's old"); + } + return; + } + final long oldEstimatedLaunchTime = mAppStandby.getEstimatedLaunchTime(packageName, userId); + if (estimatedLaunchTime != oldEstimatedLaunchTime) { + mAppStandby.setEstimatedLaunchTime(packageName, userId, estimatedLaunchTime); + mHandler.obtainMessage( + MSG_NOTIFY_ESTIMATED_LAUNCH_TIME_CHANGED, userId, 0, packageName) + .sendToTarget(); + } + } + + private void setEstimatedLaunchTimes(int userId, List launchEstimates) { + final ArraySet changedTimes = new ArraySet<>(); + final long now = System.currentTimeMillis(); + for (int i = launchEstimates.size() - 1; i >= 0; --i) { + AppLaunchEstimateInfo estimate = launchEstimates.get(i); + if (estimate.estimatedLaunchTime <= now) { + if (DEBUG) { + Slog.w(TAG, "Ignoring new estimate for " + + userId + ":" + estimate.packageName + " because it's old"); + } + continue; + } + final long oldEstimatedLaunchTime = + mAppStandby.getEstimatedLaunchTime(estimate.packageName, userId); + if (estimate.estimatedLaunchTime != oldEstimatedLaunchTime) { + mAppStandby.setEstimatedLaunchTime( + estimate.packageName, userId, estimate.estimatedLaunchTime); + changedTimes.add(estimate.packageName); + } + } + if (changedTimes.size() > 0) { + mHandler.obtainMessage( + MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED, userId, 0, changedTimes) + .sendToTarget(); + } + } + /** * Called via the local interface. */ @@ -2292,6 +2339,37 @@ public class UsageStatsService extends SystemService implements } } + @Override + public void setEstimatedLaunchTime(String packageName, long estimatedLaunchTime, + int userId) { + getContext().enforceCallingPermission( + Manifest.permission.CHANGE_APP_LAUNCH_TIME_ESTIMATE, + "No permission to change app launch estimates"); + + final long token = Binder.clearCallingIdentity(); + try { + UsageStatsService.this + .setEstimatedLaunchTime(userId, packageName, estimatedLaunchTime); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void setEstimatedLaunchTimes(ParceledListSlice estimatedLaunchTimes, int userId) { + getContext().enforceCallingPermission( + Manifest.permission.CHANGE_APP_LAUNCH_TIME_ESTIMATE, + "No permission to change app launch estimates"); + + final long token = Binder.clearCallingIdentity(); + try { + UsageStatsService.this + .setEstimatedLaunchTimes(userId, estimatedLaunchTimes.getList()); + } finally { + Binder.restoreCallingIdentity(token); + } + } + @Override public void onCarrierPrivilegedAppsChanged() { if (DEBUG) {