blob: 24728dd8edca408338c9701d424679e2d7ff258b [file] [log] [blame]
/**
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.android.server.usage;
import static android.app.usage.UsageStatsManager.REASON_MAIN_DEFAULT;
import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_SYSTEM;
import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER;
import static android.app.usage.UsageStatsManager.REASON_MAIN_MASK;
import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED;
import static android.app.usage.UsageStatsManager.REASON_MAIN_TIMEOUT;
import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE;
import static android.app.usage.UsageStatsManager.REASON_SUB_DEFAULT_APP_UPDATE;
import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY;
import static android.app.usage.UsageStatsManager.REASON_SUB_MASK;
import static android.app.usage.UsageStatsManager.REASON_SUB_PREDICTED_RESTORED;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_ACTIVE_TIMEOUT;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_DOZE;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_EXEMPTED_SYNC_START;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_FOREGROUND_SERVICE_START;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_MOVE_TO_BACKGROUND;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_MOVE_TO_FOREGROUND;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_NOTIFICATION_SEEN;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SLICE_PINNED;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SLICE_PINNED_PRIV;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYNC_ADAPTER;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYSTEM_INTERACTION;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYSTEM_UPDATE;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_UNEXEMPTED_SYNC_SCHEDULED;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.usage.AppStandbyInfo;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManager.StandbyBuckets;
import android.app.usage.UsageStatsManager.SystemForcedReasons;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.CrossProfileAppsInternal;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
import android.database.ContentObserver;
import android.hardware.display.DisplayManager;
import android.net.NetworkScoreManager;
import android.os.BatteryManager;
import android.os.BatteryStats;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerWhitelistManager;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings.Global;
import android.telephony.TelephonyManager;
import android.util.ArraySet;
import android.util.KeyValueListParser;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.TimeUtils;
import android.view.Display;
import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import com.android.server.usage.AppIdleHistory.AppUsageHistory;
import java.io.File;
import java.io.PrintWriter;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
/**
* Manages the standby state of an app, listening to various events.
*
* Unit test:
atest com.android.server.usage.AppStandbyControllerTests
*/
public class AppStandbyController implements AppStandbyInternal {
private static final String TAG = "AppStandbyController";
// Do not submit with true.
static final boolean DEBUG = false;
static final boolean COMPRESS_TIME = false;
private static final long ONE_MINUTE = 60 * 1000;
private static final long ONE_HOUR = ONE_MINUTE * 60;
private static final long ONE_DAY = ONE_HOUR * 24;
/**
* The minimum amount of time the screen must have been on before an app can time out from its
* current bucket to the next bucket.
*/
private static final long[] SCREEN_TIME_THRESHOLDS = {
0,
0,
COMPRESS_TIME ? 2 * ONE_MINUTE : 1 * ONE_HOUR,
COMPRESS_TIME ? 4 * ONE_MINUTE : 2 * ONE_HOUR,
COMPRESS_TIME ? 8 * ONE_MINUTE : 6 * ONE_HOUR
};
/** The minimum allowed values for each index in {@link #SCREEN_TIME_THRESHOLDS}. */
private static final long[] MINIMUM_SCREEN_TIME_THRESHOLDS = COMPRESS_TIME
? new long[SCREEN_TIME_THRESHOLDS.length]
: new long[]{
0,
0,
0,
30 * ONE_MINUTE,
ONE_HOUR
};
/**
* The minimum amount of elapsed time that must have passed before an app can time out from its
* current bucket to the next bucket.
*/
private static final long[] ELAPSED_TIME_THRESHOLDS = {
0,
COMPRESS_TIME ? 1 * ONE_MINUTE : 12 * ONE_HOUR,
COMPRESS_TIME ? 4 * ONE_MINUTE : 24 * ONE_HOUR,
COMPRESS_TIME ? 16 * ONE_MINUTE : 48 * ONE_HOUR,
COMPRESS_TIME ? 32 * ONE_MINUTE : 30 * ONE_DAY
};
/** The minimum allowed values for each index in {@link #ELAPSED_TIME_THRESHOLDS}. */
private static final long[] MINIMUM_ELAPSED_TIME_THRESHOLDS = COMPRESS_TIME
? new long[ELAPSED_TIME_THRESHOLDS.length]
: new long[]{
0,
ONE_HOUR,
ONE_HOUR,
2 * ONE_HOUR,
4 * ONE_DAY
};
private static final int[] THRESHOLD_BUCKETS = {
STANDBY_BUCKET_ACTIVE,
STANDBY_BUCKET_WORKING_SET,
STANDBY_BUCKET_FREQUENT,
STANDBY_BUCKET_RARE,
STANDBY_BUCKET_RESTRICTED
};
/** Default expiration time for bucket prediction. After this, use thresholds to downgrade. */
private static final long DEFAULT_PREDICTION_TIMEOUT = 12 * ONE_HOUR;
/**
* Indicates the maximum wait time for admin data to be available;
*/
private static final long WAIT_FOR_ADMIN_DATA_TIMEOUT_MS = 10_000;
// To name the lock for stack traces
static class Lock {}
/** Lock to protect the app's standby state. Required for calls into AppIdleHistory */
private final Object mAppIdleLock = new Lock();
/** Keeps the history and state for each app. */
@GuardedBy("mAppIdleLock")
private AppIdleHistory mAppIdleHistory;
@GuardedBy("mPackageAccessListeners")
private ArrayList<AppIdleStateChangeListener>
mPackageAccessListeners = new ArrayList<>();
/** Whether we've queried the list of carrier privileged apps. */
@GuardedBy("mAppIdleLock")
private boolean mHaveCarrierPrivilegedApps;
/** List of carrier-privileged apps that should be excluded from standby */
@GuardedBy("mAppIdleLock")
private List<String> mCarrierPrivilegedApps;
@GuardedBy("mActiveAdminApps")
private final SparseArray<Set<String>> mActiveAdminApps = new SparseArray<>();
private final CountDownLatch mAdminDataAvailableLatch = new CountDownLatch(1);
// Messages for the handler
static final int MSG_INFORM_LISTENERS = 3;
static final int MSG_FORCE_IDLE_STATE = 4;
static final int MSG_CHECK_IDLE_STATES = 5;
static final int MSG_REPORT_CONTENT_PROVIDER_USAGE = 8;
static final int MSG_ONE_TIME_CHECK_IDLE_STATES = 10;
/** Check the state of one app: arg1 = userId, arg2 = uid, obj = (String) packageName */
static final int MSG_CHECK_PACKAGE_IDLE_STATE = 11;
static final int MSG_REPORT_SYNC_SCHEDULED = 12;
static final int MSG_REPORT_EXEMPTED_SYNC_START = 13;
long mCheckIdleIntervalMillis;
/**
* The minimum amount of time the screen must have been on before an app can time out from its
* current bucket to the next bucket.
*/
long[] mAppStandbyScreenThresholds = SCREEN_TIME_THRESHOLDS;
/**
* The minimum amount of elapsed time that must have passed before an app can time out from its
* current bucket to the next bucket.
*/
long[] mAppStandbyElapsedThresholds = ELAPSED_TIME_THRESHOLDS;
/** Minimum time a strong usage event should keep the bucket elevated. */
long mStrongUsageTimeoutMillis;
/** Minimum time a notification seen event should keep the bucket elevated. */
long mNotificationSeenTimeoutMillis;
/** Minimum time a system update event should keep the buckets elevated. */
long mSystemUpdateUsageTimeoutMillis;
/** Maximum time to wait for a prediction before using simple timeouts to downgrade buckets. */
long mPredictionTimeoutMillis;
/** Maximum time a sync adapter associated with a CP should keep the buckets elevated. */
long mSyncAdapterTimeoutMillis;
/**
* Maximum time an exempted sync should keep the buckets elevated, when sync is scheduled in
* non-doze
*/
long mExemptedSyncScheduledNonDozeTimeoutMillis;
/**
* Maximum time an exempted sync should keep the buckets elevated, when sync is scheduled in
* doze
*/
long mExemptedSyncScheduledDozeTimeoutMillis;
/**
* Maximum time an exempted sync should keep the buckets elevated, when sync is started.
*/
long mExemptedSyncStartTimeoutMillis;
/**
* Maximum time an unexempted sync should keep the buckets elevated, when sync is scheduled
*/
long mUnexemptedSyncScheduledTimeoutMillis;
/** Maximum time a system interaction should keep the buckets elevated. */
long mSystemInteractionTimeoutMillis;
/**
* Maximum time a foreground service start should keep the buckets elevated if the service
* start is the first usage of the app
*/
long mInitialForegroundServiceStartTimeoutMillis;
/**
* User usage that would elevate an app's standby bucket will also elevate the standby bucket of
* cross profile connected apps. Explicit standby bucket setting via
* {@link #setAppStandbyBucket(String, int, int, int, int)} will not be propagated.
*/
boolean mLinkCrossProfileApps;
private volatile boolean mAppIdleEnabled;
private boolean mIsCharging;
private boolean mSystemServicesReady = false;
// There was a system update, defaults need to be initialized after services are ready
private boolean mPendingInitializeDefaults;
private volatile boolean mPendingOneTimeCheckIdleStates;
private final AppStandbyHandler mHandler;
private final Context mContext;
private AppWidgetManager mAppWidgetManager;
private PackageManager mPackageManager;
Injector mInjector;
static final ArrayList<StandbyUpdateRecord> sStandbyUpdatePool = new ArrayList<>(4);
public static class StandbyUpdateRecord {
// Identity of the app whose standby state has changed
String packageName;
int userId;
// What the standby bucket the app is now in
int bucket;
// Whether the bucket change is because the user has started interacting with the app
boolean isUserInteraction;
// Reason for bucket change
int reason;
StandbyUpdateRecord(String pkgName, int userId, int bucket, int reason,
boolean isInteraction) {
this.packageName = pkgName;
this.userId = userId;
this.bucket = bucket;
this.reason = reason;
this.isUserInteraction = isInteraction;
}
public static StandbyUpdateRecord obtain(String pkgName, int userId,
int bucket, int reason, boolean isInteraction) {
synchronized (sStandbyUpdatePool) {
final int size = sStandbyUpdatePool.size();
if (size < 1) {
return new StandbyUpdateRecord(pkgName, userId, bucket, reason, isInteraction);
}
StandbyUpdateRecord r = sStandbyUpdatePool.remove(size - 1);
r.packageName = pkgName;
r.userId = userId;
r.bucket = bucket;
r.reason = reason;
r.isUserInteraction = isInteraction;
return r;
}
}
public void recycle() {
synchronized (sStandbyUpdatePool) {
sStandbyUpdatePool.add(this);
}
}
}
public AppStandbyController(Context context, Looper looper) {
this(new Injector(context, looper));
}
AppStandbyController(Injector injector) {
mInjector = injector;
mContext = mInjector.getContext();
mHandler = new AppStandbyHandler(mInjector.getLooper());
mPackageManager = mContext.getPackageManager();
DeviceStateReceiver deviceStateReceiver = new DeviceStateReceiver();
IntentFilter deviceStates = new IntentFilter(BatteryManager.ACTION_CHARGING);
deviceStates.addAction(BatteryManager.ACTION_DISCHARGING);
mContext.registerReceiver(deviceStateReceiver, deviceStates);
synchronized (mAppIdleLock) {
mAppIdleHistory = new AppIdleHistory(mInjector.getDataSystemDirectory(),
mInjector.elapsedRealtime());
}
IntentFilter packageFilter = new IntentFilter();
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
packageFilter.addDataScheme("package");
mContext.registerReceiverAsUser(new PackageReceiver(), UserHandle.ALL, packageFilter,
null, mHandler);
}
@VisibleForTesting
void setAppIdleEnabled(boolean enabled) {
mAppIdleEnabled = enabled;
}
@Override
public boolean isAppIdleEnabled() {
return mAppIdleEnabled;
}
@Override
public void onBootPhase(int phase) {
mInjector.onBootPhase(phase);
if (phase == PHASE_SYSTEM_SERVICES_READY) {
Slog.d(TAG, "Setting app idle enabled state");
// Observe changes to the threshold
SettingsObserver settingsObserver = new SettingsObserver(mHandler);
settingsObserver.registerObserver();
settingsObserver.updateSettings();
mAppWidgetManager = mContext.getSystemService(AppWidgetManager.class);
mInjector.registerDisplayListener(mDisplayListener, mHandler);
synchronized (mAppIdleLock) {
mAppIdleHistory.updateDisplay(isDisplayOn(), mInjector.elapsedRealtime());
}
mSystemServicesReady = true;
boolean userFileExists;
synchronized (mAppIdleLock) {
userFileExists = mAppIdleHistory.userFileExists(UserHandle.USER_SYSTEM);
}
if (mPendingInitializeDefaults || !userFileExists) {
initializeDefaultsForSystemApps(UserHandle.USER_SYSTEM);
}
if (mPendingOneTimeCheckIdleStates) {
postOneTimeCheckIdleStates();
}
} else if (phase == PHASE_BOOT_COMPLETED) {
setChargingState(mInjector.isCharging());
}
}
private void reportContentProviderUsage(String authority, String providerPkgName, int userId) {
if (!mAppIdleEnabled) return;
// Get sync adapters for the authority
String[] packages = ContentResolver.getSyncAdapterPackagesForAuthorityAsUser(
authority, userId);
final long elapsedRealtime = mInjector.elapsedRealtime();
for (String packageName: packages) {
// Only force the sync adapters to active if the provider is not in the same package and
// the sync adapter is a system package.
try {
PackageInfo pi = mPackageManager.getPackageInfoAsUser(
packageName, PackageManager.MATCH_SYSTEM_ONLY, userId);
if (pi == null || pi.applicationInfo == null) {
continue;
}
if (!packageName.equals(providerPkgName)) {
final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName,
userId);
synchronized (mAppIdleLock) {
reportNoninteractiveUsageCrossUserLocked(packageName, userId,
STANDBY_BUCKET_ACTIVE, REASON_SUB_USAGE_SYNC_ADAPTER,
elapsedRealtime, mSyncAdapterTimeoutMillis, linkedProfiles);
}
}
} catch (PackageManager.NameNotFoundException e) {
// Shouldn't happen
}
}
}
private void reportExemptedSyncScheduled(String packageName, int userId) {
if (!mAppIdleEnabled) return;
final int bucketToPromote;
final int usageReason;
final long durationMillis;
if (!mInjector.isDeviceIdleMode()) {
// Not dozing.
bucketToPromote = STANDBY_BUCKET_ACTIVE;
usageReason = REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE;
durationMillis = mExemptedSyncScheduledNonDozeTimeoutMillis;
} else {
// Dozing.
bucketToPromote = STANDBY_BUCKET_WORKING_SET;
usageReason = REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_DOZE;
durationMillis = mExemptedSyncScheduledDozeTimeoutMillis;
}
final long elapsedRealtime = mInjector.elapsedRealtime();
final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName, userId);
synchronized (mAppIdleLock) {
reportNoninteractiveUsageCrossUserLocked(packageName, userId, bucketToPromote,
usageReason, elapsedRealtime, durationMillis, linkedProfiles);
}
}
private void reportUnexemptedSyncScheduled(String packageName, int userId) {
if (!mAppIdleEnabled) return;
final long elapsedRealtime = mInjector.elapsedRealtime();
synchronized (mAppIdleLock) {
final int currentBucket =
mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime);
if (currentBucket == STANDBY_BUCKET_NEVER) {
final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName, userId);
// Bring the app out of the never bucket
reportNoninteractiveUsageCrossUserLocked(packageName, userId,
STANDBY_BUCKET_WORKING_SET, REASON_SUB_USAGE_UNEXEMPTED_SYNC_SCHEDULED,
elapsedRealtime, mUnexemptedSyncScheduledTimeoutMillis, linkedProfiles);
}
}
}
private void reportExemptedSyncStart(String packageName, int userId) {
if (!mAppIdleEnabled) return;
final long elapsedRealtime = mInjector.elapsedRealtime();
final List<UserHandle> linkedProfiles = getCrossProfileTargets(packageName, userId);
synchronized (mAppIdleLock) {
reportNoninteractiveUsageCrossUserLocked(packageName, userId, STANDBY_BUCKET_ACTIVE,
REASON_SUB_USAGE_EXEMPTED_SYNC_START, elapsedRealtime,
mExemptedSyncStartTimeoutMillis, linkedProfiles);
}
}
/**
* Helper method to report indirect user usage of an app and handle reporting the usage
* against cross profile connected apps. <br>
* Use {@link #reportNoninteractiveUsageLocked(String, int, int, int, long, long)} if
* cross profile connected apps do not need to be handled.
*/
private void reportNoninteractiveUsageCrossUserLocked(String packageName, int userId,
int bucket, int subReason, long elapsedRealtime, long nextCheckDelay,
List<UserHandle> otherProfiles) {
reportNoninteractiveUsageLocked(packageName, userId, bucket, subReason, elapsedRealtime,
nextCheckDelay);
final int size = otherProfiles.size();
for (int profileIndex = 0; profileIndex < size; profileIndex++) {
final int otherUserId = otherProfiles.get(profileIndex).getIdentifier();
reportNoninteractiveUsageLocked(packageName, otherUserId, bucket, subReason,
elapsedRealtime, nextCheckDelay);
}
}
/**
* Helper method to report indirect user usage of an app. <br>
* Use
* {@link #reportNoninteractiveUsageCrossUserLocked(String, int, int, int, long, long, List)}
* if cross profile connected apps need to be handled.
*/
private void reportNoninteractiveUsageLocked(String packageName, int userId, int bucket,
int subReason, long elapsedRealtime, long nextCheckDelay) {
final AppUsageHistory appUsage = mAppIdleHistory.reportUsage(packageName, userId, bucket,
subReason, 0, elapsedRealtime + nextCheckDelay);
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, packageName),
nextCheckDelay);
maybeInformListeners(packageName, userId, elapsedRealtime, appUsage.currentBucket,
appUsage.bucketingReason, false);
}
@VisibleForTesting
void setChargingState(boolean isCharging) {
synchronized (mAppIdleLock) {
if (mIsCharging != isCharging) {
if (DEBUG) Slog.d(TAG, "Setting mIsCharging to " + isCharging);
mIsCharging = isCharging;
}
}
}
@Override
public void postCheckIdleStates(int userId) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_CHECK_IDLE_STATES, userId, 0));
}
@Override
public void postOneTimeCheckIdleStates() {
if (mInjector.getBootPhase() < PHASE_SYSTEM_SERVICES_READY) {
// Not booted yet; wait for it!
mPendingOneTimeCheckIdleStates = true;
} else {
mHandler.sendEmptyMessage(MSG_ONE_TIME_CHECK_IDLE_STATES);
mPendingOneTimeCheckIdleStates = false;
}
}
@VisibleForTesting
boolean checkIdleStates(int checkUserId) {
if (!mAppIdleEnabled) {
return false;
}
final int[] runningUserIds;
try {
runningUserIds = mInjector.getRunningUserIds();
if (checkUserId != UserHandle.USER_ALL
&& !ArrayUtils.contains(runningUserIds, checkUserId)) {
return false;
}
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
final long elapsedRealtime = mInjector.elapsedRealtime();
for (int i = 0; i < runningUserIds.length; i++) {
final int userId = runningUserIds[i];
if (checkUserId != UserHandle.USER_ALL && checkUserId != userId) {
continue;
}
if (DEBUG) {
Slog.d(TAG, "Checking idle state for user " + userId);
}
List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
PackageManager.MATCH_DISABLED_COMPONENTS,
userId);
final int packageCount = packages.size();
for (int p = 0; p < packageCount; p++) {
final PackageInfo pi = packages.get(p);
final String packageName = pi.packageName;
checkAndUpdateStandbyState(packageName, userId, pi.applicationInfo.uid,
elapsedRealtime);
}
}
if (DEBUG) {
Slog.d(TAG, "checkIdleStates took "
+ (mInjector.elapsedRealtime() - elapsedRealtime));
}
return true;
}
/** Check if we need to update the standby state of a specific app. */
private void checkAndUpdateStandbyState(String packageName, @UserIdInt int userId,
int uid, long elapsedRealtime) {
if (uid <= 0) {
try {
uid = mPackageManager.getPackageUidAsUser(packageName, userId);
} catch (PackageManager.NameNotFoundException e) {
// Not a valid package for this user, nothing to do
// TODO: Remove any history of removed packages
return;
}
}
final boolean isSpecial = isAppSpecial(packageName,
UserHandle.getAppId(uid),
userId);
if (DEBUG) {
Slog.d(TAG, " Checking idle state for " + packageName + " special=" +
isSpecial);
}
if (isSpecial) {
synchronized (mAppIdleLock) {
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
}
maybeInformListeners(packageName, userId, elapsedRealtime,
STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT, false);
} else {
synchronized (mAppIdleLock) {
final AppIdleHistory.AppUsageHistory app =
mAppIdleHistory.getAppUsageHistory(packageName,
userId, elapsedRealtime);
int reason = app.bucketingReason;
final int oldMainReason = reason & REASON_MAIN_MASK;
// If the bucket was forced by the user/developer, leave it alone.
// A usage event will be the only way to bring it out of this forced state
if (oldMainReason == REASON_MAIN_FORCED_BY_USER) {
return;
}
final int oldBucket = app.currentBucket;
int newBucket = Math.max(oldBucket, STANDBY_BUCKET_ACTIVE); // Undo EXEMPTED
boolean predictionLate = predictionTimedOut(app, elapsedRealtime);
// Compute age-based bucket
if (oldMainReason == REASON_MAIN_DEFAULT
|| oldMainReason == REASON_MAIN_USAGE
|| oldMainReason == REASON_MAIN_TIMEOUT
|| predictionLate) {
if (!predictionLate && app.lastPredictedBucket >= STANDBY_BUCKET_ACTIVE
&& app.lastPredictedBucket <= STANDBY_BUCKET_RARE) {
newBucket = app.lastPredictedBucket;
reason = REASON_MAIN_PREDICTED | REASON_SUB_PREDICTED_RESTORED;
if (DEBUG) {
Slog.d(TAG, "Restored predicted newBucket = " + newBucket);
}
} else {
newBucket = getBucketForLocked(packageName, userId,
elapsedRealtime);
if (DEBUG) {
Slog.d(TAG, "Evaluated AOSP newBucket = " + newBucket);
}
reason = REASON_MAIN_TIMEOUT;
}
}
// Check if the app is within one of the timeouts for forced bucket elevation
final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime);
if (newBucket >= STANDBY_BUCKET_ACTIVE
&& app.bucketActiveTimeoutTime > elapsedTimeAdjusted) {
newBucket = STANDBY_BUCKET_ACTIVE;
reason = app.bucketingReason;
if (DEBUG) {
Slog.d(TAG, " Keeping at ACTIVE due to min timeout");
}
} else if (newBucket >= STANDBY_BUCKET_WORKING_SET
&& app.bucketWorkingSetTimeoutTime > elapsedTimeAdjusted) {
newBucket = STANDBY_BUCKET_WORKING_SET;
// If it was already there, keep the reason, else assume timeout to WS
reason = (newBucket == oldBucket)
? app.bucketingReason
: REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT;
if (DEBUG) {
Slog.d(TAG, " Keeping at WORKING_SET due to min timeout");
}
}
if (app.lastRestrictAttemptElapsedTime > app.lastUsedByUserElapsedTime
&& elapsedTimeAdjusted - app.lastUsedByUserElapsedTime
>= mInjector.getAutoRestrictedBucketDelayMs()) {
newBucket = STANDBY_BUCKET_RESTRICTED;
reason = app.lastRestrictReason;
if (DEBUG) {
Slog.d(TAG, "Bringing down to RESTRICTED due to timeout");
}
}
if (DEBUG) {
Slog.d(TAG, " Old bucket=" + oldBucket
+ ", newBucket=" + newBucket);
}
if (oldBucket < newBucket || predictionLate) {
mAppIdleHistory.setAppStandbyBucket(packageName, userId,
elapsedRealtime, newBucket, reason);
maybeInformListeners(packageName, userId, elapsedRealtime,
newBucket, reason, false);
}
}
}
}
/** Returns true if there hasn't been a prediction for the app in a while. */
private boolean predictionTimedOut(AppIdleHistory.AppUsageHistory app, long elapsedRealtime) {
return app.lastPredictedTime > 0
&& mAppIdleHistory.getElapsedTime(elapsedRealtime)
- app.lastPredictedTime > mPredictionTimeoutMillis;
}
/** Inform listeners if the bucket has changed since it was last reported to listeners */
private void maybeInformListeners(String packageName, int userId,
long elapsedRealtime, int bucket, int reason, boolean userStartedInteracting) {
synchronized (mAppIdleLock) {
if (mAppIdleHistory.shouldInformListeners(packageName, userId,
elapsedRealtime, bucket)) {
final StandbyUpdateRecord r = StandbyUpdateRecord.obtain(packageName, userId,
bucket, reason, userStartedInteracting);
if (DEBUG) Slog.d(TAG, "Standby bucket for " + packageName + "=" + bucket);
mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, r));
}
}
}
/**
* Evaluates next bucket based on time since last used and the bucketing thresholds.
* @param packageName the app
* @param userId the user
* @param elapsedRealtime as the name suggests, current elapsed time
* @return the bucket for the app, based on time since last used
*/
@GuardedBy("mAppIdleLock")
@StandbyBuckets
private int getBucketForLocked(String packageName, int userId,
long elapsedRealtime) {
int bucketIndex = mAppIdleHistory.getThresholdIndex(packageName, userId,
elapsedRealtime, mAppStandbyScreenThresholds, mAppStandbyElapsedThresholds);
return THRESHOLD_BUCKETS[bucketIndex];
}
private void notifyBatteryStats(String packageName, int userId, boolean idle) {
try {
final int uid = mPackageManager.getPackageUidAsUser(packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
if (idle) {
mInjector.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE,
packageName, uid);
} else {
mInjector.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE,
packageName, uid);
}
} catch (PackageManager.NameNotFoundException | RemoteException e) {
}
}
@Override
public void reportEvent(UsageEvents.Event event, long elapsedRealtime, int userId) {
if (!mAppIdleEnabled) return;
final int eventType = event.getEventType();
if ((eventType == UsageEvents.Event.ACTIVITY_RESUMED
|| eventType == UsageEvents.Event.ACTIVITY_PAUSED
|| eventType == UsageEvents.Event.SYSTEM_INTERACTION
|| eventType == UsageEvents.Event.USER_INTERACTION
|| eventType == UsageEvents.Event.NOTIFICATION_SEEN
|| eventType == UsageEvents.Event.SLICE_PINNED
|| eventType == UsageEvents.Event.SLICE_PINNED_PRIV
|| eventType == UsageEvents.Event.FOREGROUND_SERVICE_START)) {
final String pkg = event.getPackageName();
final List<UserHandle> linkedProfiles = getCrossProfileTargets(pkg, userId);
synchronized (mAppIdleLock) {
reportEventLocked(pkg, eventType, elapsedRealtime, userId);
final int size = linkedProfiles.size();
for (int profileIndex = 0; profileIndex < size; profileIndex++) {
final int linkedUserId = linkedProfiles.get(profileIndex).getIdentifier();
reportEventLocked(pkg, eventType, elapsedRealtime, linkedUserId);
}
}
}
}
private void reportEventLocked(String pkg, int eventType, long elapsedRealtime, int userId) {
// TODO: Ideally this should call isAppIdleFiltered() to avoid calling back
// about apps that are on some kind of whitelist anyway.
final boolean previouslyIdle = mAppIdleHistory.isIdle(
pkg, userId, elapsedRealtime);
final AppUsageHistory appHistory = mAppIdleHistory.getAppUsageHistory(
pkg, userId, elapsedRealtime);
final int prevBucket = appHistory.currentBucket;
final int prevBucketReason = appHistory.bucketingReason;
final long nextCheckDelay;
final int subReason = usageEventToSubReason(eventType);
final int reason = REASON_MAIN_USAGE | subReason;
if (eventType == UsageEvents.Event.NOTIFICATION_SEEN
|| eventType == UsageEvents.Event.SLICE_PINNED) {
// Mild usage elevates to WORKING_SET but doesn't change usage time.
mAppIdleHistory.reportUsage(appHistory, pkg, userId,
STANDBY_BUCKET_WORKING_SET, subReason,
0, elapsedRealtime + mNotificationSeenTimeoutMillis);
nextCheckDelay = mNotificationSeenTimeoutMillis;
} else if (eventType == UsageEvents.Event.SYSTEM_INTERACTION) {
mAppIdleHistory.reportUsage(appHistory, pkg, userId,
STANDBY_BUCKET_ACTIVE, subReason,
0, elapsedRealtime + mSystemInteractionTimeoutMillis);
nextCheckDelay = mSystemInteractionTimeoutMillis;
} else if (eventType == UsageEvents.Event.FOREGROUND_SERVICE_START) {
// Only elevate bucket if this is the first usage of the app
if (prevBucket != STANDBY_BUCKET_NEVER) return;
mAppIdleHistory.reportUsage(appHistory, pkg, userId,
STANDBY_BUCKET_ACTIVE, subReason,
0, elapsedRealtime + mInitialForegroundServiceStartTimeoutMillis);
nextCheckDelay = mInitialForegroundServiceStartTimeoutMillis;
} else {
mAppIdleHistory.reportUsage(appHistory, pkg, userId,
STANDBY_BUCKET_ACTIVE, subReason,
elapsedRealtime, elapsedRealtime + mStrongUsageTimeoutMillis);
nextCheckDelay = mStrongUsageTimeoutMillis;
}
if (appHistory.currentBucket != prevBucket) {
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkg),
nextCheckDelay);
final boolean userStartedInteracting =
appHistory.currentBucket == STANDBY_BUCKET_ACTIVE
&& (prevBucketReason & REASON_MAIN_MASK) != REASON_MAIN_USAGE;
maybeInformListeners(pkg, userId, elapsedRealtime,
appHistory.currentBucket, reason, userStartedInteracting);
}
if (previouslyIdle) {
notifyBatteryStats(pkg, userId, false);
}
}
/**
* Note: don't call this with the lock held since it makes calls to other system services.
*/
private @NonNull List<UserHandle> getCrossProfileTargets(String pkg, int userId) {
synchronized (mAppIdleLock) {
if (!mLinkCrossProfileApps) return Collections.emptyList();
}
return mInjector.getValidCrossProfileTargets(pkg, userId);
}
private int usageEventToSubReason(int eventType) {
switch (eventType) {
case UsageEvents.Event.ACTIVITY_RESUMED: return REASON_SUB_USAGE_MOVE_TO_FOREGROUND;
case UsageEvents.Event.ACTIVITY_PAUSED: return REASON_SUB_USAGE_MOVE_TO_BACKGROUND;
case UsageEvents.Event.SYSTEM_INTERACTION: return REASON_SUB_USAGE_SYSTEM_INTERACTION;
case UsageEvents.Event.USER_INTERACTION: return REASON_SUB_USAGE_USER_INTERACTION;
case UsageEvents.Event.NOTIFICATION_SEEN: return REASON_SUB_USAGE_NOTIFICATION_SEEN;
case UsageEvents.Event.SLICE_PINNED: return REASON_SUB_USAGE_SLICE_PINNED;
case UsageEvents.Event.SLICE_PINNED_PRIV: return REASON_SUB_USAGE_SLICE_PINNED_PRIV;
case UsageEvents.Event.FOREGROUND_SERVICE_START:
return REASON_SUB_USAGE_FOREGROUND_SERVICE_START;
default: return 0;
}
}
@VisibleForTesting
void forceIdleState(String packageName, int userId, boolean idle) {
if (!mAppIdleEnabled) return;
final int appId = getAppId(packageName);
if (appId < 0) return;
final long elapsedRealtime = mInjector.elapsedRealtime();
final boolean previouslyIdle = isAppIdleFiltered(packageName, appId,
userId, elapsedRealtime);
final int standbyBucket;
synchronized (mAppIdleLock) {
standbyBucket = mAppIdleHistory.setIdle(packageName, userId, idle, elapsedRealtime);
}
final boolean stillIdle = isAppIdleFiltered(packageName, appId,
userId, elapsedRealtime);
// Inform listeners if necessary
if (previouslyIdle != stillIdle) {
maybeInformListeners(packageName, userId, elapsedRealtime, standbyBucket,
REASON_MAIN_FORCED_BY_USER, false);
if (!stillIdle) {
notifyBatteryStats(packageName, userId, idle);
}
}
}
@Override
public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) {
synchronized (mAppIdleLock) {
mAppIdleHistory.setLastJobRunTime(packageName, userId, elapsedRealtime);
}
}
@Override
public long getTimeSinceLastJobRun(String packageName, int userId) {
final long elapsedRealtime = mInjector.elapsedRealtime();
synchronized (mAppIdleLock) {
return mAppIdleHistory.getTimeSinceLastJobRun(packageName, userId, elapsedRealtime);
}
}
@Override
public void onUserRemoved(int userId) {
synchronized (mAppIdleLock) {
mAppIdleHistory.onUserRemoved(userId);
synchronized (mActiveAdminApps) {
mActiveAdminApps.remove(userId);
}
}
}
private boolean isAppIdleUnfiltered(String packageName, int userId, long elapsedRealtime) {
synchronized (mAppIdleLock) {
return mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
}
}
@Override
public void addListener(AppIdleStateChangeListener listener) {
synchronized (mPackageAccessListeners) {
if (!mPackageAccessListeners.contains(listener)) {
mPackageAccessListeners.add(listener);
}
}
}
@Override
public void removeListener(AppIdleStateChangeListener listener) {
synchronized (mPackageAccessListeners) {
mPackageAccessListeners.remove(listener);
}
}
@Override
public int getAppId(String packageName) {
try {
ApplicationInfo ai = mPackageManager.getApplicationInfo(packageName,
PackageManager.MATCH_ANY_USER
| PackageManager.MATCH_DISABLED_COMPONENTS);
return ai.uid;
} catch (PackageManager.NameNotFoundException re) {
return -1;
}
}
@Override
public boolean isAppIdleFiltered(String packageName, int userId, long elapsedRealtime,
boolean shouldObfuscateInstantApps) {
if (shouldObfuscateInstantApps &&
mInjector.isPackageEphemeral(userId, packageName)) {
return false;
}
return isAppIdleFiltered(packageName, getAppId(packageName), userId, elapsedRealtime);
}
private boolean isAppSpecial(String packageName, int appId, int userId) {
if (packageName == null) return false;
// If not enabled at all, of course nobody is ever idle.
if (!mAppIdleEnabled) {
return true;
}
if (appId < Process.FIRST_APPLICATION_UID) {
// System uids never go idle.
return true;
}
if (packageName.equals("android")) {
// Nor does the framework (which should be redundant with the above, but for MR1 we will
// retain this for safety).
return true;
}
if (mSystemServicesReady) {
try {
// We allow all whitelisted apps, including those that don't want to be whitelisted
// for idle mode, because app idle (aka app standby) is really not as big an issue
// for controlling who participates vs. doze mode.
if (mInjector.isNonIdleWhitelisted(packageName)) {
return true;
}
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
if (isActiveDeviceAdmin(packageName, userId)) {
return true;
}
if (isActiveNetworkScorer(packageName)) {
return true;
}
if (mAppWidgetManager != null
&& mInjector.isBoundWidgetPackage(mAppWidgetManager, packageName, userId)) {
return true;
}
if (isDeviceProvisioningPackage(packageName)) {
return true;
}
}
// Check this last, as it can be the most expensive check
if (isCarrierApp(packageName)) {
return true;
}
return false;
}
@Override
public boolean isAppIdleFiltered(String packageName, int appId, int userId,
long elapsedRealtime) {
if (isAppSpecial(packageName, appId, userId)) {
return false;
} else {
synchronized (mAppIdleLock) {
if (!mAppIdleEnabled || mIsCharging) {
return false;
}
}
return isAppIdleUnfiltered(packageName, userId, elapsedRealtime);
}
}
static boolean isUserUsage(int reason) {
if ((reason & REASON_MAIN_MASK) == REASON_MAIN_USAGE) {
final int subReason = reason & REASON_SUB_MASK;
return subReason == REASON_SUB_USAGE_USER_INTERACTION
|| subReason == REASON_SUB_USAGE_MOVE_TO_FOREGROUND;
}
return false;
}
@Override
public int[] getIdleUidsForUser(int userId) {
if (!mAppIdleEnabled) {
return new int[0];
}
final long elapsedRealtime = mInjector.elapsedRealtime();
List<ApplicationInfo> apps;
try {
ParceledListSlice<ApplicationInfo> slice = AppGlobals.getPackageManager()
.getInstalledApplications(/* flags= */ 0, userId);
if (slice == null) {
return new int[0];
}
apps = slice.getList();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
// State of each uid. Key is the uid. Value lower 16 bits is the number of apps
// associated with that uid, upper 16 bits is the number of those apps that is idle.
SparseIntArray uidStates = new SparseIntArray();
// Now resolve all app state. Iterating over all apps, keeping track of how many
// we find for each uid and how many of those are idle.
for (int i = apps.size() - 1; i >= 0; i--) {
ApplicationInfo ai = apps.get(i);
// Check whether this app is idle.
boolean idle = isAppIdleFiltered(ai.packageName, UserHandle.getAppId(ai.uid),
userId, elapsedRealtime);
int index = uidStates.indexOfKey(ai.uid);
if (index < 0) {
uidStates.put(ai.uid, 1 + (idle ? 1<<16 : 0));
} else {
int value = uidStates.valueAt(index);
uidStates.setValueAt(index, value + 1 + (idle ? 1<<16 : 0));
}
}
if (DEBUG) {
Slog.d(TAG, "getIdleUids took " + (mInjector.elapsedRealtime() - elapsedRealtime));
}
int numIdle = 0;
for (int i = uidStates.size() - 1; i >= 0; i--) {
int value = uidStates.valueAt(i);
if ((value&0x7fff) == (value>>16)) {
numIdle++;
}
}
int[] res = new int[numIdle];
numIdle = 0;
for (int i = uidStates.size() - 1; i >= 0; i--) {
int value = uidStates.valueAt(i);
if ((value&0x7fff) == (value>>16)) {
res[numIdle] = uidStates.keyAt(i);
numIdle++;
}
}
return res;
}
@Override
public void setAppIdleAsync(String packageName, boolean idle, int userId) {
if (packageName == null || !mAppIdleEnabled) return;
mHandler.obtainMessage(MSG_FORCE_IDLE_STATE, userId, idle ? 1 : 0, packageName)
.sendToTarget();
}
@Override
@StandbyBuckets public int getAppStandbyBucket(String packageName, int userId,
long elapsedRealtime, boolean shouldObfuscateInstantApps) {
if (!mAppIdleEnabled || (shouldObfuscateInstantApps
&& mInjector.isPackageEphemeral(userId, packageName))) {
return STANDBY_BUCKET_ACTIVE;
}
synchronized (mAppIdleLock) {
return mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime);
}
}
@VisibleForTesting
int getAppStandbyBucketReason(String packageName, int userId, long elapsedRealtime) {
synchronized (mAppIdleLock) {
return mAppIdleHistory.getAppStandbyReason(packageName, userId, elapsedRealtime);
}
}
@Override
public List<AppStandbyInfo> getAppStandbyBuckets(int userId) {
synchronized (mAppIdleLock) {
return mAppIdleHistory.getAppStandbyBuckets(userId, mAppIdleEnabled);
}
}
@Override
public void restrictApp(@NonNull String packageName, int userId,
@SystemForcedReasons int restrictReason) {
// If the package is not installed, don't allow the bucket to be set.
if (!mInjector.isPackageInstalled(packageName, 0, userId)) {
Slog.e(TAG, "Tried to restrict uninstalled app: " + packageName);
return;
}
final int reason = REASON_MAIN_FORCED_BY_SYSTEM | (REASON_SUB_MASK & restrictReason);
final long nowElapsed = mInjector.elapsedRealtime();
setAppStandbyBucket(packageName, userId, STANDBY_BUCKET_RESTRICTED, reason,
nowElapsed, false);
}
@Override
public void setAppStandbyBucket(@NonNull String packageName, int bucket, int userId,
int callingUid, int callingPid) {
setAppStandbyBuckets(
Collections.singletonList(new AppStandbyInfo(packageName, bucket)),
userId, callingUid, callingPid);
}
@Override
public void setAppStandbyBuckets(@NonNull List<AppStandbyInfo> appBuckets, int userId,
int callingUid, int callingPid) {
userId = ActivityManager.handleIncomingUser(
callingPid, callingUid, userId, false, true, "setAppStandbyBucket", null);
final boolean shellCaller = callingUid == Process.ROOT_UID
|| callingUid == Process.SHELL_UID;
final int reason;
// The Settings app runs in the system UID but in a separate process. Assume
// things coming from other processes are due to the user.
if ((UserHandle.isSameApp(callingUid, Process.SYSTEM_UID) && callingPid != Process.myPid())
|| shellCaller) {
reason = REASON_MAIN_FORCED_BY_USER;
} else if (UserHandle.isCore(callingUid)) {
reason = REASON_MAIN_FORCED_BY_SYSTEM;
} else {
reason = REASON_MAIN_PREDICTED;
}
final int packageFlags = PackageManager.MATCH_ANY_USER
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
| PackageManager.MATCH_DIRECT_BOOT_AWARE;
final int numApps = appBuckets.size();
final long elapsedRealtime = mInjector.elapsedRealtime();
for (int i = 0; i < numApps; ++i) {
final AppStandbyInfo bucketInfo = appBuckets.get(i);
final String packageName = bucketInfo.mPackageName;
final int bucket = bucketInfo.mStandbyBucket;
if (bucket < STANDBY_BUCKET_ACTIVE || bucket > STANDBY_BUCKET_NEVER) {
throw new IllegalArgumentException("Cannot set the standby bucket to " + bucket);
}
final int packageUid = mInjector.getPackageManagerInternal()
.getPackageUid(packageName, packageFlags, userId);
// Caller cannot set their own standby state
if (packageUid == callingUid) {
throw new IllegalArgumentException("Cannot set your own standby bucket");
}
if (packageUid < 0) {
throw new IllegalArgumentException(
"Cannot set standby bucket for non existent package (" + packageName + ")");
}
setAppStandbyBucket(packageName, userId, bucket, reason, elapsedRealtime, shellCaller);
}
}
@VisibleForTesting
void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket,
int reason) {
setAppStandbyBucket(
packageName, userId, newBucket, reason, mInjector.elapsedRealtime(), false);
}
private void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket,
int reason, long elapsedRealtime, boolean resetTimeout) {
synchronized (mAppIdleLock) {
// If the package is not installed, don't allow the bucket to be set.
if (!mInjector.isPackageInstalled(packageName, 0, userId)) {
Slog.e(TAG, "Tried to set bucket of uninstalled app: " + packageName);
return;
}
AppIdleHistory.AppUsageHistory app = mAppIdleHistory.getAppUsageHistory(packageName,
userId, elapsedRealtime);
boolean predicted = (reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED;
// Don't allow changing bucket if higher than ACTIVE
if (app.currentBucket < STANDBY_BUCKET_ACTIVE) return;
// Don't allow prediction to change from/to NEVER.
if ((app.currentBucket == STANDBY_BUCKET_NEVER || newBucket == STANDBY_BUCKET_NEVER)
&& predicted) {
return;
}
final boolean wasForcedBySystem =
(app.bucketingReason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_SYSTEM;
// If the bucket was forced, don't allow prediction to override
if (predicted
&& ((app.bucketingReason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_USER
|| wasForcedBySystem)) {
return;
}
final boolean isForcedBySystem =
(reason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_SYSTEM;
if (app.currentBucket == newBucket && wasForcedBySystem && isForcedBySystem) {
mAppIdleHistory
.noteRestrictionAttempt(packageName, userId, elapsedRealtime, reason);
// Keep track of all restricting reasons
reason = REASON_MAIN_FORCED_BY_SYSTEM
| (app.bucketingReason & REASON_SUB_MASK)
| (reason & REASON_SUB_MASK);
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
newBucket, reason, resetTimeout);
return;
}
final boolean isForcedByUser =
(reason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_USER;
if (app.currentBucket == STANDBY_BUCKET_RESTRICTED) {
if ((app.bucketingReason & REASON_MAIN_MASK) == REASON_MAIN_TIMEOUT) {
if (predicted && newBucket >= STANDBY_BUCKET_RARE) {
// Predicting into RARE or below means we don't expect the user to use the
// app anytime soon, so don't elevate it from RESTRICTED.
return;
}
} else if (!isUserUsage(reason) && !isForcedByUser) {
// If the current bucket is RESTRICTED, only user force or usage should bring
// it out, unless the app was put into the bucket due to timing out.
return;
}
}
if (newBucket == STANDBY_BUCKET_RESTRICTED) {
mAppIdleHistory
.noteRestrictionAttempt(packageName, userId, elapsedRealtime, reason);
if (isForcedByUser) {
// Only user force can bypass the delay restriction. If the user forced the
// app into the RESTRICTED bucket, then a toast confirming the action
// shouldn't be surprising.
if (Build.IS_DEBUGGABLE) {
Toast.makeText(mContext,
// Since AppStandbyController sits low in the lock hierarchy,
// make sure not to call out with the lock held.
mHandler.getLooper(),
mContext.getResources().getString(
R.string.as_app_forced_to_restricted_bucket, packageName),
Toast.LENGTH_SHORT)
.show();
} else {
Slog.i(TAG, packageName + " restricted by user");
}
} else {
final long timeUntilRestrictPossibleMs = app.lastUsedByUserElapsedTime
+ mInjector.getAutoRestrictedBucketDelayMs() - elapsedRealtime;
if (timeUntilRestrictPossibleMs > 0) {
Slog.w(TAG, "Tried to restrict recently used app: " + packageName
+ " due to " + reason);
mHandler.sendMessageDelayed(
mHandler.obtainMessage(
MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, packageName),
timeUntilRestrictPossibleMs);
return;
}
}
}
// If the bucket is required to stay in a higher state for a specified duration, don't
// override unless the duration has passed
if (predicted) {
// Check if the app is within one of the timeouts for forced bucket elevation
final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime);
// In case of not using the prediction, just keep track of it for applying after
// ACTIVE or WORKING_SET timeout.
mAppIdleHistory.updateLastPrediction(app, elapsedTimeAdjusted, newBucket);
if (newBucket > STANDBY_BUCKET_ACTIVE
&& app.bucketActiveTimeoutTime > elapsedTimeAdjusted) {
newBucket = STANDBY_BUCKET_ACTIVE;
reason = app.bucketingReason;
if (DEBUG) {
Slog.d(TAG, " Keeping at ACTIVE due to min timeout");
}
} else if (newBucket > STANDBY_BUCKET_WORKING_SET
&& app.bucketWorkingSetTimeoutTime > elapsedTimeAdjusted) {
newBucket = STANDBY_BUCKET_WORKING_SET;
if (app.currentBucket != newBucket) {
reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT;
} else {
reason = app.bucketingReason;
}
if (DEBUG) {
Slog.d(TAG, " Keeping at WORKING_SET due to min timeout");
}
} else if (newBucket == STANDBY_BUCKET_RARE
&& getBucketForLocked(packageName, userId, elapsedRealtime)
== STANDBY_BUCKET_RESTRICTED) {
// Prediction doesn't think the app will be used anytime soon and
// it's been long enough that it could just time out into restricted,
// so time it out there instead. Using TIMEOUT will allow prediction
// to raise the bucket when it needs to.
newBucket = STANDBY_BUCKET_RESTRICTED;
reason = REASON_MAIN_TIMEOUT;
if (DEBUG) {
Slog.d(TAG,
"Prediction to RARE overridden by timeout into RESTRICTED");
}
}
}
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
reason, resetTimeout);
}
maybeInformListeners(packageName, userId, elapsedRealtime, newBucket, reason, false);
}
@VisibleForTesting
boolean isActiveDeviceAdmin(String packageName, int userId) {
synchronized (mActiveAdminApps) {
final Set<String> adminPkgs = mActiveAdminApps.get(userId);
return adminPkgs != null && adminPkgs.contains(packageName);
}
}
@Override
public void addActiveDeviceAdmin(String adminPkg, int userId) {
synchronized (mActiveAdminApps) {
Set<String> adminPkgs = mActiveAdminApps.get(userId);
if (adminPkgs == null) {
adminPkgs = new ArraySet<>();
mActiveAdminApps.put(userId, adminPkgs);
}
adminPkgs.add(adminPkg);
}
}
@Override
public void setActiveAdminApps(Set<String> adminPkgs, int userId) {
synchronized (mActiveAdminApps) {
if (adminPkgs == null) {
mActiveAdminApps.remove(userId);
} else {
mActiveAdminApps.put(userId, adminPkgs);
}
}
}
@Override
public void onAdminDataAvailable() {
mAdminDataAvailableLatch.countDown();
}
/**
* This will only ever be called once - during device boot.
*/
private void waitForAdminData() {
if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) {
ConcurrentUtils.waitForCountDownNoInterrupt(mAdminDataAvailableLatch,
WAIT_FOR_ADMIN_DATA_TIMEOUT_MS, "Wait for admin data");
}
}
@VisibleForTesting
Set<String> getActiveAdminAppsForTest(int userId) {
synchronized (mActiveAdminApps) {
return mActiveAdminApps.get(userId);
}
}
/**
* Returns {@code true} if the supplied package is the device provisioning app. Otherwise,
* returns {@code false}.
*/
private boolean isDeviceProvisioningPackage(String packageName) {
String deviceProvisioningPackage = mContext.getResources().getString(
com.android.internal.R.string.config_deviceProvisioningPackage);
return deviceProvisioningPackage != null && deviceProvisioningPackage.equals(packageName);
}
private boolean isCarrierApp(String packageName) {
synchronized (mAppIdleLock) {
if (!mHaveCarrierPrivilegedApps) {
fetchCarrierPrivilegedAppsLocked();
}
if (mCarrierPrivilegedApps != null) {
return mCarrierPrivilegedApps.contains(packageName);
}
return false;
}
}
@Override
public void clearCarrierPrivilegedApps() {
if (DEBUG) {
Slog.i(TAG, "Clearing carrier privileged apps list");
}
synchronized (mAppIdleLock) {
mHaveCarrierPrivilegedApps = false;
mCarrierPrivilegedApps = null; // Need to be refetched.
}
}
@GuardedBy("mAppIdleLock")
private void fetchCarrierPrivilegedAppsLocked() {
TelephonyManager telephonyManager =
mContext.getSystemService(TelephonyManager.class);
mCarrierPrivilegedApps =
telephonyManager.getCarrierPrivilegedPackagesForAllActiveSubscriptions();
mHaveCarrierPrivilegedApps = true;
if (DEBUG) {
Slog.d(TAG, "apps with carrier privilege " + mCarrierPrivilegedApps);
}
}
private boolean isActiveNetworkScorer(String packageName) {
String activeScorer = mInjector.getActiveNetworkScorer();
return packageName != null && packageName.equals(activeScorer);
}
private void informListeners(String packageName, int userId, int bucket, int reason,
boolean userInteraction) {
final boolean idle = bucket >= STANDBY_BUCKET_RARE;
synchronized (mPackageAccessListeners) {
for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
listener.onAppIdleStateChanged(packageName, userId, idle, bucket, reason);
if (userInteraction) {
listener.onUserInteractionStarted(packageName, userId);
}
}
}
}
@Override
public void flushToDisk(int userId) {
synchronized (mAppIdleLock) {
mAppIdleHistory.writeAppIdleTimes(userId);
}
}
@Override
public void flushDurationsToDisk() {
// Persist elapsed and screen on time. If this fails for whatever reason, the apps will be
// considered not-idle, which is the safest outcome in such an event.
synchronized (mAppIdleLock) {
mAppIdleHistory.writeAppIdleDurations();
}
}
private boolean isDisplayOn() {
return mInjector.isDefaultDisplayOn();
}
@VisibleForTesting
void clearAppIdleForPackage(String packageName, int userId) {
synchronized (mAppIdleLock) {
mAppIdleHistory.clearUsage(packageName, userId);
}
}
/**
* Remove an app from the {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
* bucket if it was forced into the bucket by the system because it was buggy.
*/
@VisibleForTesting
void maybeUnrestrictBuggyApp(String packageName, int userId) {
synchronized (mAppIdleLock) {
final long elapsedRealtime = mInjector.elapsedRealtime();
final AppIdleHistory.AppUsageHistory app =
mAppIdleHistory.getAppUsageHistory(packageName, userId, elapsedRealtime);
if (app.currentBucket != STANDBY_BUCKET_RESTRICTED
|| (app.bucketingReason & REASON_MAIN_MASK) != REASON_MAIN_FORCED_BY_SYSTEM) {
return;
}
final int newBucket;
final int newReason;
if ((app.bucketingReason & REASON_SUB_MASK) == REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY) {
// If bugginess was the only reason the app should be restricted, then lift it out.
newBucket = STANDBY_BUCKET_RARE;
newReason = REASON_MAIN_DEFAULT | REASON_SUB_DEFAULT_APP_UPDATE;
} else {
// There's another reason the app was restricted. Remove the buggy bit and call
// it a day.
newBucket = STANDBY_BUCKET_RESTRICTED;
newReason = app.bucketingReason & ~REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY;
}
mAppIdleHistory.setAppStandbyBucket(
packageName, userId, elapsedRealtime, newBucket, newReason);
}
}
private class PackageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (Intent.ACTION_PACKAGE_ADDED.equals(action)
|| Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
clearCarrierPrivilegedApps();
}
if ((Intent.ACTION_PACKAGE_REMOVED.equals(action) ||
Intent.ACTION_PACKAGE_ADDED.equals(action))) {
final String pkgName = intent.getData().getSchemeSpecificPart();
final int userId = getSendingUserId();
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
maybeUnrestrictBuggyApp(pkgName, userId);
} else {
clearAppIdleForPackage(pkgName, userId);
}
}
}
}
@Override
public void initializeDefaultsForSystemApps(int userId) {
if (!mSystemServicesReady) {
// Do it later, since SettingsProvider wasn't queried yet for app_standby_enabled
mPendingInitializeDefaults = true;
return;
}
Slog.d(TAG, "Initializing defaults for system apps on user " + userId + ", "
+ "appIdleEnabled=" + mAppIdleEnabled);
final long elapsedRealtime = mInjector.elapsedRealtime();
List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
PackageManager.MATCH_DISABLED_COMPONENTS,
userId);
final int packageCount = packages.size();
synchronized (mAppIdleLock) {
for (int i = 0; i < packageCount; i++) {
final PackageInfo pi = packages.get(i);
String packageName = pi.packageName;
if (pi.applicationInfo != null && pi.applicationInfo.isSystemApp()) {
// Mark app as used for 2 hours. After that it can timeout to whatever the
// past usage pattern was.
mAppIdleHistory.reportUsage(packageName, userId, STANDBY_BUCKET_ACTIVE,
REASON_SUB_USAGE_SYSTEM_UPDATE, 0,
elapsedRealtime + mSystemUpdateUsageTimeoutMillis);
}
}
// Immediately persist defaults to disk
mAppIdleHistory.writeAppIdleTimes(userId);
}
}
@Override
public void postReportContentProviderUsage(String name, String packageName, int userId) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = name;
args.arg2 = packageName;
args.arg3 = userId;
mHandler.obtainMessage(MSG_REPORT_CONTENT_PROVIDER_USAGE, args)
.sendToTarget();
}
@Override
public void postReportSyncScheduled(String packageName, int userId, boolean exempted) {
mHandler.obtainMessage(MSG_REPORT_SYNC_SCHEDULED, userId, exempted ? 1 : 0, packageName)
.sendToTarget();
}
@Override
public void postReportExemptedSyncStart(String packageName, int userId) {
mHandler.obtainMessage(MSG_REPORT_EXEMPTED_SYNC_START, userId, 0, packageName)
.sendToTarget();
}
@Override
public void dumpUser(IndentingPrintWriter idpw, int userId, List<String> pkgs) {
synchronized (mAppIdleLock) {
mAppIdleHistory.dump(idpw, userId, pkgs);
}
}
@Override
public void dumpState(String[] args, PrintWriter pw) {
synchronized (mAppIdleLock) {
pw.println("Carrier privileged apps (have=" + mHaveCarrierPrivilegedApps
+ "): " + mCarrierPrivilegedApps);
}
final long now = System.currentTimeMillis();
pw.println();
pw.println("Settings:");
pw.print(" mCheckIdleIntervalMillis=");
TimeUtils.formatDuration(mCheckIdleIntervalMillis, pw);
pw.println();
pw.print(" mStrongUsageTimeoutMillis=");
TimeUtils.formatDuration(mStrongUsageTimeoutMillis, pw);
pw.println();
pw.print(" mNotificationSeenTimeoutMillis=");
TimeUtils.formatDuration(mNotificationSeenTimeoutMillis, pw);
pw.println();
pw.print(" mSyncAdapterTimeoutMillis=");
TimeUtils.formatDuration(mSyncAdapterTimeoutMillis, pw);
pw.println();
pw.print(" mSystemInteractionTimeoutMillis=");
TimeUtils.formatDuration(mSystemInteractionTimeoutMillis, pw);
pw.println();
pw.print(" mInitialForegroundServiceStartTimeoutMillis=");
TimeUtils.formatDuration(mInitialForegroundServiceStartTimeoutMillis, pw);
pw.println();
pw.print(" mPredictionTimeoutMillis=");
TimeUtils.formatDuration(mPredictionTimeoutMillis, pw);
pw.println();
pw.print(" mExemptedSyncScheduledNonDozeTimeoutMillis=");
TimeUtils.formatDuration(mExemptedSyncScheduledNonDozeTimeoutMillis, pw);
pw.println();
pw.print(" mExemptedSyncScheduledDozeTimeoutMillis=");
TimeUtils.formatDuration(mExemptedSyncScheduledDozeTimeoutMillis, pw);
pw.println();
pw.print(" mExemptedSyncStartTimeoutMillis=");
TimeUtils.formatDuration(mExemptedSyncStartTimeoutMillis, pw);
pw.println();
pw.print(" mUnexemptedSyncScheduledTimeoutMillis=");
TimeUtils.formatDuration(mUnexemptedSyncScheduledTimeoutMillis, pw);
pw.println();
pw.print(" mSystemUpdateUsageTimeoutMillis=");
TimeUtils.formatDuration(mSystemUpdateUsageTimeoutMillis, pw);
pw.println();
pw.println();
pw.print("mAppIdleEnabled="); pw.print(mAppIdleEnabled);
pw.print(" mIsCharging=");
pw.print(mIsCharging);
pw.println();
pw.print("mScreenThresholds="); pw.println(Arrays.toString(mAppStandbyScreenThresholds));
pw.print("mElapsedThresholds="); pw.println(Arrays.toString(mAppStandbyElapsedThresholds));
pw.println();
}
/**
* Injector for interaction with external code. Override methods to provide a mock
* implementation for tests.
* onBootPhase() must be called with at least the PHASE_SYSTEM_SERVICES_READY
*/
static class Injector {
private final Context mContext;
private final Looper mLooper;
private IBatteryStats mBatteryStats;
private BatteryManager mBatteryManager;
private PackageManagerInternal mPackageManagerInternal;
private DisplayManager mDisplayManager;
private PowerManager mPowerManager;
private PowerWhitelistManager mPowerWhitelistManager;
private CrossProfileAppsInternal mCrossProfileAppsInternal;
int mBootPhase;
/**
* The minimum amount of time required since the last user interaction before an app can be
* automatically placed in the RESTRICTED bucket.
*/
long mAutoRestrictedBucketDelayMs = ONE_DAY;
Injector(Context context, Looper looper) {
mContext = context;
mLooper = looper;
}
Context getContext() {
return mContext;
}
Looper getLooper() {
return mLooper;
}
void onBootPhase(int phase) {
if (phase == PHASE_SYSTEM_SERVICES_READY) {
mPowerWhitelistManager = mContext.getSystemService(PowerWhitelistManager.class);
mBatteryStats = IBatteryStats.Stub.asInterface(
ServiceManager.getService(BatteryStats.SERVICE_NAME));
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mDisplayManager = (DisplayManager) mContext.getSystemService(
Context.DISPLAY_SERVICE);
mPowerManager = mContext.getSystemService(PowerManager.class);
mBatteryManager = mContext.getSystemService(BatteryManager.class);
mCrossProfileAppsInternal = LocalServices.getService(
CrossProfileAppsInternal.class);
final ActivityManager activityManager =
(ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager.isLowRamDevice() || ActivityManager.isSmallBatteryDevice()) {
mAutoRestrictedBucketDelayMs = 12 * ONE_HOUR;
}
}
mBootPhase = phase;
}
int getBootPhase() {
return mBootPhase;
}
/**
* Returns the elapsed realtime since the device started. Override this
* to control the clock.
* @return elapsed realtime
*/
long elapsedRealtime() {
return SystemClock.elapsedRealtime();
}
long currentTimeMillis() {
return System.currentTimeMillis();
}
boolean isAppIdleEnabled() {
final boolean buildFlag = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableAutoPowerModes);
final boolean runtimeFlag = Global.getInt(mContext.getContentResolver(),
Global.APP_STANDBY_ENABLED, 1) == 1
&& Global.getInt(mContext.getContentResolver(),
Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED, 1) == 1;
return buildFlag && runtimeFlag;
}
boolean isCharging() {
return mBatteryManager.isCharging();
}
boolean isNonIdleWhitelisted(String packageName) throws RemoteException {
return mPowerWhitelistManager.isWhitelisted(packageName, false);
}
File getDataSystemDirectory() {
return Environment.getDataSystemDirectory();
}
/**
* Return the minimum amount of time that must have passed since the last user usage before
* an app can be automatically put into the
* {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
*/
long getAutoRestrictedBucketDelayMs() {
return mAutoRestrictedBucketDelayMs;
}
void noteEvent(int event, String packageName, int uid) throws RemoteException {
mBatteryStats.noteEvent(event, packageName, uid);
}
PackageManagerInternal getPackageManagerInternal() {
return mPackageManagerInternal;
}
boolean isPackageEphemeral(int userId, String packageName) {
return mPackageManagerInternal.isPackageEphemeral(userId, packageName);
}
boolean isPackageInstalled(String packageName, int flags, int userId) {
return mPackageManagerInternal.getPackageUid(packageName, flags, userId) >= 0;
}
int[] getRunningUserIds() throws RemoteException {
return ActivityManager.getService().getRunningUserIds();
}
boolean isDefaultDisplayOn() {
return mDisplayManager
.getDisplay(Display.DEFAULT_DISPLAY).getState() == Display.STATE_ON;
}
void registerDisplayListener(DisplayManager.DisplayListener listener, Handler handler) {
mDisplayManager.registerDisplayListener(listener, handler);
}
String getActiveNetworkScorer() {
NetworkScoreManager nsm = (NetworkScoreManager) mContext.getSystemService(
Context.NETWORK_SCORE_SERVICE);
return nsm.getActiveScorerPackage();
}
public boolean isBoundWidgetPackage(AppWidgetManager appWidgetManager, String packageName,
int userId) {
return appWidgetManager.isBoundWidgetPackage(packageName, userId);
}
String getAppIdleSettings() {
return Global.getString(mContext.getContentResolver(),
Global.APP_IDLE_CONSTANTS);
}
/** Whether the device is in doze or not. */
public boolean isDeviceIdleMode() {
return mPowerManager.isDeviceIdleMode();
}
public List<UserHandle> getValidCrossProfileTargets(String pkg, int userId) {
final int uid = mPackageManagerInternal.getPackageUidInternal(pkg, 0, userId);
final AndroidPackage aPkg = mPackageManagerInternal.getPackage(uid);
if (uid < 0
|| aPkg == null
|| !aPkg.isCrossProfile()
|| !mCrossProfileAppsInternal
.verifyUidHasInteractAcrossProfilePermission(pkg, uid)) {
if (uid >= 0 && aPkg == null) {
Slog.wtf(TAG, "Null package retrieved for UID " + uid);
}
return Collections.emptyList();
}
return mCrossProfileAppsInternal.getTargetUserProfiles(pkg, userId);
}
}
class AppStandbyHandler extends Handler {
AppStandbyHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INFORM_LISTENERS:
StandbyUpdateRecord r = (StandbyUpdateRecord) msg.obj;
informListeners(r.packageName, r.userId, r.bucket, r.reason,
r.isUserInteraction);
r.recycle();
break;
case MSG_FORCE_IDLE_STATE:
forceIdleState((String) msg.obj, msg.arg1, msg.arg2 == 1);
break;
case MSG_CHECK_IDLE_STATES:
if (checkIdleStates(msg.arg1) && mAppIdleEnabled) {
mHandler.sendMessageDelayed(mHandler.obtainMessage(
MSG_CHECK_IDLE_STATES, msg.arg1, 0),
mCheckIdleIntervalMillis);
}
break;
case MSG_ONE_TIME_CHECK_IDLE_STATES:
mHandler.removeMessages(MSG_ONE_TIME_CHECK_IDLE_STATES);
waitForAdminData();
checkIdleStates(UserHandle.USER_ALL);
break;
case MSG_REPORT_CONTENT_PROVIDER_USAGE:
SomeArgs args = (SomeArgs) msg.obj;
reportContentProviderUsage((String) args.arg1, // authority name
(String) args.arg2, // package name
(int) args.arg3); // userId
args.recycle();
break;
case MSG_CHECK_PACKAGE_IDLE_STATE:
checkAndUpdateStandbyState((String) msg.obj, msg.arg1, msg.arg2,
mInjector.elapsedRealtime());
break;
case MSG_REPORT_SYNC_SCHEDULED:
final boolean exempted = msg.arg2 > 0 ? true : false;
if (exempted) {
reportExemptedSyncScheduled((String) msg.obj, msg.arg1);
} else {
reportUnexemptedSyncScheduled((String) msg.obj, msg.arg1);
}
break;
case MSG_REPORT_EXEMPTED_SYNC_START:
reportExemptedSyncStart((String) msg.obj, msg.arg1);
break;
default:
super.handleMessage(msg);
break;
}
}
};
private class DeviceStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case BatteryManager.ACTION_CHARGING:
setChargingState(true);
break;
case BatteryManager.ACTION_DISCHARGING:
setChargingState(false);
break;
}
}
}
private final DisplayManager.DisplayListener mDisplayListener
= new DisplayManager.DisplayListener() {
@Override public void onDisplayAdded(int displayId) {
}
@Override public void onDisplayRemoved(int displayId) {
}
@Override public void onDisplayChanged(int displayId) {
if (displayId == Display.DEFAULT_DISPLAY) {
final boolean displayOn = isDisplayOn();
synchronized (mAppIdleLock) {
mAppIdleHistory.updateDisplay(displayOn, mInjector.elapsedRealtime());
}
}
}
};
/**
* Observe settings changes for {@link Global#APP_IDLE_CONSTANTS}.
*/
private class SettingsObserver extends ContentObserver {
private static final String KEY_SCREEN_TIME_THRESHOLDS = "screen_thresholds";
private static final String KEY_ELAPSED_TIME_THRESHOLDS = "elapsed_thresholds";
private static final String KEY_STRONG_USAGE_HOLD_DURATION = "strong_usage_duration";
private static final String KEY_NOTIFICATION_SEEN_HOLD_DURATION =
"notification_seen_duration";
private static final String KEY_SYSTEM_UPDATE_HOLD_DURATION =
"system_update_usage_duration";
private static final String KEY_PREDICTION_TIMEOUT = "prediction_timeout";
private static final String KEY_SYNC_ADAPTER_HOLD_DURATION = "sync_adapter_duration";
private static final String KEY_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_HOLD_DURATION =
"exempted_sync_scheduled_nd_duration";
private static final String KEY_EXEMPTED_SYNC_SCHEDULED_DOZE_HOLD_DURATION =
"exempted_sync_scheduled_d_duration";
private static final String KEY_EXEMPTED_SYNC_START_HOLD_DURATION =
"exempted_sync_start_duration";
private static final String KEY_UNEXEMPTED_SYNC_SCHEDULED_HOLD_DURATION =
"unexempted_sync_scheduled_duration";
private static final String KEY_SYSTEM_INTERACTION_HOLD_DURATION =
"system_interaction_duration";
private static final String KEY_INITIAL_FOREGROUND_SERVICE_START_HOLD_DURATION =
"initial_foreground_service_start_duration";
private static final String KEY_AUTO_RESTRICTED_BUCKET_DELAY_MS =
"auto_restricted_bucket_delay_ms";
private static final String KEY_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS =
"cross_profile_apps_share_standby_buckets";
public static final long DEFAULT_STRONG_USAGE_TIMEOUT = 1 * ONE_HOUR;
public static final long DEFAULT_NOTIFICATION_TIMEOUT = 12 * ONE_HOUR;
public static final long DEFAULT_SYSTEM_UPDATE_TIMEOUT = 2 * ONE_HOUR;
public static final long DEFAULT_SYSTEM_INTERACTION_TIMEOUT = 10 * ONE_MINUTE;
public static final long DEFAULT_SYNC_ADAPTER_TIMEOUT = 10 * ONE_MINUTE;
public static final long DEFAULT_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_TIMEOUT = 10 * ONE_MINUTE;
public static final long DEFAULT_EXEMPTED_SYNC_SCHEDULED_DOZE_TIMEOUT = 4 * ONE_HOUR;
public static final long DEFAULT_EXEMPTED_SYNC_START_TIMEOUT = 10 * ONE_MINUTE;
public static final long DEFAULT_UNEXEMPTED_SYNC_SCHEDULED_TIMEOUT = 10 * ONE_MINUTE;
public static final long DEFAULT_INITIAL_FOREGROUND_SERVICE_START_TIMEOUT = 30 * ONE_MINUTE;
public static final long DEFAULT_AUTO_RESTRICTED_BUCKET_DELAY_MS = ONE_DAY;
public static final boolean DEFAULT_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS = true;
private final KeyValueListParser mParser = new KeyValueListParser(',');
SettingsObserver(Handler handler) {
super(handler);
}
void registerObserver() {
final ContentResolver cr = mContext.getContentResolver();
cr.registerContentObserver(Global.getUriFor(Global.APP_IDLE_CONSTANTS), false, this);
cr.registerContentObserver(Global.getUriFor(Global.APP_STANDBY_ENABLED), false, this);
cr.registerContentObserver(Global.getUriFor(Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED),
false, this);
}
@Override
public void onChange(boolean selfChange) {
updateSettings();
postOneTimeCheckIdleStates();
}
void updateSettings() {
if (DEBUG) {
Slog.d(TAG,
"appidle=" + Global.getString(mContext.getContentResolver(),
Global.APP_STANDBY_ENABLED));
Slog.d(TAG,
"adaptivebat=" + Global.getString(mContext.getContentResolver(),
Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED));
Slog.d(TAG, "appidleconstants=" + Global.getString(
mContext.getContentResolver(),
Global.APP_IDLE_CONSTANTS));
}
// Look at global settings for this.
// TODO: Maybe apply different thresholds for different users.
try {
mParser.setString(mInjector.getAppIdleSettings());
} catch (IllegalArgumentException e) {
Slog.e(TAG, "Bad value for app idle settings: " + e.getMessage());
// fallthrough, mParser is empty and all defaults will be returned.
}
synchronized (mAppIdleLock) {
String screenThresholdsValue = mParser.getString(KEY_SCREEN_TIME_THRESHOLDS, null);
mAppStandbyScreenThresholds = parseLongArray(screenThresholdsValue,
SCREEN_TIME_THRESHOLDS, MINIMUM_SCREEN_TIME_THRESHOLDS);
String elapsedThresholdsValue = mParser.getString(KEY_ELAPSED_TIME_THRESHOLDS,
null);
mAppStandbyElapsedThresholds = parseLongArray(elapsedThresholdsValue,
ELAPSED_TIME_THRESHOLDS, MINIMUM_ELAPSED_TIME_THRESHOLDS);
mCheckIdleIntervalMillis = Math.min(mAppStandbyElapsedThresholds[1] / 4,
COMPRESS_TIME ? ONE_MINUTE : 4 * 60 * ONE_MINUTE); // 4 hours
mStrongUsageTimeoutMillis = mParser.getDurationMillis(
KEY_STRONG_USAGE_HOLD_DURATION,
COMPRESS_TIME ? ONE_MINUTE : DEFAULT_STRONG_USAGE_TIMEOUT);
mNotificationSeenTimeoutMillis = mParser.getDurationMillis(
KEY_NOTIFICATION_SEEN_HOLD_DURATION,
COMPRESS_TIME ? 12 * ONE_MINUTE : DEFAULT_NOTIFICATION_TIMEOUT);
mSystemUpdateUsageTimeoutMillis = mParser.getDurationMillis(
KEY_SYSTEM_UPDATE_HOLD_DURATION,
COMPRESS_TIME ? 2 * ONE_MINUTE : DEFAULT_SYSTEM_UPDATE_TIMEOUT);
mPredictionTimeoutMillis = mParser.getDurationMillis(
KEY_PREDICTION_TIMEOUT,
COMPRESS_TIME ? 10 * ONE_MINUTE : DEFAULT_PREDICTION_TIMEOUT);
mSyncAdapterTimeoutMillis = mParser.getDurationMillis(
KEY_SYNC_ADAPTER_HOLD_DURATION,
COMPRESS_TIME ? ONE_MINUTE : DEFAULT_SYNC_ADAPTER_TIMEOUT);
mExemptedSyncScheduledNonDozeTimeoutMillis = mParser.getDurationMillis(
KEY_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_HOLD_DURATION,
COMPRESS_TIME ? (ONE_MINUTE / 2)
: DEFAULT_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_TIMEOUT);
mExemptedSyncScheduledDozeTimeoutMillis = mParser.getDurationMillis(
KEY_EXEMPTED_SYNC_SCHEDULED_DOZE_HOLD_DURATION,
COMPRESS_TIME ? ONE_MINUTE
: DEFAULT_EXEMPTED_SYNC_SCHEDULED_DOZE_TIMEOUT);
mExemptedSyncStartTimeoutMillis = mParser.getDurationMillis(
KEY_EXEMPTED_SYNC_START_HOLD_DURATION,
COMPRESS_TIME ? ONE_MINUTE
: DEFAULT_EXEMPTED_SYNC_START_TIMEOUT);
mUnexemptedSyncScheduledTimeoutMillis = mParser.getDurationMillis(
KEY_UNEXEMPTED_SYNC_SCHEDULED_HOLD_DURATION,
COMPRESS_TIME
? ONE_MINUTE : DEFAULT_UNEXEMPTED_SYNC_SCHEDULED_TIMEOUT);
mSystemInteractionTimeoutMillis = mParser.getDurationMillis(
KEY_SYSTEM_INTERACTION_HOLD_DURATION,
COMPRESS_TIME ? ONE_MINUTE : DEFAULT_SYSTEM_INTERACTION_TIMEOUT);
mInitialForegroundServiceStartTimeoutMillis = mParser.getDurationMillis(
KEY_INITIAL_FOREGROUND_SERVICE_START_HOLD_DURATION,
COMPRESS_TIME ? ONE_MINUTE :
DEFAULT_INITIAL_FOREGROUND_SERVICE_START_TIMEOUT);
mInjector.mAutoRestrictedBucketDelayMs = Math.max(
COMPRESS_TIME ? ONE_MINUTE : 2 * ONE_HOUR,
mParser.getDurationMillis(KEY_AUTO_RESTRICTED_BUCKET_DELAY_MS,
COMPRESS_TIME
? ONE_MINUTE : DEFAULT_AUTO_RESTRICTED_BUCKET_DELAY_MS));
mLinkCrossProfileApps = mParser.getBoolean(
KEY_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS,
DEFAULT_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS);
}
// Check if app_idle_enabled has changed. Do this after getting the rest of the settings
// in case we need to change something based on the new values.
setAppIdleEnabled(mInjector.isAppIdleEnabled());
}
long[] parseLongArray(String values, long[] defaults, long[] minValues) {
if (values == null) return defaults;
if (values.isEmpty()) {
// Reset to defaults
return defaults;
} else {
String[] thresholds = values.split("/");
if (thresholds.length == THRESHOLD_BUCKETS.length) {
if (minValues.length != THRESHOLD_BUCKETS.length) {
Slog.wtf(TAG, "minValues array is the wrong size");
// Use zeroes as the minimums.
minValues = new long[THRESHOLD_BUCKETS.length];
}
long[] array = new long[THRESHOLD_BUCKETS.length];
for (int i = 0; i < THRESHOLD_BUCKETS.length; i++) {
try {
if (thresholds[i].startsWith("P") || thresholds[i].startsWith("p")) {
array[i] = Math.max(minValues[i],
Duration.parse(thresholds[i]).toMillis());
} else {
array[i] = Math.max(minValues[i], Long.parseLong(thresholds[i]));
}
} catch (NumberFormatException|DateTimeParseException e) {
return defaults;
}
}
return array;
} else {
return defaults;
}
}
}
}
}