/**
* Allow DeviceIdleController to tell us about what apps are whitelisted.
*/
- public abstract void setDeviceIdleWhitelist(int[] appids);
+ public abstract void setDeviceIdleWhitelist(int[] userAppids, int[] allAppids);
/**
* Update information about which app IDs are on the temp whitelist.
public static final int OP_INSTANT_APP_START_FOREGROUND = 68;
/** @hide Answer incoming phone calls */
public static final int OP_ANSWER_PHONE_CALLS = 69;
+ /** @hide Run jobs when in background */
+ public static final int OP_RUN_ANY_IN_BACKGROUND = 70;
/** @hide */
- public static final int _NUM_OP = 70;
+ public static final int _NUM_OP = 71;
/** Access to coarse location information. */
public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
OP_REQUEST_INSTALL_PACKAGES,
OP_PICTURE_IN_PICTURE,
OP_INSTANT_APP_START_FOREGROUND,
- OP_ANSWER_PHONE_CALLS
+ OP_ANSWER_PHONE_CALLS,
+ OP_RUN_ANY_IN_BACKGROUND,
};
/**
OPSTR_PICTURE_IN_PICTURE,
OPSTR_INSTANT_APP_START_FOREGROUND,
OPSTR_ANSWER_PHONE_CALLS,
+ null, // OP_RUN_ANY_IN_BACKGROUND
};
/**
"PICTURE_IN_PICTURE",
"INSTANT_APP_START_FOREGROUND",
"ANSWER_PHONE_CALLS",
+ "RUN_ANY_IN_BACKGROUND",
};
/**
null, // no permission for entering picture-in-picture on hide
Manifest.permission.INSTANT_APP_FOREGROUND_SERVICE,
Manifest.permission.ANSWER_PHONE_CALLS,
+ null, // no permission for OP_RUN_ANY_IN_BACKGROUND
};
/**
null, // ENTER_PICTURE_IN_PICTURE_ON_HIDE
null, // INSTANT_APP_START_FOREGROUND
null, // ANSWER_PHONE_CALLS
+ null, // OP_RUN_ANY_IN_BACKGROUND
};
/**
false, // ENTER_PICTURE_IN_PICTURE_ON_HIDE
false, // INSTANT_APP_START_FOREGROUND
false, // ANSWER_PHONE_CALLS
+ false, // OP_RUN_ANY_IN_BACKGROUND
};
/**
AppOpsManager.MODE_ALLOWED, // OP_PICTURE_IN_PICTURE
AppOpsManager.MODE_DEFAULT, // OP_INSTANT_APP_START_FOREGROUND
AppOpsManager.MODE_ALLOWED, // ANSWER_PHONE_CALLS
+ AppOpsManager.MODE_ALLOWED, // OP_RUN_ANY_IN_BACKGROUND
};
/**
false, // OP_PICTURE_IN_PICTURE
false,
false, // ANSWER_PHONE_CALLS
+ false, // OP_RUN_ANY_IN_BACKGROUND
};
/**
int getLaunchedFromUid(in IBinder activityToken);
void unstableProviderDied(in IBinder connection);
boolean isIntentSenderAnActivity(in IIntentSender sender);
+ boolean isIntentSenderAForegroundService(in IIntentSender sender);
int startActivityAsUser(in IApplicationThread caller, in String callingPackage,
in Intent intent, in String resolvedType, in IBinder resultTo, in String resultWho,
int requestCode, int flags, in ProfilerInfo profilerInfo,
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.Intent;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
+import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
-import android.os.Looper;
-import android.os.RemoteException;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
-import android.os.Process;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.util.AndroidException;
/**
* @hide
+ * Check whether this PendingIntent will launch a foreground service
+ */
+ public boolean isForegroundService() {
+ try {
+ return ActivityManager.getService()
+ .isIntentSenderAForegroundService(mTarget);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
* Return the Intent of this PendingIntent.
*/
public Intent getIntent() {
import android.util.ArraySet;
import android.util.Log;
import android.util.MemoryIntArray;
+import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.ArrayUtils;
if (makeDefault) {
arg.putBoolean(CALL_METHOD_MAKE_DEFAULT_KEY, true);
}
+
+ // Log all EBS relevant config changes.
+ if (Global.LOW_POWER_MODE_TRIGGER_LEVEL.equals(name)
+ || name.startsWith(Settings.Global.BATTERY_SAVER_CONSTANTS)) {
+ Slog.w("XXX", "Detected write: " + mCallSetCommand + ", " + name + ", args=" + arg, new RuntimeException("HERE"));
+ }
+
IContentProvider cp = mProviderHolder.getProvider(cr);
cp.call(cr.getPackageName(), mCallSetCommand, name, arg);
} catch (RemoteException e) {
*/
public static final String BATTERY_SAVER_CONSTANTS = "battery_saver_constants";
+ /** @hide */
+ public static final String BATTERY_SAVER_USE_RED_BAR = "battery_saver_use_red_bar";
+
/**
* Battery anomaly detection specific settings
* This is encoded as a key=value list, separated by commas.
import android.text.TextUtils;
+import java.util.Collection;
+
/**
* Parses a list of key=value pairs, separated by some delimiter, and puts the results in
* an internal Map. Values can be then queried by key, or if not found, a default value
}
return def;
}
+
+ /**
+ * @return All the keys.
+ */
+ public Collection<String> getKeys() {
+ return mValues.keySet();
+ }
}
MAX_HISTORY_BUFFER = 96*1024; // 96KB
MAX_MAX_HISTORY_BUFFER = 128*1024; // 128KB
} else {
- MAX_HISTORY_ITEMS = 2000;
- MAX_MAX_HISTORY_ITEMS = 3000;
- MAX_WAKELOCKS_PER_UID = 100;
- MAX_HISTORY_BUFFER = 256*1024; // 256KB
- MAX_MAX_HISTORY_BUFFER = 320*1024; // 256KB
+ final int INCREASE_FACTOR = 4;
+ MAX_HISTORY_ITEMS = 2000 * INCREASE_FACTOR;
+ MAX_MAX_HISTORY_ITEMS = 3000 * INCREASE_FACTOR;
+ MAX_WAKELOCKS_PER_UID = 100 * INCREASE_FACTOR;
+ MAX_HISTORY_BUFFER = 256*1024 * INCREASE_FACTOR;
+ MAX_MAX_HISTORY_BUFFER = 320*1024 * INCREASE_FACTOR;
}
}
@Override
public Bundle call(String method, String name, Bundle args) {
+ if (name != null && args != null && Settings.CALL_METHOD_PUT_GLOBAL.equals(method)) {
+ Object value = args.get(Settings.NameValueTable.VALUE);
+
+ if ((value == null)
+ && (Global.LOW_POWER_MODE_TRIGGER_LEVEL.equals(name)
+ || name.startsWith(Settings.Global.BATTERY_SAVER_CONSTANTS)
+ || name.startsWith(Settings.Global.ALARM_MANAGER_CONSTANTS)
+ || name.startsWith(Settings.Global.JOB_SCHEDULER_CONSTANTS))) {
+ Slog.wtf("XXX", "Detected writing null: " + method + ", " + name);
+ return new Bundle();
+ }
+ }
+
+
final int requestingUserId = getRequestingUserId(args);
switch (method) {
case Settings.CALL_METHOD_GET_GLOBAL: {
int mUser = -1; // unspecified
CommandVerb mVerb = CommandVerb.UNSPECIFIED;
+ private final String NULL_MARKER = "";
String mTable = null;
String mKey = null;
String mValue = null;
mVerb = CommandVerb.GET;
} else if ("put".equalsIgnoreCase(arg)) {
mVerb = CommandVerb.PUT;
+ } else if ("putnull".equalsIgnoreCase(arg)) {
+ mVerb = CommandVerb.PUT;
+ mValue = NULL_MARKER;
} else if ("delete".equalsIgnoreCase(arg)) {
mVerb = CommandVerb.DELETE;
} else if ("list".equalsIgnoreCase(arg)) {
perr.println("Bad arguments");
return -1;
}
+ if (mValue == NULL_MARKER) { // Note "==", not equals().
+ mValue = null;
+ }
if (mUser == UserHandle.USER_CURRENT) {
try {
pw.println(" Change the contents of KEY to VALUE.");
pw.println(" TAG to associate with the setting.");
pw.println(" {default} to set as the default, case-insensitive only for global/secure namespace");
+ pw.println(" putnull [--user <USER_ID> | current] NAMESPACE KEY [TAG] [default]");
+ pw.println(" Same as \"put\", except it sets null to KEY.");
pw.println(" delete NAMESPACE KEY");
pw.println(" Delete the entry for KEY.");
pw.println(" reset [--user <USER_ID> | current] NAMESPACE {PACKAGE_NAME | RESET_MODE}");
final boolean powerSave = mBatteryController.isPowerSave();
final boolean anim = !mNoAnimationOnNextBarModeChange && mDeviceInteractive
&& windowState != WINDOW_STATE_HIDDEN && !powerSave;
- if (powerSave && getBarState() == StatusBarState.SHADE) {
+ if (powerSave && getBarState() == StatusBarState.SHADE
+ && (Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.BATTERY_SAVER_USE_RED_BAR, 1) != 0)) {
mode = MODE_WARNING;
}
transitions.transitionTo(mode, anim);
import android.content.pm.PermissionInfo;
import android.database.ContentObserver;
import android.net.Uri;
+import android.os.BatteryManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
+import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
import android.os.Process;
import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.KeyValueListParser;
import android.util.Log;
import android.util.Slog;
import static android.app.AlarmManager.ELAPSED_REALTIME;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.LocalLog;
+import com.android.server.power.BatterySaverPolicy.ServiceType;
class AlarmManagerService extends SystemService {
private static final int RTC_WAKEUP_MASK = 1 << RTC_WAKEUP;
static final boolean DEBUG_ALARM_CLOCK = localLOGV || false;
static final boolean DEBUG_LISTENER_CALLBACK = localLOGV || false;
static final boolean DEBUG_WAKELOCK = localLOGV || false;
+ static final boolean DEBUG_BG_LIMIT = localLOGV || false;
static final boolean RECORD_ALARMS_IN_HISTORY = true;
static final boolean RECORD_DEVICE_IDLE_ALARMS = false;
static final int ALARM_EVENT = 1;
private final Intent mBackgroundIntent
= new Intent().addFlags(Intent.FLAG_FROM_BACKGROUND);
static final IncreasingTimeOrder sIncreasingTimeOrder = new IncreasingTimeOrder();
-
+
static final boolean WAKEUP_STATS = false;
private static final Intent NEXT_ALARM_CLOCK_CHANGED_INTENT =
final LocalLog mLog = new LocalLog(TAG);
AppOpsManager mAppOps;
+ IAppOpsService mAppOpsService;
DeviceIdleController.LocalService mLocalDeviceIdleController;
final Object mLock = new Object();
+ ArraySet<String> mForcedAppStandbyPackages = new ArraySet<>();
+ SparseBooleanArray mForegroundUids = new SparseBooleanArray();
+ // List of alarms per uid deferred due to user applied background restrictions on the source app
+ SparseArray<ArrayList<Alarm>> mPendingBackgroundAlarms = new SparseArray<>();
long mNativeData;
private long mNextWakeup;
private long mNextNonWakeup;
long mAllowWhileIdleMinTime;
int mNumTimeChanged;
+ boolean mForceAppStandby;
+ boolean mForegroundAppExperiment;
+ ChargingReceiver mChargingReceiver;
+
// Bookkeeping about the identity of the "System UI" package, determined at runtime.
/**
*/
int mSystemUiUid;
- /**
- * The current set of user whitelisted apps for device idle mode, meaning these are allowed
- * to freely schedule alarms.
- */
- int[] mDeviceIdleUserWhitelist = new int[0];
+ int[] mDeviceIdleSystemWhitelist = new int[0];
/**
* For each uid, this is the last time we dispatched an "allow while idle" alarm,
*/
final SparseLongArray mLastAllowWhileIdleDispatch = new SparseLongArray();
+ private static final String ALARM_MANAGER_EXPERIMENT_KEY =
+ Settings.Global.ALARM_MANAGER_CONSTANTS + "_om";
+
final static class IdleDispatchEntry {
int uid;
String pkg;
private static final String KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION
= "allow_while_idle_whitelist_duration";
private static final String KEY_LISTENER_TIMEOUT = "listener_timeout";
+ private static final String KEY_BG_RESTRICTIONS_ENABLED = "limit_bg_alarms_enabled";
+ private static final String KEY_FOREGROUND_ONLY_EXPERIMENT = "foreground_only";
private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
private static final long DEFAULT_LISTENER_TIMEOUT = 5 * 1000;
+ private static final boolean DEFAULT_BACKGROUND_RESTRICTIONS_ENABLED = false;
+
// Minimum futurity of a new alarm
public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY;
// Direct alarm listener callback timeout
public long LISTENER_TIMEOUT = DEFAULT_LISTENER_TIMEOUT;
+ public boolean BACKGROUND_ALARMS_BLOCKED = DEFAULT_BACKGROUND_RESTRICTIONS_ENABLED;
+
private ContentResolver mResolver;
private final KeyValueListParser mParser = new KeyValueListParser(',');
private long mLastAllowWhileIdleWhitelistDuration = -1;
mResolver = resolver;
mResolver.registerContentObserver(Settings.Global.getUriFor(
Settings.Global.ALARM_MANAGER_CONSTANTS), false, this);
+ mResolver.registerContentObserver(Settings.Global.getUriFor(
+ ALARM_MANAGER_EXPERIMENT_KEY), false, this);
+
updateConstants();
}
DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION);
LISTENER_TIMEOUT = mParser.getLong(KEY_LISTENER_TIMEOUT,
DEFAULT_LISTENER_TIMEOUT);
+ BACKGROUND_ALARMS_BLOCKED = mParser.getBoolean(KEY_BG_RESTRICTIONS_ENABLED,
+ DEFAULT_BACKGROUND_RESTRICTIONS_ENABLED);
+ maybeRunPendingBackgroundAlarms();
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Background limiting enabled");
+ }
+
+ String expSettings =
+ Settings.Global.getString(mResolver, ALARM_MANAGER_EXPERIMENT_KEY);
+ try {
+ Slog.d(TAG, "Parsing: " + expSettings);
+ mParser.setString(expSettings);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Failed to parse experiments key", e);
+ }
+
+ mForegroundAppExperiment =
+ mParser.getBoolean(KEY_FOREGROUND_ONLY_EXPERIMENT, false);
+ updateForceAppStandby(mForegroundAppExperiment);
updateAllowWhileIdleMinTimeLocked();
updateAllowWhileIdleWhitelistDurationLocked();
static final BatchTimeOrder sBatchOrder = new BatchTimeOrder();
final ArrayList<Batch> mAlarmBatches = new ArrayList<>();
- // set to null if in idle mode; while in this mode, any alarms we don't want
+ // set to non-null if in idle mode; while in this mode, any alarms we don't want
// to run during this time are placed in mPendingWhileIdleAlarms
Alarm mPendingIdleUntil = null;
Alarm mNextWakeFromIdle = null;
setImplLocked(a, true, doValidate);
}
+ /**
+ * Sends alarms that were blocked due to user applied background restrictions - either because
+ * the user lifted those or the uid came to foreground.
+ *
+ * @param uid uid to filter on
+ * @param packageName package to filter on, or null for all packages in uid
+ */
+ void sendPendingBackgroundAlarmsLocked(int uid, String packageName) {
+ final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.get(uid);
+ if (alarmsForUid == null || alarmsForUid.size() == 0) {
+ return;
+ }
+ final ArrayList<Alarm> alarmsToDeliver;
+ if (packageName != null) {
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Sending blocked alarms for uid " + uid + ", package " + packageName);
+ }
+ alarmsToDeliver = new ArrayList<>();
+ for (int i = alarmsForUid.size() - 1; i >= 0; i--) {
+ final Alarm a = alarmsForUid.get(i);
+ if (a.matches(packageName)) {
+ alarmsToDeliver.add(alarmsForUid.remove(i));
+ }
+ }
+ if (alarmsForUid.size() == 0) {
+ mPendingBackgroundAlarms.remove(uid);
+ }
+ } else {
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Sending blocked alarms for uid " + uid);
+ }
+ alarmsToDeliver = alarmsForUid;
+ mPendingBackgroundAlarms.remove(uid);
+ }
+ deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, SystemClock.elapsedRealtime());
+ }
+
+ void sendPendingBackgroundAlarmsForAppIdLocked(int appId) {
+ final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+ final int uid = mPendingBackgroundAlarms.keyAt(i);
+ final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+ if (UserHandle.getAppId(uid) == appId) {
+ alarmsToDeliver.addAll(alarmsForUid);
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
+ deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, SystemClock.elapsedRealtime());
+ }
+
+ private void deliverPendingBackgroundAlarmsLocked(ArrayList<Alarm> alarms, long nowELAPSED) {
+ final int N = alarms.size();
+ boolean hasWakeup = false;
+ for (int i = 0; i < N; i++) {
+ final Alarm alarm = alarms.get(i);
+ if (alarm.wakeup) {
+ hasWakeup = true;
+ }
+ alarm.count = 1;
+ // Recurring alarms may have passed several alarm intervals while the
+ // alarm was kept pending. Send the appropriate trigger count.
+ if (alarm.repeatInterval > 0) {
+ alarm.count += (nowELAPSED - alarm.whenElapsed) / alarm.repeatInterval;
+ // Also schedule its next recurrence
+ final long delta = alarm.count * alarm.repeatInterval;
+ final long nextElapsed = alarm.whenElapsed + delta;
+ setImplLocked(alarm.type, alarm.when + delta, nextElapsed, alarm.windowLength,
+ maxTriggerTime(nowELAPSED, nextElapsed, alarm.repeatInterval),
+ alarm.repeatInterval, alarm.operation, null, null, alarm.flags, true,
+ alarm.workSource, alarm.alarmClock, alarm.uid, alarm.packageName);
+ // Kernel alarms will be rescheduled as needed in setImplLocked
+ }
+ }
+ if (!hasWakeup && checkAllowNonWakeupDelayLocked(nowELAPSED)) {
+ // No need to wakeup for non wakeup alarms
+ if (mPendingNonWakeupAlarms.size() == 0) {
+ mStartCurrentDelayTime = nowELAPSED;
+ mNextNonWakeupDeliveryTime = nowELAPSED
+ + ((currentNonWakeupFuzzLocked(nowELAPSED)*3)/2);
+ }
+ mPendingNonWakeupAlarms.addAll(alarms);
+ mNumDelayedAlarms += alarms.size();
+ } else {
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Waking up to deliver pending blocked alarms");
+ }
+ // Since we are waking up, also deliver any pending non wakeup alarms we have.
+ if (mPendingNonWakeupAlarms.size() > 0) {
+ alarms.addAll(mPendingNonWakeupAlarms);
+ final long thisDelayTime = nowELAPSED - mStartCurrentDelayTime;
+ mTotalDelayTime += thisDelayTime;
+ if (mMaxDelayTime < thisDelayTime) {
+ mMaxDelayTime = thisDelayTime;
+ }
+ mPendingNonWakeupAlarms.clear();
+ }
+ calculateDeliveryPriorities(alarms);
+ Collections.sort(alarms, mAlarmDispatchComparator);
+ deliverAlarmsLocked(alarms, nowELAPSED);
+ }
+ }
+
void restorePendingWhileIdleAlarmsLocked() {
if (RECORD_DEVICE_IDLE_ALARMS) {
IdleDispatchEntry ent = new IdleDispatchEntry();
try {
ActivityManager.getService().registerUidObserver(new UidObserver(),
- ActivityManager.UID_OBSERVER_IDLE, ActivityManager.PROCESS_STATE_UNKNOWN, null);
+ ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_IDLE
+ | ActivityManager.UID_OBSERVER_ACTIVE,
+ ActivityManager.PROCESS_STATE_UNKNOWN, null);
} catch (RemoteException e) {
// ignored; both services live in system_server
}
-
+ mAppOpsService = IAppOpsService.Stub.asInterface(
+ ServiceManager.getService(Context.APP_OPS_SERVICE));
publishBinderService(Context.ALARM_SERVICE, mService);
publishLocalService(LocalService.class, new LocalService());
}
mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
mLocalDeviceIdleController
= LocalServices.getService(DeviceIdleController.LocalService.class);
+ try {
+ mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_IN_BACKGROUND, null,
+ new AppOpsWatcher());
+ } catch (RemoteException rexc) {
+ // Shouldn't happen as they are in the same process.
+ Slog.e(TAG, "AppOps service not reachable", rexc);
+ }
+
+ final PowerManagerInternal pmi = LocalServices.getService(PowerManagerInternal.class);
+ if (pmi != null) {
+ pmi.registerLowPowerModeObserver(
+ new PowerManagerInternal.LowPowerModeListener() {
+ @Override
+ public int getServiceType() {
+ return ServiceType.FORCE_APPS_STANDBY;
+ }
+
+ @Override
+ public void onLowPowerModeChanged(PowerSaveState result) {
+ updateForceAppStandby(result.batterySaverEnabled);
+ }
+ });
+ updateForceAppStandby(
+ pmi.getLowPowerState(ServiceType.FORCE_APPS_STANDBY).batterySaverEnabled);
+ } else {
+ Slog.wtf(TAG, "PowerManagerInternal not found.");
+ }
+ mChargingReceiver = new ChargingReceiver();
}
}
}
}
+ void updateForceAppStandby(boolean enabled) {
+ synchronized (mLock) {
+ int status = ((BatteryManager) getContext().getSystemService(Context.BATTERY_SERVICE))
+ .getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS);
+ boolean charging = status != BatteryManager.BATTERY_STATUS_DISCHARGING;
+ mForceAppStandby = mForegroundAppExperiment ? !charging : enabled;
+ Slog.d(TAG, "Force app standby is: " + mForceAppStandby + " charging: " + charging
+ + " FAE: " + mForegroundAppExperiment + " status: " + status);
+ maybeRunPendingBackgroundAlarms();
+ }
+ }
+
+ void maybeRunPendingBackgroundAlarms() {
+ synchronized (mLock) {
+ if (!isBackgroundRestrictionEnabled()) {
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Running pending-background alarms.");
+ }
+
+ // TODO: remove this code and constant when feature is turned on
+ // deliver all blocked alarms
+ final ArrayList<Alarm> allBlockedAlarms = new ArrayList<>();
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+ allBlockedAlarms.addAll(mPendingBackgroundAlarms.valueAt(i));
+ }
+ mPendingBackgroundAlarms = new SparseArray<>();
+ deliverPendingBackgroundAlarmsLocked(allBlockedAlarms,
+ SystemClock.elapsedRealtime());
+ }
+ }
+ }
+
+ boolean isBackgroundRestrictionEnabled() {
+ synchronized (mLock) {
+ return mForceAppStandby || mConstants.BACKGROUND_ALARMS_BLOCKED;
+ }
+ }
+
void setTimeZoneImpl(String tz) {
if (TextUtils.isEmpty(tz)) {
return;
// Prevent reentrant calls from stepping on each other when writing
// the time zone property
boolean timeZoneWasChanged = false;
- synchronized (this) {
+ synchronized (mLock) {
String current = SystemProperties.get(TIMEZONE_PROPERTY);
if (current == null || !current.equals(zone.getID())) {
if (localLOGV) {
// timing restrictions.
} else if (workSource == null && (callingUid < Process.FIRST_APPLICATION_UID
|| callingUid == mSystemUiUid
- || Arrays.binarySearch(mDeviceIdleUserWhitelist,
+ || Arrays.binarySearch(mDeviceIdleSystemWhitelist,
UserHandle.getAppId(callingUid)) >= 0)) {
flags |= AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
flags &= ~AlarmManager.FLAG_ALLOW_WHILE_IDLE;
};
public final class LocalService {
- public void setDeviceIdleUserWhitelist(int[] appids) {
- setDeviceIdleUserWhitelistImpl(appids);
+ public void setDeviceIdleUserWhitelist(int[] userAppids, int[] systemAppids) {
+ setDeviceIdleWhitelistImpl(systemAppids, mDeviceIdleSystemWhitelist);
+ mDeviceIdleSystemWhitelist = systemAppids;
}
}
void dumpImpl(PrintWriter pw) {
synchronized (mLock) {
pw.println("Current Alarm Manager state:");
+ pw.println(" ForceAppStandby: " + mForceAppStandby);
mConstants.dump(pw);
pw.println();
+ pw.print(" Foreground uids: [");
+ for (int i = 0; i < mForegroundUids.size(); i++) {
+ if (mForegroundUids.valueAt(i)) pw.print(mForegroundUids.keyAt(i) + " ");
+ }
+ pw.println("]");
+ pw.println(" Forced app standby packages: " + mForcedAppStandbyPackages);
final long nowRTC = System.currentTimeMillis();
final long nowELAPSED = SystemClock.elapsedRealtime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
pw.print(" set at "); TimeUtils.formatDuration(mLastWakeupSet, nowELAPSED, pw);
pw.println();
pw.print(" Num time change events: "); pw.println(mNumTimeChanged);
- pw.println(" mDeviceIdleUserWhitelist=" + Arrays.toString(mDeviceIdleUserWhitelist));
+ pw.println(" mDeviceIdleSystemWhitelist=" + Arrays.toString(mDeviceIdleSystemWhitelist));
pw.println();
pw.println(" Next alarm clock information: ");
dumpAlarmList(pw, b.alarms, " ", nowELAPSED, nowRTC, sdf);
}
}
+ pw.println();
+ pw.println(" Pending user blocked background alarms: ");
+ boolean blocked = false;
+ for (int i = 0; i < mPendingBackgroundAlarms.size(); i++) {
+ final ArrayList<Alarm> blockedAlarms = mPendingBackgroundAlarms.valueAt(i);
+ if (blockedAlarms != null && blockedAlarms.size() > 0) {
+ blocked = true;
+ dumpAlarmList(pw, blockedAlarms, " ", nowELAPSED, nowRTC, sdf);
+ }
+ }
+ if (!blocked) {
+ pw.println(" none");
+ }
if (mPendingIdleUntil != null || mPendingWhileIdleAlarms.size() > 0) {
pw.println();
pw.println(" Idle mode state:");
}
}
- void setDeviceIdleUserWhitelistImpl(int[] appids) {
+ void setDeviceIdleWhitelistImpl(int[] appids, int[] toArray) {
synchronized (mLock) {
- mDeviceIdleUserWhitelist = appids;
+ // appids are sorted, just send pending alarms for any new appids added to the whitelist
+ int i = 0, j = 0;
+ while (i < appids.length) {
+ while (j < toArray.length
+ && toArray[j] < appids[i]) {
+ j++;
+ }
+ if (j < toArray.length
+ && appids[i] != toArray[j]) {
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Sending blocked alarms for whitelisted appid " + appids[j]);
+ }
+ sendPendingBackgroundAlarmsForAppIdLocked(appids[j]);
+ }
+ i++;
+ }
}
}
mPendingWhileIdleAlarms.remove(i);
}
}
-
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+ final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+ for (int j = alarmsForUid.size() - 1; j >= 0; j--) {
+ if (alarmsForUid.get(j).matches(operation, directReceiver)) {
+ // Don't set didRemove, since this doesn't impact the scheduled alarms.
+ alarmsForUid.remove(j);
+ }
+ }
+ if (alarmsForUid.size() == 0) {
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
if (didRemove) {
if (DEBUG_BATCH) {
Slog.v(TAG, "remove(operation) changed bounds; rebatching");
mPendingWhileIdleAlarms.remove(i);
}
}
-
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i --) {
+ final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+ for (int j = alarmsForUid.size() - 1; j >= 0; j--) {
+ if (alarmsForUid.get(j).matches(packageName)) {
+ alarmsForUid.remove(j);
+ }
+ }
+ if (alarmsForUid.size() == 0) {
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
if (didRemove) {
if (DEBUG_BATCH) {
Slog.v(TAG, "remove(package) changed bounds; rebatching");
mPendingWhileIdleAlarms.remove(i);
}
}
-
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+ if (mPendingBackgroundAlarms.keyAt(i) == uid) {
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
if (didRemove) {
if (DEBUG_BATCH) {
Slog.v(TAG, "remove(package) changed bounds; rebatching");
mPendingWhileIdleAlarms.remove(i);
}
}
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+ if (UserHandle.getUserId(mPendingBackgroundAlarms.keyAt(i)) == userHandle) {
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
for (int i = mLastAllowWhileIdleDispatch.size() - 1; i >= 0; i--) {
if (UserHandle.getUserId(mLastAllowWhileIdleDispatch.keyAt(i)) == userHandle) {
mLastAllowWhileIdleDispatch.removeAt(i);
}
}
+ private boolean isBackgroundRestrictedLocked(Alarm alarm) {
+ if (!isBackgroundRestrictionEnabled()) {
+ return false;
+ }
+ if (alarm.alarmClock != null) {
+ // Don't block alarm clocks
+ return false;
+ }
+ if (alarm.operation != null
+ && (alarm.operation.isActivity() || alarm.operation.isForegroundService())) {
+ // Don't block starting foreground components
+ return false;
+ }
+ final String sourcePackage =
+ (alarm.operation != null) ? alarm.operation.getCreatorPackage() : alarm.packageName;
+ final int sourceUid = alarm.creatorUid;
+ return shouldForceStandbyPackageLocked(sourcePackage, sourceUid);
+ }
+
+ private boolean shouldForceStandbyPackageLocked(String pkg, int uid) {
+ if (isUidForeground(uid)) {
+ return false;
+ }
+ if (Arrays.binarySearch(mDeviceIdleSystemWhitelist, UserHandle.getAppId(uid)) >= 0) {
+ return false;
+ }
+ if (mForceAppStandby) {
+ return true;
+ }
+ return mForcedAppStandbyPackages.contains(pkg);
+ }
+
+ private boolean isUidForeground(int uid) {
+ return (uid < Process.FIRST_APPLICATION_UID) || mForegroundUids.get(uid);
+ }
+
private native long init();
private native void close(long nativeData);
private native void set(long nativeData, int type, long seconds, long nanoseconds);
continue;
}
}
+ if (isBackgroundRestrictedLocked(alarm)) {
+ // Alarms with FLAG_WAKE_FROM_IDLE or mPendingIdleUntil alarm are not deferred
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG, "Deferring alarm " + alarm + " due to user forced app standby");
+ }
+ ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.get(alarm.creatorUid);
+ if (alarmsForUid == null) {
+ alarmsForUid = new ArrayList<>();
+ mPendingBackgroundAlarms.put(alarm.creatorUid, alarmsForUid);
+ }
+ alarmsForUid.add(alarm);
+ continue;
+ }
alarm.count = 1;
triggerList.add(alarm);
}
}
+ class ChargingReceiver extends BroadcastReceiver {
+
+ public ChargingReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_POWER_CONNECTED);
+ filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
+ getContext().registerReceiver(this, filter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // false as we just want to recheck if FAE is on given battery state.
+ updateForceAppStandby(false);
+ }
+ }
+
+
final class UidObserver extends IUidObserver.Stub {
@Override public void onUidStateChanged(int uid, int procState, long procStateSeq) {
}
@Override public void onUidGone(int uid, boolean disabled) {
- if (disabled) {
- synchronized (mLock) {
+ if (uid < Process.FIRST_APPLICATION_UID) return; // Ignore
+ synchronized (mLock) {
+ if (disabled) {
removeForStoppedLocked(uid);
}
+ mForegroundUids.delete(uid);
}
}
@Override public void onUidActive(int uid) {
+ if (uid < Process.FIRST_APPLICATION_UID) return; // Ignore
+ synchronized (mLock) {
+ if (!mForegroundUids.get(uid)) {
+ mForegroundUids.put(uid, true);
+ sendPendingBackgroundAlarmsLocked(uid, null);
+ }
+ }
}
@Override public void onUidIdle(int uid, boolean disabled) {
- if (disabled) {
- synchronized (mLock) {
+ if (uid < Process.FIRST_APPLICATION_UID) return; // Ignore
+ synchronized (mLock) {
+ if (disabled) {
removeForStoppedLocked(uid);
}
+ mForegroundUids.delete(uid);
}
}
}
};
+ private final class AppOpsWatcher extends IAppOpsCallback.Stub {
+ @Override
+ public void opChanged(int op, int uid, String packageName) throws RemoteException {
+ synchronized (mLock) {
+ final int mode = mAppOpsService.checkOperation(op, uid, packageName);
+ if (DEBUG_BG_LIMIT) {
+ Slog.d(TAG,
+ "Appop changed for " + uid + ", " + packageName + " to " + mode);
+ }
+ final boolean changed;
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ changed = mForcedAppStandbyPackages.add(packageName);
+ } else {
+ changed = mForcedAppStandbyPackages.remove(packageName);
+ }
+ if (changed && mode == AppOpsManager.MODE_ALLOWED) {
+ sendPendingBackgroundAlarmsLocked(uid, packageName);
+ }
+ }
+ }
+ }
+
private final BroadcastStats getStatsLocked(PendingIntent pi) {
String pkg = pi.getCreatorPackage();
int uid = pi.getCreatorUid();
import android.util.TimeUtils;
import android.util.Xml;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IAppOpsCallback;
import com.android.internal.app.IAppOpsService;
import com.android.internal.os.Zygote;
static final String TAG = "AppOps";
static final boolean DEBUG = false;
+ private static final int NO_VERSION = -1;
+ /** Increment by one every time and add the corresponding upgrade logic in
+ * {@link #upgradeLocked(int)} below. The first version was 1 */
+ private static final int CURRENT_VERSION = 1;
+
// Write at most every 30 minutes.
static final long WRITE_DELAY = DEBUG ? 1000 : 30*60*1000;
}
};
- private final SparseArray<UidState> mUidStates = new SparseArray<>();
+ @VisibleForTesting
+ final SparseArray<UidState> mUidStates = new SparseArray<>();
/*
* These are app op restrictions imposed per user from various parties.
*/
private final ArrayMap<IBinder, ClientRestrictionState> mOpUserRestrictions = new ArrayMap<>();
- private static final class UidState {
+ @VisibleForTesting
+ static final class UidState {
public final int uid;
public ArrayMap<String, Ops> pkgOps;
public SparseIntArray opModes;
}
void readState() {
+ int oldVersion = NO_VERSION;
synchronized (mFile) {
synchronized (this) {
FileInputStream stream;
throw new IllegalStateException("no start tag found");
}
+ final String versionString = parser.getAttributeValue(null, "v");
+ if (versionString != null) {
+ oldVersion = Integer.parseInt(versionString);
+ }
+
int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
}
}
}
+ synchronized (this) {
+ upgradeLocked(oldVersion);
+ }
+ }
+
+ private void upgradeRunAnyInBackgroundLocked() {
+ for (int i = 0; i < mUidStates.size(); i++) {
+ final UidState uidState = mUidStates.valueAt(i);
+ if (uidState == null) {
+ continue;
+ }
+ if (uidState.opModes != null) {
+ final int idx = uidState.opModes.indexOfKey(AppOpsManager.OP_RUN_IN_BACKGROUND);
+ if (idx >= 0) {
+ uidState.opModes.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
+ uidState.opModes.valueAt(idx));
+ }
+ }
+ if (uidState.pkgOps == null) {
+ continue;
+ }
+ for (int j = 0; j < uidState.pkgOps.size(); j++) {
+ Ops ops = uidState.pkgOps.valueAt(j);
+ if (ops != null) {
+ final Op op = ops.get(AppOpsManager.OP_RUN_IN_BACKGROUND);
+ if (op != null && op.mode != AppOpsManager.opToDefaultMode(op.op)) {
+ final Op copy = new Op(op.uid, op.packageName,
+ AppOpsManager.OP_RUN_ANY_IN_BACKGROUND);
+ copy.mode = op.mode;
+ ops.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, copy);
+ }
+ }
+ }
+ }
+ }
+
+ private void upgradeLocked(int oldVersion) {
+ if (oldVersion >= CURRENT_VERSION) {
+ return;
+ }
+ Slog.d(TAG, "Upgrading app-ops xml from version " + oldVersion + " to " + CURRENT_VERSION);
+ switch (oldVersion) {
+ case NO_VERSION:
+ upgradeRunAnyInBackgroundLocked();
+ // fall through
+ case 1:
+ // for future upgrades
+ }
+ scheduleFastWriteLocked();
}
void readUidOps(XmlPullParser parser) throws NumberFormatException,
out.setOutput(stream, StandardCharsets.UTF_8.name());
out.startDocument(null, true);
out.startTag(null, "app-ops");
+ out.attribute(null, "v", String.valueOf(CURRENT_VERSION));
final int uidStateCount = mUidStates.size();
for (int i = 0; i < uidStateCount; i++) {
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.KeyValueListParser;
private static final String KEY_NOTIFICATION_WHITELIST_DURATION =
"notification_whitelist_duration";
+ private static final String KEY_WHITELISTED_PACKAGES = "whitelist_pkgs";
+
/**
* This is the time, after becoming inactive, that we go in to the first
* light-weight idle mode.
*/
public long NOTIFICATION_WHITELIST_DURATION;
+ public String[] WHITELISTED_PACKAGES = new String[0];
+
private final ContentResolver mResolver;
private final boolean mHasWatch;
private final KeyValueListParser mParser = new KeyValueListParser(',');
+ private static final String SETTING = Settings.Global.DEVICE_IDLE_CONSTANTS + "_om";
+
public Constants(Handler handler, ContentResolver resolver) {
super(handler);
mResolver = resolver;
mHasWatch = getContext().getPackageManager().hasSystemFeature(
PackageManager.FEATURE_WATCH);
- mResolver.registerContentObserver(Settings.Global.getUriFor(
- mHasWatch ? Settings.Global.DEVICE_IDLE_CONSTANTS_WATCH
- : Settings.Global.DEVICE_IDLE_CONSTANTS),
+ mResolver.registerContentObserver(Settings.Global.getUriFor(SETTING),
false, this);
updateConstants();
}
private void updateConstants() {
synchronized (DeviceIdleController.this) {
+ final String config = Settings.Global.getString(mResolver, SETTING);
try {
- mParser.setString(Settings.Global.getString(mResolver,
- mHasWatch ? Settings.Global.DEVICE_IDLE_CONSTANTS_WATCH
- : Settings.Global.DEVICE_IDLE_CONSTANTS));
+ mParser.setString(config);
} catch (IllegalArgumentException e) {
// Failed to parse the settings string, log this and move on
// with defaults.
- Slog.e(TAG, "Bad device idle settings", e);
+ Slog.e(TAG, "Bad device idle settings: " + config, e);
}
LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getLong(
KEY_SMS_TEMP_APP_WHITELIST_DURATION, 20 * 1000L);
NOTIFICATION_WHITELIST_DURATION = mParser.getLong(
KEY_NOTIFICATION_WHITELIST_DURATION, 30 * 1000L);
+
+
+ WHITELISTED_PACKAGES = mParser.getString(KEY_WHITELISTED_PACKAGES, "").split(",");
+
+ boolean changed = false;
+ for (String name : WHITELISTED_PACKAGES) {
+ if (TextUtils.isEmpty(name)) {
+ continue;
+ }
+
+ try {
+ ApplicationInfo ai = getContext().getPackageManager()
+ .getApplicationInfo(name, PackageManager.MATCH_ANY_USER);
+ if (mPowerSaveWhitelistUserApps.put(name, UserHandle.getAppId(ai.uid))
+ == null) {
+ changed = true;
+ }
+ } catch (NameNotFoundException e) {
+ // Ignore
+ }
+ }
+ if (changed) {
+ reportPowerSaveWhitelistChangedLocked();
+ updateWhitelistAppIdsLocked();
+ writeConfigFileLocked();
+ }
}
}
pw.print(" "); pw.print(KEY_NOTIFICATION_WHITELIST_DURATION); pw.print("=");
TimeUtils.formatDuration(NOTIFICATION_WHITELIST_DURATION, pw);
pw.println();
+
+ pw.print(" "); pw.print(KEY_WHITELISTED_PACKAGES); pw.print("=");
+ pw.println(Arrays.toString(WHITELISTED_PACKAGES));
}
}
filter.addAction(Intent.ACTION_SCREEN_ON);
getContext().registerReceiver(mInteractivityReceiver, filter);
- mLocalActivityManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
+ mLocalActivityManager.setDeviceIdleWhitelist(mPowerSaveWhitelistUserAppIdArray, mPowerSaveWhitelistAllAppIdArray);
mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
- mLocalAlarmManager.setDeviceIdleUserWhitelist(mPowerSaveWhitelistUserAppIdArray);
+ mLocalAlarmManager.setDeviceIdleUserWhitelist(mPowerSaveWhitelistUserAppIdArray, mPowerSaveWhitelistAllAppIdArray);
updateInteractivityLocked();
}
Slog.d(TAG, "Setting activity manager whitelist to "
+ Arrays.toString(mPowerSaveWhitelistAllAppIdArray));
}
- mLocalActivityManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
+ mLocalActivityManager.setDeviceIdleWhitelist(mPowerSaveWhitelistUserAppIdArray, mPowerSaveWhitelistAllAppIdArray);
}
if (mLocalPowerManager != null) {
if (DEBUG) {
Slog.d(TAG, "Setting alarm whitelist to "
+ Arrays.toString(mPowerSaveWhitelistUserAppIdArray));
}
- mLocalAlarmManager.setDeviceIdleUserWhitelist(mPowerSaveWhitelistUserAppIdArray);
+ mLocalAlarmManager.setDeviceIdleUserWhitelist(mPowerSaveWhitelistUserAppIdArray, mPowerSaveWhitelistAllAppIdArray);
}
}
import android.os.PersistableBundle;
import android.os.PowerManager;
import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import com.android.server.firewall.IntentFirewall;
import com.android.server.pm.Installer;
import com.android.server.pm.Installer.InstallerException;
+import com.android.server.power.BatterySaverPolicy.ServiceType;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.vr.VrManagerInternal;
import com.android.server.wm.PinnedStackWindowController;
*/
int[] mDeviceIdleWhitelist = new int[0];
+ int[] mDeviceIdleUserWhitelist = new int[0];
+
/**
* Set of app ids that are temporarily allowed to escape bg check due to high-pri message
*/
}
@Override
+ public boolean isIntentSenderAForegroundService(IIntentSender pendingResult) {
+ if (pendingResult instanceof PendingIntentRecord) {
+ final PendingIntentRecord res = (PendingIntentRecord) pendingResult;
+ return res.key.type == ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE;
+ }
+ return false;
+ }
+
+ @Override
public Intent getIntentForIntentSender(IIntentSender pendingResult) {
enforceCallingPermission(Manifest.permission.GET_INTENT_SENDER_INTENT,
"getIntentForIntentSender()");
}
}
+//xxx
// Unified app-op and target sdk check
int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
// Apps that target O+ are always subject to background check
}
return ActivityManager.APP_START_MODE_DELAYED_RIGID;
}
+ if (uid >= Process.FIRST_APPLICATION_UID && mForceAppStandby) {
+
+ // This is used for implicit broadcasts. User-whitelist should still affect it.
+ if (!isOnDeviceIdleUserWhitelistLocked(uid)) {
+ return ActivityManager.APP_START_MODE_DELAYED;
+ }
+ }
+
// ...and legacy apps get an AppOp check
int appop = mAppOpsService.noteOperation(AppOpsManager.OP_RUN_IN_BACKGROUND,
uid, packageName);
|| mPendingTempWhitelist.indexOfKey(uid) >= 0;
}
+ boolean isOnDeviceIdleUserWhitelistLocked(int uid) {
+ final int appId = UserHandle.getAppId(uid);
+ return Arrays.binarySearch(mDeviceIdleUserWhitelist, appId) >= 0;
+ }
+
private ProviderInfo getProviderInfoLocked(String authority, int userHandle, int pmFlags) {
ProviderInfo pi = null;
ContentProviderRecord cpr = mProviderMap.getProviderByName(authority, userHandle);
mSystemReady = true;
}
+ final PowerManagerInternal pmi = LocalServices.getService(PowerManagerInternal.class);
+ if (pmi != null) {
+ pmi.registerLowPowerModeObserver(
+ new PowerManagerInternal.LowPowerModeListener() {
+ @Override
+ public int getServiceType() {
+ return ServiceType.FORCE_APPS_STANDBY;
+ }
+
+ @Override
+ public void onLowPowerModeChanged(PowerSaveState result) {
+ updateForceAppStandby(result.batterySaverEnabled);
+ }
+ });
+ updateForceAppStandby(
+ pmi.getLowPowerState(ServiceType.FORCE_APPS_STANDBY).batterySaverEnabled);
+ } else {
+ Slog.wtf(TAG, "PowerManagerInternal not found.");
+ }
+
try {
sTheRealBuildSerial = IDeviceIdentifiersPolicyService.Stub.asInterface(
ServiceManager.getService(Context.DEVICE_IDENTIFIERS_SERVICE))
}
}
+ boolean mForceAppStandby;
+
+ void updateForceAppStandby(boolean enabled) {
+ synchronized (this) {
+ if (mForceAppStandby != enabled) {
+ mForceAppStandby = enabled;
+
+ if (mForceAppStandby) {
+ Slog.w(TAG, "Forcing app standby.");
+
+ doStopUidForIdleUidsLocked();
+ }
+ }
+ }
+ }
+
void killAppAtUsersRequest(ProcessRecord app, Dialog fromDialog) {
synchronized (this) {
mAppErrors.killAppAtUserRequestLocked(app, fromDialog);
}
}
pw.println(" mDeviceIdleWhitelist=" + Arrays.toString(mDeviceIdleWhitelist));
+ pw.println(" mDeviceIdleUserWhitelist=" + Arrays.toString(mDeviceIdleUserWhitelist));
pw.println(" mDeviceIdleTempWhitelist=" + Arrays.toString(mDeviceIdleTempWhitelist));
if (mPendingTempWhitelist.size() > 0) {
pw.println(" mPendingTempWhitelist:");
}
}
+ void doStopUidForIdleUidsLocked() {
+ final int size = mActiveUids.size();
+ for (int i = 0; i < size; i++) {
+ final int uid = mActiveUids.keyAt(i);
+ if (uid < Process.FIRST_APPLICATION_UID) {
+ continue;
+ }
+ final UidRecord uidRec = mActiveUids.valueAt(i);
+ if (!uidRec.idle) {
+ continue;
+ }
+ doStopUidLocked(uidRec.uid, uidRec);
+ }
+ }
+
final void doStopUidLocked(int uid, final UidRecord uidRec) {
mServices.stopInBackgroundLocked(uid);
enqueueUidChangeLocked(uidRec, uid, UidRecord.CHANGE_IDLE);
}
@Override
- public void setDeviceIdleWhitelist(int[] appids) {
+ public void setDeviceIdleWhitelist(int[] userAppids, int[] allAppids) {
synchronized (ActivityManagerService.this) {
- mDeviceIdleWhitelist = appids;
+ mDeviceIdleUserWhitelist = userAppids;
+ mDeviceIdleWhitelist = allAppids;
}
}
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.ContentObserver;
import android.net.Uri;
+import android.os.BatteryManager;
import android.os.BatteryStats;
import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
import android.os.Process;
import android.os.PowerManager;
import android.os.RemoteException;
import com.android.server.LocalServices;
import com.android.server.job.JobStore.JobStatusFunctor;
import com.android.server.job.controllers.AppIdleController;
+import com.android.server.job.controllers.BackgroundJobsController;
import com.android.server.job.controllers.BatteryController;
import com.android.server.job.controllers.ConnectivityController;
import com.android.server.job.controllers.ContentObserverController;
import com.android.server.job.controllers.StateController;
import com.android.server.job.controllers.StorageController;
import com.android.server.job.controllers.TimeController;
+import com.android.server.power.BatterySaverPolicy.ServiceType;
import libcore.util.EmptyArray;
/** The maximum number of jobs that we allow an unprivileged app to schedule */
private static final int MAX_JOBS_PER_APP = 100;
+ private static final String JOB_SCHEDULER_EXPERIMENT_KEY =
+ Settings.Global.JOB_SCHEDULER_CONSTANTS + "_om";
+
/** Global local for all job scheduler state. */
final Object mLock = new Object();
BatteryController mBatteryController;
/** Need direct access to this for testing. */
StorageController mStorageController;
+ /** Need directly for sending uid state changes */
+ private BackgroundJobsController mBackgroundJobsController;
/**
* Queue of pending jobs. The JobServiceContext class will receive jobs from this list
* when ready to execute them.
*/
int[] mTmpAssignPreferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT];
+ boolean mForceAppStandby;
+ boolean mForegroundAppExperiment;
+ ChargingReceiver mChargingReceiver;
+
/**
* All times are in milliseconds. These constants are kept synchronized with the system
* global Settings. Any access to this class or its fields should be done while
private static final String KEY_MAX_WORK_RESCHEDULE_COUNT = "max_work_reschedule_count";
private static final String KEY_MIN_LINEAR_BACKOFF_TIME = "min_linear_backoff_time";
private static final String KEY_MIN_EXP_BACKOFF_TIME = "min_exp_backoff_time";
+ private static final String KEY_BG_JOBS_RESTRICTED = "bg_jobs_restricted";
+ private static final String KEY_FOREGROUND_ONLY_EXPERIMENT = "foreground_only";
private static final int DEFAULT_MIN_IDLE_COUNT = 1;
private static final int DEFAULT_MIN_CHARGING_COUNT = 1;
private static final int DEFAULT_BG_MODERATE_JOB_COUNT = 4;
private static final int DEFAULT_BG_LOW_JOB_COUNT = 1;
private static final int DEFAULT_BG_CRITICAL_JOB_COUNT = 1;
+ private static final boolean DEFAULT_BG_JOBS_RESTRICTED = false;
private static final int DEFAULT_MAX_STANDARD_RESCHEDULE_COUNT = Integer.MAX_VALUE;
private static final int DEFAULT_MAX_WORK_RESCHEDULE_COUNT = Integer.MAX_VALUE;
private static final long DEFAULT_MIN_LINEAR_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS;
*/
long MIN_EXP_BACKOFF_TIME = DEFAULT_MIN_EXP_BACKOFF_TIME;
+ /**
+ * Runtime switch for throttling background jobs
+ */
+ boolean BACKGROUND_JOBS_RESTRICTED = DEFAULT_BG_JOBS_RESTRICTED;
+
private ContentResolver mResolver;
private final KeyValueListParser mParser = new KeyValueListParser(',');
mResolver = resolver;
mResolver.registerContentObserver(Settings.Global.getUriFor(
Settings.Global.JOB_SCHEDULER_CONSTANTS), false, this);
+ mResolver.registerContentObserver(Settings.Global.getUriFor(
+ JOB_SCHEDULER_EXPERIMENT_KEY), false, this);
updateConstants();
}
DEFAULT_MIN_LINEAR_BACKOFF_TIME);
MIN_EXP_BACKOFF_TIME = mParser.getLong(KEY_MIN_EXP_BACKOFF_TIME,
DEFAULT_MIN_EXP_BACKOFF_TIME);
+ final boolean bgJobsRestricted = mParser.getBoolean(KEY_BG_JOBS_RESTRICTED,
+ DEFAULT_BG_JOBS_RESTRICTED);
+ if (bgJobsRestricted != BACKGROUND_JOBS_RESTRICTED) {
+ BACKGROUND_JOBS_RESTRICTED = bgJobsRestricted;
+ maybeEnableBackgroundRestriction();
+ }
+
+ String expSettings =
+ Settings.Global.getString(mResolver, JOB_SCHEDULER_EXPERIMENT_KEY);
+ try {
+ Slog.d(TAG, "Parsing: " + expSettings);
+ mParser.setString(expSettings);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Bad job scheduler experiment settings.");
+ }
+ mForegroundAppExperiment =
+ mParser.getBoolean(KEY_FOREGROUND_ONLY_EXPERIMENT, false);
+ updateForceAppStandby(mForegroundAppExperiment);
}
}
pw.print(" "); pw.print(KEY_MIN_EXP_BACKOFF_TIME); pw.print("=");
pw.print(MIN_EXP_BACKOFF_TIME); pw.println();
+
+ pw.print(" "); pw.print(KEY_BG_JOBS_RESTRICTED); pw.print("=");
+ pw.print(BACKGROUND_JOBS_RESTRICTED); pw.println();
}
}
if (disabled) {
cancelJobsForUid(uid, "uid gone");
}
+ synchronized (mLock) {
+ mBackgroundJobsController.setUidActiveLocked(uid, false);
+ }
}
@Override public void onUidActive(int uid) throws RemoteException {
+ synchronized (mLock) {
+ mBackgroundJobsController.setUidActiveLocked(uid, true);
+ }
}
@Override public void onUidIdle(int uid, boolean disabled) {
if (disabled) {
cancelJobsForUid(uid, "app uid idle");
}
+ synchronized (mLock) {
+ mBackgroundJobsController.setUidActiveLocked(uid, false);
+ }
}
@Override public void onUidCachedChanged(int uid, boolean cached) {
mControllers.add(mBatteryController);
mStorageController = StorageController.get(this);
mControllers.add(mStorageController);
+ mBackgroundJobsController = BackgroundJobsController.get(this);
+ mControllers.add(mBackgroundJobsController);
mControllers.add(AppIdleController.get(this));
mControllers.add(ContentObserverController.get(this));
mControllers.add(DeviceIdleJobsController.get(this));
try {
ActivityManager.getService().registerUidObserver(mUidObserver,
ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE
- | ActivityManager.UID_OBSERVER_IDLE, ActivityManager.PROCESS_STATE_UNKNOWN,
- null);
+ | ActivityManager.UID_OBSERVER_IDLE | ActivityManager.UID_OBSERVER_ACTIVE,
+ ActivityManager.PROCESS_STATE_UNKNOWN, null);
} catch (RemoteException e) {
// ignored; both services live in system_server
}
+
+ final PowerManagerInternal pmi = LocalServices.getService(PowerManagerInternal.class);
+ if (pmi != null) {
+ pmi.registerLowPowerModeObserver(
+ new PowerManagerInternal.LowPowerModeListener() {
+ @Override
+ public int getServiceType() {
+ return ServiceType.FORCE_APPS_STANDBY;
+ }
+
+ @Override
+ public void onLowPowerModeChanged(PowerSaveState result) {
+ updateForceAppStandby(result.batterySaverEnabled);
+ }
+ });
+ updateForceAppStandby(
+ pmi.getLowPowerState(ServiceType.FORCE_APPS_STANDBY).batterySaverEnabled);
+ } else {
+ Slog.wtf(TAG, "PowerManagerInternal not found.");
+ }
+
// Remove any jobs that are not associated with any of the current users.
cancelJobsForNonExistentUsers();
+ mChargingReceiver = new ChargingReceiver();
} else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
synchronized (mLock) {
// Let's go!
}
}
+ void updateForceAppStandby(boolean enabled) {
+ synchronized (this) {
+ int status = ((BatteryManager) getContext().getSystemService(Context.BATTERY_SERVICE))
+ .getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS);
+ boolean charging = status != BatteryManager.BATTERY_STATUS_DISCHARGING;
+ mForceAppStandby = mForegroundAppExperiment ? !charging : enabled;
+ Slog.d(TAG, "Force app standby is: " + mForceAppStandby + " charging: " + charging
+ + " FAE: " + mForegroundAppExperiment + " status: " + status);
+ maybeEnableBackgroundRestriction();
+ }
+ }
+
+ void maybeEnableBackgroundRestriction() {
+ mBackgroundJobsController.enableRestrictionsLocked(mConstants.BACKGROUND_JOBS_RESTRICTED,
+ mForceAppStandby);
+ }
+
/**
* Called when we have a job status object that we need to insert in our
* {@link com.android.server.job.JobStore}, and make sure all the relevant controllers know
final long nowElapsed = SystemClock.elapsedRealtime();
final long nowUptime = SystemClock.uptimeMillis();
synchronized (mLock) {
+ pw.println("ForceAppStandby: " + mForceAppStandby);
mConstants.dump(pw);
pw.println();
pw.println("Started users: " + Arrays.toString(mStartedUsers));
}
pw.println();
}
+
+ class ChargingReceiver extends BroadcastReceiver {
+
+ public ChargingReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_POWER_CONNECTED);
+ filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
+ getContext().registerReceiver(this, filter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // false as we just want to recheck if FAE is on given battery state.
+ updateForceAppStandby(false);
+ }
+ }
}
--- /dev/null
+/*
+ * 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.job.controllers;
+
+import android.app.AppOpsManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.IDeviceIdleController;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
+import com.android.internal.util.ArrayUtils;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobStore;
+
+import java.io.PrintWriter;
+
+public final class BackgroundJobsController extends StateController {
+
+ private static final String LOG_TAG = "BackgroundJobsController";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG;
+
+ // Singleton factory
+ private static final Object sCreationLock = new Object();
+ private static volatile BackgroundJobsController sController;
+
+ /* Runtime switch to keep feature under wraps */
+ private boolean mEnableSwitch;
+
+ /** Enable regardless of what app ops says */
+ private boolean mForceEnableSwitch;
+ private final JobSchedulerService mJobSchedulerService;
+ private final IAppOpsService mAppOpsService;
+ private final IDeviceIdleController mDeviceIdleController;
+
+ private final SparseBooleanArray mForegroundUids;
+ private int[] mPowerWhitelistedAppIds;
+ private int[] mTempWhitelistedAppIds;
+ /**
+ * Only tracks jobs for which source package app op RUN_ANY_IN_BACKGROUND is not ALLOWED.
+ * Maps jobs to the sourceUid unlike the global {@link JobSchedulerService#mJobs JobStore}
+ * which uses callingUid.
+ */
+ private SparseArray<ArraySet<JobStatus>> mTrackedJobs;
+
+ public static BackgroundJobsController get(JobSchedulerService service) {
+ synchronized (sCreationLock) {
+ if (sController == null) {
+ sController = new BackgroundJobsController(service, service.getContext(),
+ service.getLock());
+ }
+ return sController;
+ }
+ }
+
+ private BroadcastReceiver mDozeWhitelistReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ synchronized (mLock) {
+ try {
+ switch (intent.getAction()) {
+ case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
+ mPowerWhitelistedAppIds = mDeviceIdleController.getAppIdWhitelist();
+ break;
+ case PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED:
+ mTempWhitelistedAppIds = mDeviceIdleController.getAppIdTempWhitelist();
+ break;
+ }
+ } catch (RemoteException rexc) {
+ Slog.e(LOG_TAG, "Device idle controller not reachable");
+ }
+ if (checkAllTrackedJobsLocked()) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ }
+ };
+
+ private BackgroundJobsController(JobSchedulerService service, Context context, Object lock) {
+ super(service, context, lock);
+ mJobSchedulerService = service;
+ mAppOpsService = IAppOpsService.Stub.asInterface(
+ ServiceManager.getService(Context.APP_OPS_SERVICE));
+ mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
+ ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
+
+ mForegroundUids = new SparseBooleanArray();
+ mTrackedJobs = new SparseArray<>();
+ try {
+ mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_IN_BACKGROUND, null,
+ new AppOpsWatcher());
+ mPowerWhitelistedAppIds = mDeviceIdleController.getAppIdWhitelist();
+ mTempWhitelistedAppIds = mDeviceIdleController.getAppIdTempWhitelist();
+ } catch (RemoteException rexc) {
+ // Shouldn't happen as they are in the same process.
+ Slog.e(LOG_TAG, "AppOps or DeviceIdle service not reachable", rexc);
+ }
+ IntentFilter powerWhitelistFilter = new IntentFilter();
+ powerWhitelistFilter.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+ powerWhitelistFilter.addAction(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
+ context.registerReceiverAsUser(mDozeWhitelistReceiver, UserHandle.ALL, powerWhitelistFilter,
+ null, null);
+
+ mForceEnableSwitch = false;
+ mEnableSwitch = false;
+ }
+
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+ final int uid = jobStatus.getSourceUid();
+ final String packageName = jobStatus.getSourcePackageName();
+ try {
+ final boolean restrict = mForceEnableSwitch || (mAppOpsService.checkOperation(
+ AppOpsManager.OP_RUN_IN_BACKGROUND,
+ uid, packageName) != AppOpsManager.MODE_ALLOWED);
+ if (!restrict) {
+ jobStatus.setBackgroundNotRestrictedConstraintSatisfied(true);
+ return;
+ }
+ } catch (RemoteException rexc) {
+ Slog.e(LOG_TAG, "Cannot reach app ops service", rexc);
+ }
+ jobStatus.setBackgroundNotRestrictedConstraintSatisfied(canRunJobLocked(uid));
+ startTrackingJobLocked(jobStatus);
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+ boolean forUpdate) {
+ stopTrackingJobLocked(jobStatus);
+ }
+
+ /* Called by JobSchedulerService to report uid state changes between active and idle */
+ public void setUidActiveLocked(int uid, boolean active) {
+ if (uid < Process.FIRST_APPLICATION_UID) return; // Ignore
+
+ final boolean changed = (active != mForegroundUids.get(uid));
+ if (!changed) {
+ return;
+ }
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "uid " + uid + " going to " + (active ? "fg" : "bg"));
+ }
+ if (active) {
+ mForegroundUids.put(uid, true);
+ } else {
+ mForegroundUids.delete(uid);
+ }
+ if (checkTrackedJobsForUidLocked(uid)) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+
+ @Override
+ public void dumpControllerStateLocked(final PrintWriter pw, final int filterUid) {
+ pw.println("Background restrictions: global switch = " + mEnableSwitch
+ + " force = " + mEnableSwitch);
+ pw.print("Foreground uids: [");
+ for (int i = 0; i < mForegroundUids.size(); i++) {
+ if (mForegroundUids.valueAt(i)) pw.print(mForegroundUids.keyAt(i) + " ");
+ }
+ pw.println("]");
+ mJobSchedulerService.getJobStore().forEachJob(new JobStore.JobStatusFunctor() {
+ @Override
+ public void process(JobStatus jobStatus) {
+ if (!jobStatus.shouldDump(filterUid)) {
+ return;
+ }
+ final int uid = jobStatus.getSourceUid();
+ pw.print(" #");
+ jobStatus.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, uid);
+ pw.print(mForegroundUids.get(uid) ? " foreground" : " background");
+ if (isWhitelistedLocked(uid)) {
+ pw.print(", whitelisted");
+ }
+ pw.print(": ");
+ pw.print(jobStatus.getSourcePackageName());
+ pw.print(" [background restrictions");
+ final ArraySet<JobStatus> jobsForUid = mTrackedJobs.get(uid);
+ pw.print(jobsForUid != null && jobsForUid.contains(jobStatus) ? " on]" : " off]");
+ if ((jobStatus.satisfiedConstraints
+ & JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
+ pw.println(" RUNNABLE");
+ } else {
+ pw.println(" WAITING");
+ }
+ }
+ });
+ }
+
+ public void enableRestrictionsLocked(boolean enable, boolean forceEnabled) {
+ mEnableSwitch = enable;
+ mForceEnableSwitch = forceEnabled;
+ Slog.d(LOG_TAG, "Background jobs restrictions switch changed to " + mEnableSwitch
+ + " force " + mForceEnableSwitch);
+ if (checkAllTrackedJobsLocked()) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+
+ void startTrackingJobLocked(JobStatus jobStatus) {
+ final int uid = jobStatus.getSourceUid();
+ ArraySet<JobStatus> jobsForUid = mTrackedJobs.get(uid);
+ if (jobsForUid == null) {
+ jobsForUid = new ArraySet<>();
+ mTrackedJobs.put(uid, jobsForUid);
+ }
+ jobsForUid.add(jobStatus);
+ }
+
+ void stopTrackingJobLocked(JobStatus jobStatus) {
+ final int uid = jobStatus.getSourceUid();
+ ArraySet<JobStatus> jobsForUid = mTrackedJobs.get(uid);
+ if (jobsForUid != null) {
+ jobsForUid.remove(jobStatus);
+ }
+ }
+
+ boolean checkAllTrackedJobsLocked() {
+ boolean changed = false;
+ for (int i = 0; i < mTrackedJobs.size(); i++) {
+ changed |= checkTrackedJobsForUidLocked(mTrackedJobs.keyAt(i));
+ }
+ return changed;
+ }
+
+ private boolean checkTrackedJobsForUidLocked(int uid) {
+ final ArraySet<JobStatus> jobsForUid = mTrackedJobs.get(uid);
+ boolean changed = false;
+ if (jobsForUid != null) {
+ for (int i = 0; i < jobsForUid.size(); i++) {
+ JobStatus jobStatus = jobsForUid.valueAt(i);
+ changed |= jobStatus.setBackgroundNotRestrictedConstraintSatisfied(
+ canRunJobLocked(uid));
+ }
+ }
+ return changed;
+ }
+
+ boolean isWhitelistedLocked(int uid) {
+ int appId = UserHandle.getAppId(uid);
+ return ArrayUtils.contains(mTempWhitelistedAppIds, appId)
+ || ArrayUtils.contains(mPowerWhitelistedAppIds, appId);
+ }
+
+ boolean canRunJobLocked(int uid) {
+ return !(mEnableSwitch || mForceEnableSwitch)
+ || isUidForeground(uid) || isWhitelistedLocked(uid);
+ }
+
+ private boolean isUidForeground(int uid) {
+ return (uid < Process.FIRST_APPLICATION_UID) || mForegroundUids.get(uid);
+ }
+
+ private final class AppOpsWatcher extends IAppOpsCallback.Stub {
+ @Override
+ public void opChanged(int op, int uid, String packageName) throws RemoteException {
+ synchronized (mLock) {
+ final int mode = mAppOpsService.checkOperation(op, uid, packageName);
+ if (DEBUG) {
+ Slog.d(LOG_TAG,
+ "Appop changed for " + uid + ", " + packageName + " to " + mode);
+ }
+ final boolean shouldTrack = mForceEnableSwitch
+ || (mode != AppOpsManager.MODE_ALLOWED);
+ UpdateTrackedJobsFunc updateTrackedJobs = new UpdateTrackedJobsFunc(uid,
+ packageName, shouldTrack);
+ mJobSchedulerService.getJobStore().forEachJob(updateTrackedJobs);
+ if (updateTrackedJobs.mChanged) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ }
+ }
+
+ private final class UpdateTrackedJobsFunc implements JobStore.JobStatusFunctor {
+ private final String mPackageName;
+ private final int mUid;
+ private final boolean mShouldTrack;
+ private boolean mChanged = false;
+
+ UpdateTrackedJobsFunc(int uid, String packageName, boolean shouldTrack) {
+ mUid = uid;
+ mPackageName = packageName;
+ mShouldTrack = shouldTrack;
+ }
+
+ @Override
+ public void process(JobStatus jobStatus) {
+ final String packageName = jobStatus.getSourcePackageName();
+ final int uid = jobStatus.getSourceUid();
+ if (mUid != uid || !mPackageName.equals(packageName)) {
+ return;
+ }
+ if (mShouldTrack) {
+ mChanged |= jobStatus.setBackgroundNotRestrictedConstraintSatisfied(
+ canRunJobLocked(uid));
+ startTrackingJobLocked(jobStatus);
+ } else {
+ mChanged |= jobStatus.setBackgroundNotRestrictedConstraintSatisfied(true);
+ stopTrackingJobLocked(jobStatus);
+ }
+ }
+ }
+}
static final int CONSTRAINT_DEVICE_NOT_DOZING = 1<<25;
static final int CONSTRAINT_NOT_ROAMING = 1<<24;
static final int CONSTRAINT_METERED = 1<<23;
+ static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1<<22;
static final int CONNECTIVITY_MASK =
CONSTRAINT_UNMETERED | CONSTRAINT_CONNECTIVITY |
return setConstraintSatisfied(CONSTRAINT_DEVICE_NOT_DOZING, state);
}
+ boolean setBackgroundNotRestrictedConstraintSatisfied(boolean state) {
+ return setConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED, state);
+ }
+
boolean setConstraintSatisfied(int constraint, boolean state) {
boolean old = (satisfiedConstraints&constraint) != 0;
if (old == state) {
// satisfied).
// AppNotIdle implicit constraint must be satisfied
// DeviceNotDozing implicit constraint must be satisfied
+ // NotRestrictedInBackground implicit constraint must be satisfied
final boolean deadlineSatisfied = (!job.isPeriodic() && hasDeadlineConstraint()
&& (satisfiedConstraints & CONSTRAINT_DEADLINE) != 0);
final boolean notIdle = (satisfiedConstraints & CONSTRAINT_APP_NOT_IDLE) != 0;
final boolean notDozing = (satisfiedConstraints & CONSTRAINT_DEVICE_NOT_DOZING) != 0
|| (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
- return (isConstraintsSatisfied() || deadlineSatisfied) && notIdle && notDozing;
+ final boolean notRestrictedInBg =
+ (satisfiedConstraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0;
+ return (isConstraintsSatisfied() || deadlineSatisfied) && notIdle && notDozing
+ && notRestrictedInBg;
}
static final int CONSTRAINTS_OF_INTEREST =
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.UserManager;
import android.os.WorkSource;
import android.provider.Settings;
import android.provider.Telephony.Carriers;
final PowerSaveState result =
mPowerManager.getPowerSaveState(ServiceType.GPS);
switch (result.gpsMode) {
+ case BatterySaverPolicy.GPS_MODE_REALLY_DISABLED_WHEN_SCREEN_OFF:
case BatterySaverPolicy.GPS_MODE_DISABLED_WHEN_SCREEN_OFF:
// If we are in battery saver mode and the screen is off, disable GPS.
disableGps |= result.batterySaverEnabled && !mPowerManager.isInteractive();
if (disableGps != mDisableGps) {
mDisableGps = disableGps;
updateRequirements();
+
+ updateLocationModeForReallyDisabledWhenScreenOff(result.gpsMode);
+ }
+ }
+
+ boolean mLocationDisabledForPowerSaving;
+
+ private void updateLocationModeForReallyDisabledWhenScreenOff(int gpsMode) {
+ final boolean disableLocation = mDisableGps
+ && (gpsMode == BatterySaverPolicy.GPS_MODE_REALLY_DISABLED_WHEN_SCREEN_OFF);
+
+// TODO Secondary user, intent sent in LocationSettingsBase
+ if (disableLocation != mLocationDisabledForPowerSaving) {
+Log.w("XXX:GnssLP", "mLocationDisabledForPowerSaving=" + mLocationDisabledForPowerSaving + " on u" + UserHandle.myUserId());
+ mLocationDisabledForPowerSaving = disableLocation;
+ if (disableLocation) {
+ Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.LOCATION_MODE,
+ android.provider.Settings.Secure.LOCATION_MODE_OFF);
+ } else {
+ Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.LOCATION_MODE,
+ android.provider.Settings.Secure.LOCATION_MODE_PREVIOUS);
+ }
}
}
s.append(" mStarted=").append(mStarted).append('\n');
s.append(" mFixInterval=").append(mFixInterval).append('\n');
s.append(" mDisableGps (battery saver mode)=").append(mDisableGps).append('\n');
+ s.append(" mDisableLocation (battery saver mode)=").append(mLocationDisabledForPowerSaving)
+ .append('\n');
s.append(" mEngineCapabilities=0x").append(Integer.toHexString(mEngineCapabilities));
s.append(" ( ");
if (hasCapability(GPS_CAPABILITY_SCHEDULING)) s.append("SCHEDULING ");
package com.android.server.power;
import android.annotation.IntDef;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.content.BroadcastReceiver;
import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
+import android.os.PowerManager;
+import android.os.UserHandle;
import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.text.TextUtils;
import android.util.KeyValueListParser;
+import android.util.Pair;
import android.util.Slog;
import android.os.PowerSaveState;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.IoThread;
+import libcore.io.IoUtils;
+
+import java.io.FileWriter;
+import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Class to decide whether to turn on battery saver mode for specific service
ServiceType.SCREEN_BRIGHTNESS,
ServiceType.SOUND,
ServiceType.BATTERY_STATS,
- ServiceType.DATA_SAVER})
+ ServiceType.DATA_SAVER,
+ ServiceType.FORCE_APPS_STANDBY,
+ ServiceType.LOWER_MAX_FREQUENCY,
+ ServiceType.AOD,
+ })
public @interface ServiceType {
int NULL = 0;
int GPS = 1;
int SOUND = 8;
int BATTERY_STATS = 9;
int DATA_SAVER = 10;
+
+ int FORCE_APPS_STANDBY = 11;
+ int LOWER_MAX_FREQUENCY = 13;
+ int AOD = 15;
}
private static final String TAG = "BatterySaverPolicy";
// Value of batterySaverGpsMode such that GPS isn't affected by battery saver mode.
public static final int GPS_MODE_NO_CHANGE = 0;
+
// Value of batterySaverGpsMode such that GPS is disabled when battery saver mode
// is enabled and the screen is off.
public static final int GPS_MODE_DISABLED_WHEN_SCREEN_OFF = 1;
+
+ public static final int GPS_MODE_REALLY_DISABLED_WHEN_SCREEN_OFF = 2;
+
// Secure setting for GPS behavior when battery saver mode is on.
public static final String SECURE_KEY_GPS_MODE = "batterySaverGpsMode";
private static final String KEY_ADJUST_BRIGHTNESS_FACTOR = "adjust_brightness_factor";
private static final String KEY_FULLBACKUP_DEFERRED = "fullbackup_deferred";
private static final String KEY_KEYVALUE_DEFERRED = "keyvaluebackup_deferred";
+ private static final String KEY_FORCE_APPS_STANDBY_ENABLED = "force_apps_standby_enabled";
+ private static final String KEY_POWER_HINT_BLOCK = "power_hint_block";
+ private static final String KEY_RED_BAR_ENABLED = "red_bar_enabled";
+ private static final String KEY_MAX_FILE_WRITE_RETRIES = "max_file_write_retries";
+
+ private static final String KEY_FILE_OVERRIDE_PREFIX = "file:";
+ private static final String KEY_SECURE_SETTINGS_OVERRIDE_PREFIX = "secure:";
+ private static final String KEY_GLOBAL_SETTINGS_OVERRIDE_PREFIX = "global:";
+ private static final String KEY_SYSTEM_SETTINGS_OVERRIDE_PREFIX = "system:";
+
+ private static final String KEY_FILE_OVERRIDE_PREFIX_NS = "file-ns:";
+ private static final String KEY_SECURE_SETTINGS_OVERRIDE_PREFIX_NS = "secure-ns:";
+ private static final String KEY_GLOBAL_SETTINGS_OVERRIDE_PREFIX_NS = "global-ns:";
+ private static final String KEY_SYSTEM_SETTINGS_OVERRIDE_PREFIX_NS = "system-ns:";
+
+ private static final String KEY_ACTIVATE_BROADCASTS = "bc-act:";
+ private static final String KEY_DEACTIVATE_BROADCASTS = "bc-deact:";
private final KeyValueListParser mParser = new KeyValueListParser(',');
*/
private float mAdjustBrightnessFactor;
+ private boolean mForceAppsStandbyEnabled;
+ private int mPowerHintMask;
+ private boolean mRedBarEnabled;
+
+ /**
+ * scaling_max_freq wouldn't accept a value lower than the current frequency, so we need to
+ * retry, and this limits the max number of retries.
+ */
+ private int mMaxFileWriteRetries;
+
+ private final ArrayList<Pair<String, String>> mFileOverrides = new ArrayList<>();
+ private final ArrayList<Pair<String, String>> mFileOverridesNS = new ArrayList<>();
+ private final ArrayList<Pair<String, String>> mFileRestores = new ArrayList<>();
+
+ private final ArrayList<Pair<String, String>> mSecureOverrides = new ArrayList<>();
+ private final ArrayList<Pair<String, String>> mGlobalOverrides = new ArrayList<>();
+ private final ArrayList<Pair<String, String>> mSystemOverrides = new ArrayList<>();
+ private final ArrayList<Pair<String, String>> mSecureOverridesNS = new ArrayList<>();
+ private final ArrayList<Pair<String, String>> mGlobalOverridesNS = new ArrayList<>();
+ private final ArrayList<Pair<String, String>> mSystemOverridesNS = new ArrayList<>();
+
+ private final ArrayList<Intent> mActivateBroadcasts = new ArrayList<>();
+ private final ArrayList<Intent> mDeactivateBroadcasts = new ArrayList<>();
+
+ private static final String SETTING_ORIGINAL_SUFFIX = "_bs_orig";
+
private ContentResolver mContentResolver;
- public BatterySaverPolicy(Handler handler) {
+ private static final String BATTERY_SAVER_CONSTANTS_DEFAULT_KEY = Settings.Global.BATTERY_SAVER_CONSTANTS + "_om";
+ private static String BATTERY_SAVER_CONSTANTS_KEY = BATTERY_SAVER_CONSTANTS_DEFAULT_KEY;
+
+ private boolean mActivated = false;
+ private boolean mScreenOn = true;
+
+ private final Context mContext;
+ private final Handler mHandler;
+
+ public BatterySaverPolicy(Context context, Handler handler) {
super(handler);
+ mContext = context;
+ mHandler = handler;
}
public void start(ContentResolver contentResolver) {
mContentResolver = contentResolver;
+ final String key = Settings.Global.getString(mContentResolver, Settings.Global.BATTERY_SAVER_CONSTANTS + "_key");
+ if (!TextUtils.isEmpty(key)) {
+ BATTERY_SAVER_CONSTANTS_KEY = key;
+ }
+
+
mContentResolver.registerContentObserver(Settings.Global.getUriFor(
- Settings.Global.BATTERY_SAVER_CONSTANTS), false, this);
+ BATTERY_SAVER_CONSTANTS_KEY), false, this);
onChange(true, null);
+
+ // In case the device rebooted while battery saver was enabled, restore all settings.
+ // restoreAllSettings relies on configuration read by onChange(), so it needs to follow it.
+ restoreAllSettings();
}
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case Intent.ACTION_SCREEN_ON:
+ case Intent.ACTION_SCREEN_OFF:
+ updateScreenState();
+ break;
+ }
+ }
+ };
+
+
@Override
public void onChange(boolean selfChange, Uri uri) {
final String value = Settings.Global.getString(mContentResolver,
- Settings.Global.BATTERY_SAVER_CONSTANTS);
+ BATTERY_SAVER_CONSTANTS_KEY);
updateConstants(value);
+
+ // Also propagate to BATTERY_SAVER_USE_RED_BAR
+ Settings.Global.putInt(mContentResolver, Global.BATTERY_SAVER_USE_RED_BAR,
+ mRedBarEnabled ? 1 : 0);
+
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ intentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mContext.registerReceiver(mBroadcastReceiver, intentFilter, null, mHandler);
+
}
@VisibleForTesting
void updateConstants(final String value) {
synchronized (BatterySaverPolicy.this) {
+ Slog.d(TAG, "Updating battery saver settings: " + value);
+
try {
mParser.setString(value);
} catch (IllegalArgumentException e) {
- Slog.e(TAG, "Bad battery saver constants");
+ Slog.e(TAG, "Bad battery saver constants: " + value);
}
mVibrationDisabled = mParser.getBoolean(KEY_VIBRATION_DISABLED, true);
mAdjustBrightnessFactor = mParser.getFloat(KEY_ADJUST_BRIGHTNESS_FACTOR, 0.5f);
mDataSaverDisabled = mParser.getBoolean(KEY_DATASAVER_DISABLED, true);
+ mForceAppsStandbyEnabled = mParser.getBoolean(KEY_FORCE_APPS_STANDBY_ENABLED, false);
+ mPowerHintMask = ~mParser.getInt(KEY_POWER_HINT_BLOCK, 0);
+
+ mRedBarEnabled = mParser.getBoolean(KEY_RED_BAR_ENABLED, true);
+
+ mMaxFileWriteRetries = mParser.getInt(KEY_MAX_FILE_WRITE_RETRIES, 20);
+
// Get default value from Settings.Secure
final int defaultGpsMode = Settings.Secure.getInt(mContentResolver, SECURE_KEY_GPS_MODE,
GPS_MODE_DISABLED_WHEN_SCREEN_OFF);
mGpsMode = mParser.getInt(KEY_GPS_MODE, defaultGpsMode);
+
+ parseOverrides(mFileOverrides, mParser, KEY_FILE_OVERRIDE_PREFIX);
+ parseOverrides(mSecureOverrides, mParser, KEY_SECURE_SETTINGS_OVERRIDE_PREFIX);
+ parseOverrides(mGlobalOverrides, mParser, KEY_GLOBAL_SETTINGS_OVERRIDE_PREFIX);
+ parseOverrides(mSystemOverrides, mParser, KEY_SYSTEM_SETTINGS_OVERRIDE_PREFIX);
+
+ parseOverrides(mFileOverridesNS, mParser, KEY_FILE_OVERRIDE_PREFIX_NS);
+ parseOverrides(mSecureOverridesNS, mParser, KEY_SECURE_SETTINGS_OVERRIDE_PREFIX_NS);
+ parseOverrides(mGlobalOverridesNS, mParser, KEY_GLOBAL_SETTINGS_OVERRIDE_PREFIX_NS);
+ parseOverrides(mSystemOverridesNS, mParser, KEY_SYSTEM_SETTINGS_OVERRIDE_PREFIX_NS);
+
+ parseIntents(mActivateBroadcasts, mParser, KEY_ACTIVATE_BROADCASTS);
+ parseIntents(mDeactivateBroadcasts, mParser, KEY_DEACTIVATE_BROADCASTS);
+
+ copyIfEmpty(mFileOverridesNS, mFileOverrides);
+ copyIfEmpty(mSecureOverridesNS, mSecureOverrides);
+ copyIfEmpty(mGlobalOverridesNS, mGlobalOverrides);
+ copyIfEmpty(mSystemOverridesNS, mSystemOverrides);
+ }
+ }
+
+ private static <T> void copyIfEmpty(ArrayList<T> dest, ArrayList<T> source) {
+ if (dest.isEmpty()) {
+ dest.addAll(source);
+ }
+ }
+
+ private static void parseOverrides(ArrayList<Pair<String, String>> target,
+ KeyValueListParser parser, String prefix) {
+ target.clear();
+
+ for (String origKey : parser.getKeys()) {
+ if (origKey.startsWith(prefix)) {
+ final String key = origKey.substring(prefix.length());
+
+ target.add(Pair.create(key, parser.getString(origKey, "")));
+ }
+ }
+ }
+
+ private void parseIntents(ArrayList<Intent> target, KeyValueListParser parser,
+ String prefix) {
+ target.clear();
+
+ for (String origKey : parser.getKeys()) {
+ if (origKey.startsWith(prefix)) {
+ final String key = origKey.substring(prefix.length());
+ final String value = parser.getString(origKey, "");
+ if (TextUtils.isEmpty(value)) {
+ continue;
+ }
+ try {
+ target.add(Intent.parseUri(value, /* flags =*/ 0));
+ } catch (URISyntaxException e) {
+ Slog.e(TAG, "Error parsing intent: " + value);
+ }
+ }
}
}
case ServiceType.VIBRATION:
return builder.setBatterySaverEnabled(mVibrationDisabled)
.build();
+
+ case ServiceType.FORCE_APPS_STANDBY:
+ return builder.setBatterySaverEnabled(mForceAppsStandbyEnabled)
+ .build();
+
default:
return builder.setBatterySaverEnabled(realMode)
.build();
public void dump(PrintWriter pw) {
pw.println();
pw.println("Battery saver policy");
- pw.println(" Settings " + Settings.Global.BATTERY_SAVER_CONSTANTS);
+ pw.println(" Settings key: " + BATTERY_SAVER_CONSTANTS_KEY);
pw.println(" value: " + Settings.Global.getString(mContentResolver,
- Settings.Global.BATTERY_SAVER_CONSTANTS));
+ BATTERY_SAVER_CONSTANTS_KEY));
pw.println();
pw.println(" " + KEY_VIBRATION_DISABLED + "=" + mVibrationDisabled);
pw.println(" " + KEY_ADJUST_BRIGHTNESS_FACTOR + "=" + mAdjustBrightnessFactor);
pw.println(" " + KEY_GPS_MODE + "=" + mGpsMode);
+ pw.println(" " + KEY_FORCE_APPS_STANDBY_ENABLED + "=" + mForceAppsStandbyEnabled);
+ pw.println(" " + KEY_POWER_HINT_BLOCK + "=" + Integer.toHexString(~mPowerHintMask));
+ pw.println(" " + KEY_RED_BAR_ENABLED + "=" + mRedBarEnabled);
+ pw.println(" " + KEY_MAX_FILE_WRITE_RETRIES + "=" + mMaxFileWriteRetries);
+
+ pw.println(" Files overrides =" + mFileOverrides);
+ pw.println(" Files overrides (NS)=" + mFileOverridesNS);
+ pw.println(" Files restores =" + mFileRestores);
+ pw.println(" Secure overrides =" + mSecureOverrides);
+ pw.println(" Secure overrides (NS)=" + mSecureOverridesNS);
+ pw.println(" Global overrides =" + mGlobalOverrides);
+ pw.println(" Global overrides (NS)=" + mGlobalOverridesNS);
+ pw.println(" System overrides =" + mSystemOverrides);
+ pw.println(" System overrides (NS)=" + mSystemOverridesNS);
+
+ pw.println(" Activate broadcasts =" + mActivateBroadcasts);
+ pw.println(" Deactivate broadcasts=" + mDeactivateBroadcasts);
+ }
+
+ // TODO Move it somewhere else.
+ public void startSaver() {
+ updateActivateState(true);
+ }
+
+ private static void applySettings(ContentResolver cr, int userId,
+ ArrayList<Pair<String, String>> overrides,
+ SettingsIf settings) {
+ for (Pair<String, String> setting : overrides) {
+ final String name = setting.first;
+ final String value = setting.second;
+ final String name_orig = name + SETTING_ORIGINAL_SUFFIX;
+
+ // Keep the original unless we've already done so.
+ final String orig = settings.read(cr, name, userId);
+ if (settings.read(cr, name_orig, userId) == null) {
+ settings.write(cr, name_orig, orig, userId);
+ }
+ settings.write(cr, name, value, userId);
+ }
+ }
+
+ public void stopSaver() {
+ updateActivateState(false);
}
-}
+
+ private void updateScreenState() {
+ updateActivateState(mActivated);
+ }
+
+ private void updateActivateState(boolean activate) {
+ final PowerManager pm = mContext.getSystemService(PowerManager.class);
+ final boolean newScreenOn = pm.isInteractive();
+
+ if ((activate == mActivated) && (newScreenOn == mScreenOn)) {
+ return; // no changes.
+ }
+
+ // When we're going to activate, save the original file values.
+ // Note we keep the original *settings* in a different way, so no need to save the original settings here.
+ if (!mActivated) {
+ mFileRestores.clear();
+
+ for (Pair<String, String> files : mFileOverrides) {
+ final String name = files.first;
+ final String value = files.second;
+ try {
+ final String org = IoUtils.readFileAsString(name).trim();
+ mFileRestores.add(Pair.create(name, org));
+ } catch (IOException e) {
+ Slog.wtf(TAG, "Can't read from" + name, e);
+ }
+ }
+ }
+ boolean wasActivated = mActivated;
+ mActivated = activate;
+ mScreenOn = newScreenOn;
+
+ if (mActivated) {
+ Slog.d(TAG, "Starting battery saver...");
+
+ PowerManagerService.setPowerHintMask(mPowerHintMask);
+
+ writeToFiles(mScreenOn ? mFileOverrides : mFileOverridesNS);
+
+ // Update settings.
+ final ContentResolver cr = mContentResolver;
+ applySettings(cr, UserHandle.USER_SYSTEM, mScreenOn ? mSecureOverrides : mSecureOverridesNS, SecureSettingsIf.INSTANCE);
+ applySettings(cr, UserHandle.USER_SYSTEM, mScreenOn ? mGlobalOverrides : mGlobalOverridesNS, GlobalSettingsIf.INSTANCE);
+ applySettings(cr, UserHandle.USER_SYSTEM, mScreenOn ? mSystemOverrides : mSystemOverridesNS, SystemSettingsIf.INSTANCE);
+ } else {
+ Slog.d(TAG, "Stopping battery saver...");
+
+ PowerManagerService.setPowerHintMask(0xffffffff);
+
+ writeToFiles(mFileRestores);
+
+ restoreAllSettings();
+ }
+ if (wasActivated != mActivated) {
+ final ArrayList<Intent> intents = mActivated ? mActivateBroadcasts : mDeactivateBroadcasts;
+
+ mHandler.post(() -> {
+ for (Intent intent : intents) {
+ Slog.i(TAG, "Broadcasting: " + intent);
+ mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+ }
+ });
+ }
+ }
+
+ private void restoreAllSettings() {
+ Slog.d(TAG, "Restoring settings...");
+
+ // TODO Other users.
+ final ContentResolver cr = mContentResolver;
+ restoreSettings(cr, UserHandle.USER_SYSTEM, mScreenOn ? mSecureOverrides : mSecureOverridesNS, SecureSettingsIf.INSTANCE);
+ restoreSettings(cr, UserHandle.USER_SYSTEM, mScreenOn ? mGlobalOverrides : mGlobalOverridesNS, GlobalSettingsIf.INSTANCE);
+ restoreSettings(cr, UserHandle.USER_SYSTEM, mScreenOn ? mSystemOverrides : mSystemOverridesNS, SystemSettingsIf.INSTANCE);
+ }
+
+ private static void restoreSettings(ContentResolver cr, int userId,
+ ArrayList<Pair<String, String>> overrides,
+ SettingsIf settings) {
+ for (Pair<String, String> setting : overrides) {
+ final String name = setting.first;
+ final String name_orig = name + SETTING_ORIGINAL_SUFFIX;
+
+ final String current = settings.read(cr, name, userId);
+ final String value = setting.second;
+
+ if (TextUtils.equals(current, value)) {
+ // If the user hans't changed it, restore the original value.
+
+ final String orig = settings.read(cr, name_orig, userId);
+ if (orig != null) {
+ settings.write(cr, name, orig, userId);
+ }
+ }
+
+ // Erase the original value.
+ settings.write(cr, name_orig, null, userId);
+ }
+ }
+
+ private static final int SAVE_RETRY_DELAY = 3000;
+
+ private final AtomicInteger mNumRetries = new AtomicInteger();
+ private volatile ArrayList<Pair<String, String>> pendingFilesAndValues;
+
+ private final Runnable mWritePendingFiles = () -> {
+ writePendingFiles(); // We need to extraact it to a separate method to avoid self-refing.
+ };
+
+ private void writeToFiles(ArrayList<Pair<String, String>> filesAndValues) {
+ pendingFilesAndValues = filesAndValues;
+
+ mNumRetries.set(0);
+
+ // Grr, we can't write a lower frequency than the current frequency to the max freq.
+ // We need a retry logic...
+ IoThread.getHandler().post(mWritePendingFiles);
+ }
+
+ private void writePendingFiles() {
+ final ArrayList<Pair<String, String>> files = pendingFilesAndValues;
+ if (files != null) {
+ for (Pair<String, String> pair : files) {
+ final String name = pair.first;
+ final String value = pair.second;
+ try {
+ writeToFile(name, value);
+ } catch (IOException e) {
+ if (mNumRetries.incrementAndGet() < mMaxFileWriteRetries) {
+ Slog.w(TAG, "Failed to write " + value + " to " + name + ", retrying.");
+ // Retry.
+ IoThread.getHandler().postDelayed(mWritePendingFiles, SAVE_RETRY_DELAY);
+ } else {
+ Slog.wtf(TAG, "Failed to write " + value + " to " + name, e);
+ }
+ return;
+ }
+ }
+ pendingFilesAndValues = null;
+ }
+ }
+
+ private void writeToFile(String name, String value) throws IOException {
+ Slog.d(TAG, "Writing " + value + " to " + name);
+ try (FileWriter out = new FileWriter(name)) {
+ out.write(value);
+ }
+ }
+
+ interface SettingsIf {
+ void write(ContentResolver cr, String name, String value, int user);
+ String read(ContentResolver cr, String name, int user);
+ }
+
+ private static class SecureSettingsIf implements SettingsIf {
+ public static final SecureSettingsIf INSTANCE = new SecureSettingsIf();
+
+ @Override
+ public void write(ContentResolver cr, String name, String value, int user) {
+ Slog.d(TAG, "Writing " + value + " to secure." + name);
+ Settings.Secure.putStringForUser(cr, name, value, user);
+ }
+
+ @Override
+ public String read(ContentResolver cr, String name, int user) {
+ return Settings.Secure.getStringForUser(cr, name, user);
+ }
+ }
+
+ private static class GlobalSettingsIf implements SettingsIf {
+ public static final GlobalSettingsIf INSTANCE = new GlobalSettingsIf();
+
+ @Override
+ public void write(ContentResolver cr, String name, String value, int user) {
+ Slog.d(TAG, "Writing " + value + " to global." + name);
+ Settings.Global.putStringForUser(cr, name, value, user);
+ }
+
+ @Override
+ public String read(ContentResolver cr, String name, int user) {
+ return Settings.Global.getStringForUser(cr, name, user);
+ }
+ }
+
+ private static class SystemSettingsIf implements SettingsIf {
+ public static final SystemSettingsIf INSTANCE = new SystemSettingsIf();
+
+ @Override
+ public void write(ContentResolver cr, String name, String value, int user) {
+ Slog.d(TAG, "Writing " + value + " to system." + name);
+ Settings.System.putStringForUser(cr, name, value, user);
+ }
+
+ @Override
+ public String read(ContentResolver cr, String name, int user) {
+ return Settings.System.getStringForUser(cr, name, user);
+ }
+ }
+
+}
\ No newline at end of file
private static native void nativeSendPowerHint(int hintId, int data);
private static native void nativeSetFeature(int featureId, int data);
+ private static native void nativeSetPowerHintMask(int featureIdMask);
+
+ public static void setPowerHintMask(int featureIdMask) {
+ nativeSetPowerHintMask(featureIdMask);
+ }
+
public PowerManagerService(Context context) {
super(context);
mContext = context;
mHandler = new PowerManagerHandler(mHandlerThread.getLooper());
mConstants = new Constants(mHandler);
mAmbientDisplayConfiguration = new AmbientDisplayConfiguration(mContext);
- mBatterySaverPolicy = new BatterySaverPolicy(mHandler);
+ mBatterySaverPolicy = new BatterySaverPolicy(mContext, mHandler);
synchronized (mLock) {
mWakeLockSuspendBlocker = createSuspendBlockerLocked("PowerManagerService.WakeLocks");
final PowerSaveState result =
mBatterySaverPolicy.getBatterySaverPolicy(
listener.getServiceType(), lowPowerModeEnabled);
+// XXX
listener.onLowPowerModeChanged(result);
}
intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
Manifest.permission.DEVICE_POWER);
+
+ if (mLowPowerModeEnabled) {
+ mBatterySaverPolicy.startSaver();
+ } else {
+ mBatterySaverPolicy.stopSaver();
+ }
}
});
}
final boolean mHasPermanentDpad;
final long mDrawLockTimeoutMillis;
- final boolean mAllowAnimationsInLowPowerMode;
+ // final boolean mAllowAnimationsInLowPowerMode;
final boolean mAllowBootMessages;
com.android.internal.R.bool.config_defaultInTouchMode);
mDrawLockTimeoutMillis = context.getResources().getInteger(
com.android.internal.R.integer.config_drawLockTimeoutMillis);
- mAllowAnimationsInLowPowerMode = context.getResources().getBoolean(
- com.android.internal.R.bool.config_allowAnimationsInLowPowerMode);
+// mAllowAnimationsInLowPowerMode = context.getResources().getBoolean(
+// com.android.internal.R.bool.config_allowAnimationsInLowPowerMode);
mMaxUiWidth = context.getResources().getInteger(
com.android.internal.R.integer.config_maxUiWidth);
mInputManager = inputManager; // Must be before createDisplayContentLocked.
public void onLowPowerModeChanged(PowerSaveState result) {
synchronized (mWindowMap) {
final boolean enabled = result.batterySaverEnabled;
- if (mAnimationsDisabled != enabled && !mAllowAnimationsInLowPowerMode) {
+ if (mAnimationsDisabled != enabled) {
mAnimationsDisabled = enabled;
dispatchNewAnimatorScaleLocked(null);
}
// Throttling interval for user activity calls.
static const nsecs_t MIN_TIME_BETWEEN_USERACTIVITIES = 100 * 1000000L; // 100ms
+static int gPowerHintMask = 0xffffffff;
+
// ----------------------------------------------------------------------------
+static bool shouldSendPowerHint(uint featureId, int data) {
+ switch ((PowerHint) featureId) {
+ case PowerHint::VSYNC:
+ case PowerHint::SUSTAINED_PERFORMANCE:
+ case PowerHint::VR_MODE:
+ case PowerHint::LAUNCH:
+ if (data == 0) return true; // Always pass 0
+ break;
+ case PowerHint::LOW_POWER: // TODO Use it?
+ if (data == 0) {
+ featureId = 16;
+ }
+ default:
+ break; // Just pass the other ones.
+ }
+ return (gPowerHintMask & (1 << (featureId - 1))) != 0;
+}
+
+static bool shouldSendPowerHint(PowerHint feature, int data) {
+ return shouldSendPowerHint((uint) feature, data);
+}
+
static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName) {
if (env->ExceptionCheck()) {
ALOGE("An exception was thrown by callback '%s'.", methodName);
// Tell the power HAL when user activity occurs.
gPowerHalMutex.lock();
- if (getPowerHal()) {
+ if (shouldSendPowerHint(PowerHint::INTERACTION, 0) && getPowerHal()) {
Return<void> ret;
if (gPowerHalV1_1 != nullptr) {
ret = gPowerHalV1_1->powerHintAsync(PowerHint::INTERACTION, 0);
}
}
+static void nativeSetPowerHintMask(JNIEnv* /* env */, jclass /* clazz */, jint mask) {
+ std::lock_guard<std::mutex> lock(gPowerHalMutex);
+ gPowerHintMask = mask;
+
+ ALOGD("nativeSetPowerHintMask: mask=%x", gPowerHintMask);
+}
+
static void nativeSetAutoSuspend(JNIEnv* /* env */, jclass /* clazz */, jboolean enable) {
if (enable) {
android::base::Timer t;
static void nativeSendPowerHint(JNIEnv *env, jclass clazz, jint hintId, jint data) {
std::lock_guard<std::mutex> lock(gPowerHalMutex);
- if (getPowerHal()) {
+
+ if (shouldSendPowerHint(hintId, data) && getPowerHal()) {
Return<void> ret;
if (gPowerHalV1_1 != nullptr) {
ret = gPowerHalV1_1->powerHintAsync((PowerHint)hintId, data);
(void*) nativeSendPowerHint },
{ "nativeSetFeature", "(II)V",
(void*) nativeSetFeature },
+
+ { "nativeSetPowerHintMask", "(I)V",
+ (void*) nativeSetPowerHintMask },
};
#define FIND_CLASS(var, className) \
LOCAL_SRC_FILES += aidl/com/android/servicestests/aidl/INetworkStateObserver.aidl \
aidl/com/android/servicestests/aidl/ICmdReceiverService.aidl
+LOCAL_SRC_FILES += $(call all-java-files-under, test-apps/JobTestApp/src)
LOCAL_JAVA_LIBRARIES := android.test.mock legacy-android-test
include $(BUILD_PACKAGE)
-include $(call all-makefiles-under, $(LOCAL_PATH))
\ No newline at end of file
+
+include $(call all-makefiles-under, $(LOCAL_PATH))
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
<uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
<uses-permission android:name="android.permission.DELETE_PACKAGES" />
+ <uses-permission android:name="android.permission.GET_APP_OPS_STATS" />
+ <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
+ <uses-permission android:name="android.permission.DEVICE_POWER" />
+ <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES" />
+ <uses-permission android:name="android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST" />
<!-- Uses API introduced in O (26) -->
<uses-sdk android:minSdkVersion="1"
<target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
<option name="test-file-name" value="FrameworksServicesTests.apk" />
<option name="test-file-name" value="ConnTestApp.apk" />
+ <option name="test-file-name" value="JobTestApp.apk" />
</target_preparer>
<option name="test-suite-tag" value="apct" />
--- /dev/null
+<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
+<app-ops>
+<uid n="1001">
+<op n="15" m="0" />
+</uid>
+<uid n="10052">
+<op n="63" m="1" />
+</uid>
+<pkg n="com.quicinc.cne.CNEService">
+<uid n="1000" p="true">
+<op n="59" t="1501535978641" pu="0" />
+<op n="60" t="1501535978641" pu="0" />
+</uid>
+</pkg>
+<pkg n="android">
+<uid n="1000" p="true">
+<op n="0" />
+<op n="3" t="1501537828283" d="22" />
+<op n="8" t="1501535987988" pu="0" pp="com.android.providers.calendar" />
+<op n="23" r="1501535979451" />
+<op n="40" t="1501621469584" d="1" />
+<op n="41" t="1501535980608" d="85615033" />
+<op n="61" t="1501557904487" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.server.telecom">
+<uid n="1000" p="true">
+<op n="6" t="1501535984350" pu="0" pp="com.android.providers.contacts" />
+</uid>
+</pkg>
+<pkg n="com.android.settings">
+<uid n="1000" p="true">
+<op n="59" t="1501536001265" pu="0" />
+<op n="60" t="1501536001265" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.providers.telephony">
+<uid n="1001" p="true">
+<op n="15" m="0" />
+</uid>
+</pkg>
+<pkg n="com.qualcomm.qti.rcsbootstraputil">
+<uid n="1001" p="false">
+<op n="59" t="1501535981233" pu="0" />
+<op n="60" t="1501535981233" pu="0" />
+<op n="63" t="1501536015379" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.phone">
+<uid n="1001" p="true">
+<op n="14" t="1501547602479" />
+<op n="15" m="0" t="1501535981903" pu="0" />
+<op n="40" t="1501621220685" d="4" />
+<op n="59" t="1501535978675" pu="0" />
+<op n="60" t="1501535978675" pu="0" />
+<op n="63" m="1" t="1501277487395" pu="0" />
+</uid>
+</pkg>
+<pkg n="audioserver">
+<uid n="1041" p="false">
+<op n="40" t="1501542152888" d="4" />
+</uid>
+</pkg>
+<pkg n="com.android.shell">
+<uid n="2000" p="true">
+<op n="59" t="1501535997600" pu="0" />
+<op n="60" t="1501535997600" pu="0" />
+<op n="63" t="1501535997600" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.apps.turbo">
+<uid n="10024" p="true">
+<op n="59" t="1501621079685" pu="0" />
+<op n="60" t="1501621079685" pu="0" />
+<op n="63" t="1501621079682" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.providers.downloads">
+<uid n="10027" p="true">
+<op n="59" t="1501601386341" />
+<op n="60" t="1501601375992" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.carriersetup">
+<uid n="10029" p="true">
+<op n="59" t="1501536001405" pu="0" />
+<op n="60" t="1501536001405" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.systemui">
+<uid n="10031" p="true">
+<op n="3" t="1501537825972" d="21" />
+<op n="40" t="1501619729317" d="7297" />
+<op n="59" t="1501535979651" pu="0" />
+<op n="60" t="1501535978058" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.chrome">
+<uid n="10096" p="false">
+<op n="23" r="1501537723291" />
+<op n="59" t="1501537615416" pu="0" />
+<op n="60" t="1501537615416" pu="0" />
+<op n="63" m="1" />
+</uid>
+</pkg>
+<pkg n="com.google.android.apps.maps">
+<uid n="10102" p="false">
+<op n="0" />
+<op n="1" t="1501620392477" pu="0" />
+<op n="59" t="1501620392609" pu="0" />
+<op n="60" t="1501620392609" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.syncadapters.contacts">
+<uid n="10109" p="false">
+<op n="4" t="1501535997715" pu="0" pp="com.android.providers.contacts" />
+<op n="59" t="1501535997265" pu="0" />
+<op n="60" t="1501535997265" pu="0" />
+<op n="63" t="1501535997589" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.youtube">
+<uid n="10111" p="false">
+<op n="59" t="1501620380957" pu="0" />
+<op n="60" t="1501620380957" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.deskclock">
+<uid n="10114" p="false">
+<op n="40" t="1501537682746" d="379" />
+<op n="59" t="1501537682098" pu="0" />
+<op n="60" t="1501537682098" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.apps.internal.betterbug">
+<uid n="10117" p="false">
+<op n="59" t="1501535989133" pu="0" />
+<op n="60" t="1501535989133" pu="0" />
+<op n="63" t="1501535989132" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.tts">
+<uid n="10118" p="false">
+<op n="59" t="1501193966186" pu="0" />
+<op n="60" t="1501193966186" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.apps.enterprise.dmagent">
+<uid n="10119" p="false">
+<op n="59" t="1501193986104" pu="0" />
+<op n="60" t="1501193986104" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.qualcomm.embms">
+<uid n="10122" p="false">
+<op n="59" t="1501535999723" pu="0" />
+<op n="60" t="1501535999723" pu="0" />
+<op n="63" t="1501535999550" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.qualcomm.qti.telephonyservice">
+<uid n="10123" p="false">
+<op n="59" t="1501535978649" pu="0" />
+<op n="60" t="1501535978649" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.qualcomm.ltebc_vzw">
+<uid n="10124" p="false">
+<op n="59" t="1501536001390" pu="0" />
+<op n="60" t="1501536001390" pu="0" />
+<op n="63" t="1501536000356" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.ramdump">
+<uid n="10125" p="false">
+<op n="59" t="1501536047490" pu="0" />
+<op n="60" t="1501536047490" pu="0" />
+<op n="63" m="1" />
+</uid>
+</pkg>
+<pkg n="com.android.nexuslogger">
+<uid n="10127" p="false">
+<op n="59" t="1501535985248" pu="0" />
+<op n="60" t="1501535985248" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.google.android.apps.multidevice.client">
+<uid n="10131" p="false">
+<op n="59" t="1501535991782" pu="0" />
+<op n="60" t="1501535991782" pu="0" />
+<op n="63" t="1501535991781" pu="0" />
+</uid>
+</pkg>
+<pkg n="com.android.frameworks.servicestests">
+<uid n="10132" p="false">
+<op n="59" t="1501551739953" pu="0" />
+<op n="60" t="1501551739953" pu="0" />
+</uid>
+</pkg>
+</app-ops>
--- /dev/null
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Tests app ops version upgrades
+ */
+@RunWith(AndroidJUnit4.class)
+public class AppOpsUpgradeTest {
+ private static final String TAG = AppOpsUpgradeTest.class.getSimpleName();
+ private static final String APP_OPS_UNVERSIONED_ASSET_PATH =
+ "AppOpsUpgradeTest/appops-unversioned.xml";
+ private static final String APP_OPS_FILENAME = "appops-test.xml";
+ private static final int NON_DEFAULT_OPS_IN_FILE = 4;
+ private static final int CURRENT_VERSION = 1;
+
+ private File mAppOpsFile;
+ private Context mContext;
+ private Handler mHandler;
+
+ private void extractAppOpsFile() {
+ mAppOpsFile.getParentFile().mkdirs();
+ if (mAppOpsFile.exists()) {
+ mAppOpsFile.delete();
+ }
+ try (FileOutputStream out = new FileOutputStream(mAppOpsFile);
+ InputStream in = mContext.getAssets().open(APP_OPS_UNVERSIONED_ASSET_PATH,
+ AssetManager.ACCESS_BUFFER)) {
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = in.read(buffer)) >= 0) {
+ out.write(buffer, 0, bytesRead);
+ }
+ out.flush();
+ Log.d(TAG, "Successfully copied xml to " + mAppOpsFile.getAbsolutePath());
+ } catch (IOException exc) {
+ Log.e(TAG, "Exception while copying appops xml", exc);
+ fail();
+ }
+ }
+
+ private void assertSameModes(SparseArray<AppOpsService.UidState> uidStates, int op1, int op2) {
+ int numberOfNonDefaultOps = 0;
+ final int defaultModeOp1 = AppOpsManager.opToDefaultMode(op1);
+ final int defaultModeOp2 = AppOpsManager.opToDefaultMode(op2);
+ for(int i = 0; i < uidStates.size(); i++) {
+ final AppOpsService.UidState uidState = uidStates.valueAt(i);
+ if (uidState.opModes != null) {
+ final int uidMode1 = uidState.opModes.get(op1, defaultModeOp1);
+ final int uidMode2 = uidState.opModes.get(op2, defaultModeOp2);
+ assertEquals(uidMode1, uidMode2);
+ if (uidMode1 != defaultModeOp1) {
+ numberOfNonDefaultOps++;
+ }
+ }
+ if (uidState.pkgOps == null) {
+ continue;
+ }
+ for (int j = 0; j < uidState.pkgOps.size(); j++) {
+ final AppOpsService.Ops ops = uidState.pkgOps.valueAt(j);
+ if (ops == null) {
+ continue;
+ }
+ final AppOpsService.Op _op1 = ops.get(op1);
+ final AppOpsService.Op _op2 = ops.get(op2);
+ final int mode1 = (_op1 == null) ? defaultModeOp1 : _op1.mode;
+ final int mode2 = (_op2 == null) ? defaultModeOp2 : _op2.mode;
+ assertEquals(mode1, mode2);
+ if (mode1 != defaultModeOp1) {
+ numberOfNonDefaultOps++;
+ }
+ }
+ }
+ assertEquals(numberOfNonDefaultOps, NON_DEFAULT_OPS_IN_FILE);
+ }
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getTargetContext();
+ mAppOpsFile = new File(mContext.getFilesDir(), APP_OPS_FILENAME);
+ extractAppOpsFile();
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper());
+ }
+
+ @Test
+ public void testUpgradeFromNoVersion() throws Exception {
+ AppOpsDataParser parser = new AppOpsDataParser(mAppOpsFile);
+ assertTrue(parser.parse());
+ assertEquals(AppOpsDataParser.NO_VERSION, parser.mVersion);
+ AppOpsService testService = new AppOpsService(mAppOpsFile, mHandler); // trigger upgrade
+ assertSameModes(testService.mUidStates, AppOpsManager.OP_RUN_IN_BACKGROUND,
+ AppOpsManager.OP_RUN_ANY_IN_BACKGROUND);
+ testService.mContext = mContext;
+ mHandler.removeCallbacks(testService.mWriteRunner);
+ testService.writeState();
+ assertTrue(parser.parse());
+ assertEquals(CURRENT_VERSION, parser.mVersion);
+ }
+
+ /**
+ * Class to parse data from the appops xml. Currently only parses and holds the version number.
+ * Other fields may be added as and when required for testing.
+ */
+ private static final class AppOpsDataParser {
+ static final int NO_VERSION = -1;
+ int mVersion;
+ private File mFile;
+
+ AppOpsDataParser(File file) {
+ mFile = file;
+ mVersion = NO_VERSION;
+ }
+
+ boolean parse() {
+ try (FileInputStream stream = new FileInputStream(mFile)) {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(stream, StandardCharsets.UTF_8.name());
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ ;
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("no start tag found");
+ }
+ final String versionString = parser.getAttributeValue(null, "v");
+ if (versionString != null) {
+ mVersion = Integer.parseInt(versionString);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed while parsing test appops xml", e);
+ return false;
+ }
+ return true;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.job;
+
+import static com.android.servicestests.apps.jobtestapp.TestJobService.ACTION_JOB_STARTED;
+import static com.android.servicestests.apps.jobtestapp.TestJobService.ACTION_JOB_STOPPED;
+import static com.android.servicestests.apps.jobtestapp.TestJobService.JOB_PARAMS_EXTRA_KEY;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.IActivityManager;
+import android.app.job.JobParameters;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.IDeviceIdleController;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+
+import com.android.servicestests.apps.jobtestapp.TestJobActivity;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * TODO: Also add a test for temp power whitelist
+ * Tests that background restrictions on jobs work as expected.
+ * This test requires test-apps/JobTestApp to be installed on the device.
+ * To run this test from root of checkout:
+ * <pre>
+ * mmm -j32 frameworks/base/services/tests/servicestests/
+ * adb install out/target/product/marlin/data/app/JobTestApp/JobTestApp.apk
+ * adb install out/target/product/marlin/data/app/FrameworksServicesTests/FrameworksServicesTests.apk
+ * adb shell am instrument -e class 'com.android.server.job.BackgroundRestrictionsTest' -w \
+ * 'com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner'
+ * </pre>
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class BackgroundRestrictionsTest {
+ private static final String TAG = BackgroundRestrictionsTest.class.getSimpleName();
+ private static final String TEST_APP_PACKAGE = "com.android.servicestests.apps.jobtestapp";
+ private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestJobActivity";
+ private static final long POLL_INTERVAL = 2000;
+ private static final long DEFAULT_WAIT_TIMEOUT = 5000;
+
+ private Context mContext;
+ private AppOpsManager mAppOpsManager;
+ private IDeviceIdleController mDeviceIdleController;
+ private IActivityManager mIActivityManager;
+ private int mTestJobId;
+ private int mTestPackageUid;
+ /* accesses must be synchronized on itself */
+ private final TestJobStatus mTestJobStatus = new TestJobStatus();
+ private final BroadcastReceiver mJobStateChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final JobParameters params = intent.getParcelableExtra(JOB_PARAMS_EXTRA_KEY);
+ Log.d(TAG, "Received action " + intent.getAction());
+ synchronized (mTestJobStatus) {
+ switch (intent.getAction()) {
+ case ACTION_JOB_STARTED:
+ mTestJobStatus.running = true;
+ mTestJobStatus.jobId = params.getJobId();
+ mTestJobStatus.stopReason = JobParameters.REASON_CANCELED;
+ break;
+ case ACTION_JOB_STOPPED:
+ mTestJobStatus.running = false;
+ mTestJobStatus.jobId = params.getJobId();
+ mTestJobStatus.stopReason = params.getStopReason();
+ break;
+ }
+ }
+ }
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+ mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
+ ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
+ mIActivityManager = ActivityManager.getService();
+ mTestPackageUid = mContext.getPackageManager().getPackageUid(TEST_APP_PACKAGE, 0);
+ mTestJobId = (int) (SystemClock.uptimeMillis() / 1000);
+ mTestJobStatus.reset();
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ACTION_JOB_STARTED);
+ intentFilter.addAction(ACTION_JOB_STOPPED);
+ mContext.registerReceiver(mJobStateChangeReceiver, intentFilter);
+ setGlobalSwitch(true);
+ setAppOpsModeAllowed(true);
+ setPowerWhiteListed(false);
+ }
+
+ private void scheduleAndAssertJobStarted() throws Exception {
+ final Intent scheduleJobIntent = new Intent(TestJobActivity.ACTION_START_JOB);
+ scheduleJobIntent.putExtra(TestJobActivity.EXTRA_JOB_ID_KEY, mTestJobId);
+ scheduleJobIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ scheduleJobIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY));
+ mContext.startActivity(scheduleJobIntent);
+ Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY);
+ assertTrue("Job did not start after scheduling", awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+ }
+
+ @Test
+ public void testGlobalSwitch() throws Exception {
+ setGlobalSwitch(false); // Job should not stop now.
+ scheduleAndAssertJobStarted();
+ setAppOpsModeAllowed(false);
+ mIActivityManager.makePackageIdle(TEST_APP_PACKAGE, UserHandle.USER_CURRENT);
+ assertFalse("Job stopped even when feature switch is off",
+ awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+ }
+
+ @Test
+ public void testPowerWhiteList() throws Exception {
+ scheduleAndAssertJobStarted();
+ setAppOpsModeAllowed(false);
+ mIActivityManager.makePackageIdle(TEST_APP_PACKAGE, UserHandle.USER_CURRENT);
+ assertTrue("Job did not stop after making idle", awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+ setPowerWhiteListed(true);
+ Thread.sleep(TestJobActivity.JOB_INITIAL_BACKOFF);
+ assertTrue("Job did not start after adding to power whitelist",
+ awaitJobStart(DEFAULT_WAIT_TIMEOUT));
+ setPowerWhiteListed(false);
+ assertTrue("Job did not stop after removing from power whitelist",
+ awaitJobStop(DEFAULT_WAIT_TIMEOUT));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ Intent cancelJobsIntent = new Intent(TestJobActivity.ACTION_CANCEL_JOBS);
+ cancelJobsIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY));
+ cancelJobsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(cancelJobsIntent);
+ mContext.unregisterReceiver(mJobStateChangeReceiver);
+ setGlobalSwitch(false);
+ setAppOpsModeAllowed(true);
+ setPowerWhiteListed(false);
+ }
+
+ private void setGlobalSwitch(boolean enabled) {
+ Settings.Global.putString(mContext.getContentResolver(),
+ Settings.Global.JOB_SCHEDULER_CONSTANTS, "bg_jobs_restricted=" + enabled);
+ }
+
+ private void setPowerWhiteListed(boolean whitelist) throws RemoteException {
+ if (whitelist) {
+ mDeviceIdleController.addPowerSaveWhitelistApp(TEST_APP_PACKAGE);
+ } else {
+ mDeviceIdleController.removePowerSaveWhitelistApp(TEST_APP_PACKAGE);
+ }
+ }
+
+ private void setAppOpsModeAllowed(boolean allow) throws PackageManager.NameNotFoundException {
+ mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mTestPackageUid,
+ TEST_APP_PACKAGE, allow ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
+ }
+
+ private boolean awaitJobStart(long timeout) throws InterruptedException {
+ return waitUntilTrue(timeout, () -> {
+ synchronized (mTestJobStatus) {
+ return (mTestJobStatus.jobId == mTestJobId) && mTestJobStatus.running;
+ }
+ });
+ }
+
+ private boolean awaitJobStop(long timeout) throws InterruptedException {
+ return waitUntilTrue(timeout, () -> {
+ synchronized (mTestJobStatus) {
+ return (mTestJobStatus.jobId == mTestJobId) && !mTestJobStatus.running &&
+ mTestJobStatus.stopReason == JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED;
+ }
+ });
+ }
+
+ private boolean waitUntilTrue(long timeout, Condition condition) throws InterruptedException {
+ final long deadLine = SystemClock.uptimeMillis() + timeout;
+ do {
+ Thread.sleep(POLL_INTERVAL);
+ } while (!condition.isTrue() && SystemClock.uptimeMillis() < deadLine);
+ return condition.isTrue();
+ }
+
+ private static final class TestJobStatus {
+ int jobId;
+ int stopReason;
+ boolean running;
+ private void reset() {
+ running = false;
+ stopReason = jobId = 0;
+ }
+ }
+
+ private interface Condition {
+ boolean isTrue();
+ }
+}
public void setUp() throws Exception {
super.setUp();
MockitoAnnotations.initMocks(this);
- mBatterySaverPolicy = new BatterySaverPolicy(mHandler);
+ mBatterySaverPolicy = new BatterySaverPolicy(getContext(), mHandler);
mBatterySaverPolicy.start(getContext().getContentResolver());
}
--- /dev/null
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_SDK_VERSION := current
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := JobTestApp
+LOCAL_DEX_PREOPT := false
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.servicestests.apps.jobtestapp">
+
+ <application>
+ <service android:name=".TestJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+ <activity android:name=".TestJobActivity"
+ android:exported="true" />
+ </application>
+
+</manifest>
\ No newline at end of file
--- /dev/null
+/*
+ * 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.servicestests.apps.jobtestapp;
+
+import android.app.Activity;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+public class TestJobActivity extends Activity {
+ private static final String TAG = TestJobActivity.class.getSimpleName();
+ public static final String EXTRA_JOB_ID_KEY =
+ "com.android.servicestests.apps.jobtestapp.extra.JOB_ID";
+ public static final String ACTION_START_JOB =
+ "com.android.servicestests.apps.jobtestapp.extra.START_JOB";
+ public static final String ACTION_CANCEL_JOBS =
+ "com.android.servicestests.apps.jobtestapp.extra.CANCEL_JOBS";
+ public static final int JOB_INITIAL_BACKOFF = 10_000;
+ public static final int JOB_MINIMUM_LATENCY = 5_000;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ComponentName jobServiceComponent = new ComponentName(this, TestJobService.class);
+ JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ final Intent intent = getIntent();
+ switch (intent.getAction()) {
+ case ACTION_CANCEL_JOBS:
+ jobScheduler.cancelAll();
+ Log.d(TAG, "Cancelled all jobs for " + getPackageName());
+ break;
+ case ACTION_START_JOB:
+ final int jobId = intent.getIntExtra(EXTRA_JOB_ID_KEY, hashCode());
+ JobInfo.Builder jobBuilder = new JobInfo.Builder(jobId, jobServiceComponent)
+ .setBackoffCriteria(JOB_INITIAL_BACKOFF, JobInfo.BACKOFF_POLICY_LINEAR)
+ .setMinimumLatency(JOB_MINIMUM_LATENCY);
+ final int result = jobScheduler.schedule(jobBuilder.build());
+ if (result != JobScheduler.RESULT_SUCCESS) {
+ Log.e(TAG, "Could not schedule job " + jobId);
+ } else {
+ Log.d(TAG, "Successfully scheduled job with id " + jobId);
+ }
+ break;
+ }
+ finish();
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * 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.servicestests.apps.jobtestapp;
+
+import android.annotation.TargetApi;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.Intent;
+import android.util.Log;
+
+@TargetApi(24)
+public class TestJobService extends JobService {
+ private static final String TAG = TestJobService.class.getSimpleName();
+ private static final String PACKAGE_NAME = "com.android.servicestests.apps.jobtestapp";
+ public static final String ACTION_JOB_STARTED = PACKAGE_NAME + ".action.JOB_STARTED";
+ public static final String ACTION_JOB_STOPPED = PACKAGE_NAME + ".action.JOB_STOPPED";
+ public static final String JOB_PARAMS_EXTRA_KEY = PACKAGE_NAME + ".extra.JOB_PARAMETERS";
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ Log.i(TAG, "Test job executing: " + params.getJobId());
+ Intent reportJobStartIntent = new Intent(ACTION_JOB_STARTED);
+ reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+ sendBroadcast(reportJobStartIntent);
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ Log.i(TAG, "Test job stopped executing: " + params.getJobId());
+ Intent reportJobStopIntent = new Intent(ACTION_JOB_STOPPED);
+ reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+ sendBroadcast(reportJobStopIntent);
+ return true;
+ }
+}