diff options
Diffstat (limited to 'services')
8 files changed, 365 insertions, 8 deletions
diff --git a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java index 7a74e45..c2b0a4d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java +++ b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java @@ -73,6 +73,7 @@ class ActivityManagerDebugConfig { static final boolean DEBUG_URI_PERMISSION = DEBUG_ALL || false; static final boolean DEBUG_USER_LEAVING = DEBUG_ALL || false; static final boolean DEBUG_VISIBILITY = DEBUG_ALL || false; + static final boolean DEBUG_USAGE_STATS = DEBUG_ALL || true; static final String POSTFIX_BACKUP = (APPEND_CATEGORY_NAME) ? "_Backup" : ""; static final String POSTFIX_BROADCAST = (APPEND_CATEGORY_NAME) ? "_Broadcast" : ""; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b0b410b..cdaa5a3 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -43,6 +43,7 @@ import android.app.ITaskStackListener; import android.app.ProfilerInfo; import android.app.admin.DevicePolicyManager; import android.app.usage.UsageEvents; +import android.app.usage.UsageStats; import android.app.usage.UsageStatsManagerInternal; import android.appwidget.AppWidgetManager; import android.content.res.Resources; @@ -61,8 +62,8 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.DebugUtils; import android.util.SparseIntArray; - import android.view.Display; + import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.DumpHeapActivity; @@ -96,7 +97,6 @@ import com.android.server.pm.UserManagerService; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.wm.AppTransition; import com.android.server.wm.WindowManagerService; - import com.google.android.collect.Lists; import com.google.android.collect.Maps; @@ -17854,6 +17854,10 @@ public final class ActivityManagerService extends ActivityManagerNative app.lastCpuTime = app.curCpuTime; } + // Inform UsageStats of important process state change + // Must be called before updating setProcState + maybeUpdateUsageStats(app); + app.setProcState = app.curProcState; if (app.setProcState >= ActivityManager.PROCESS_STATE_HOME) { app.notCachedSinceIdle = false; @@ -17916,6 +17920,28 @@ public final class ActivityManagerService extends ActivityManagerNative return success; } + private void maybeUpdateUsageStats(ProcessRecord app) { + if (DEBUG_USAGE_STATS) { + Slog.d(TAG, "Checking proc [" + Arrays.toString(app.getPackageList()) + + "] state changes: old = " + app.setProcState + ", new = " + + app.curProcState); + } + if (mUsageStatsService == null) { + return; + } + if (app.curProcState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND + && (app.setProcState > ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND + || app.setProcState < 0)) { + String[] packages = app.getPackageList(); + if (packages != null) { + for (int i = 0; i < packages.length; i++) { + mUsageStatsService.reportEvent(packages[i], app.userId, + UsageEvents.Event.INTERACTION); + } + } + } + } + private final void setProcessTrackerStateLocked(ProcessRecord proc, int memFactor, long now) { if (proc.thread != null) { if (proc.baseProcessTracker != null) { diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index d79b5fd..ecda36a 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -51,6 +51,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.app.IBatteryStats; +import com.android.server.job.controllers.AppIdleController; import com.android.server.job.controllers.BatteryController; import com.android.server.job.controllers.ConnectivityController; import com.android.server.job.controllers.IdleController; @@ -317,6 +318,7 @@ public class JobSchedulerService extends com.android.server.SystemService mControllers.add(TimeController.get(this)); mControllers.add(IdleController.get(this)); mControllers.add(BatteryController.get(this)); + mControllers.add(AppIdleController.get(this)); mHandler = new JobHandler(context.getMainLooper()); mJobSchedulerStub = new JobSchedulerStub(); @@ -688,7 +690,6 @@ public class JobSchedulerService extends com.android.server.SystemService final boolean jobPending = mPendingJobs.contains(job); final boolean jobActive = isCurrentlyActiveLocked(job); final boolean userRunning = mStartedUsers.contains(job.getUserId()); - if (DEBUG) { Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + " ready=" + jobReady + " pending=" + jobPending @@ -738,6 +739,10 @@ public class JobSchedulerService extends com.android.server.SystemService } } if (availableContext != null) { + if (DEBUG) { + Slog.d(TAG, "About to run job " + + nextPending.getJob().getService().toString()); + } if (!availableContext.executeRunnableJob(nextPending)) { if (DEBUG) { Slog.d(TAG, "Error executing " + nextPending); diff --git a/services/core/java/com/android/server/job/controllers/AppIdleController.java b/services/core/java/com/android/server/job/controllers/AppIdleController.java new file mode 100644 index 0000000..03e9ad5 --- /dev/null +++ b/services/core/java/com/android/server/job/controllers/AppIdleController.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2015 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.usage.UsageStatsManagerInternal; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.BatteryManagerInternal; +import android.util.Slog; + +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.StateChangedListener; + +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Controls when apps are considered idle and if jobs pertaining to those apps should + * be executed. Apps that haven't been actively launched or accessed from a foreground app + * for a certain amount of time (maybe hours or days) are considered idle. When the app comes + * out of idle state, it will be allowed to run scheduled jobs. + */ +public class AppIdleController extends StateController + implements UsageStatsManagerInternal.AppIdleStateChangeListener { + + private static final String LOG_TAG = "AppIdleController"; + private static final boolean DEBUG = true; + + // Singleton factory + private static Object sCreationLock = new Object(); + private static volatile AppIdleController sController; + final ArrayList<JobStatus> mTrackedTasks = new ArrayList<JobStatus>(); + private final UsageStatsManagerInternal mUsageStatsInternal; + private final BatteryManagerInternal mBatteryManagerInternal; + private boolean mPluggedIn; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) { + int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + // TODO: Allow any charger type + onPluggedIn((plugged & BatteryManager.BATTERY_PLUGGED_AC) != 0); + } + } + }; + + public static AppIdleController get(JobSchedulerService service) { + synchronized (sCreationLock) { + if (sController == null) { + sController = new AppIdleController(service, service.getContext()); + } + return sController; + } + } + + private AppIdleController(StateChangedListener stateChangedListener, Context context) { + super(stateChangedListener, context); + mUsageStatsInternal = LocalServices.getService(UsageStatsManagerInternal.class); + mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class); + mPluggedIn = isPowered(); + mUsageStatsInternal.addAppIdleStateChangeListener(this); + registerReceivers(); + } + + private void registerReceivers() { + // Monitor battery charging state + IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + mContext.registerReceiver(mReceiver, filter); + } + + private boolean isPowered() { + // TODO: Allow any charger type + return mBatteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_AC); + } + + @Override + public void maybeStartTrackingJob(JobStatus jobStatus) { + synchronized (mTrackedTasks) { + mTrackedTasks.add(jobStatus); + String packageName = jobStatus.job.getService().getPackageName(); + final boolean appIdle = !mPluggedIn && mUsageStatsInternal.isAppIdle(packageName, + jobStatus.getUserId()); + if (DEBUG) { + Slog.d(LOG_TAG, "Start tracking, setting idle state of " + + packageName + " to " + appIdle); + } + jobStatus.appNotIdleConstraintSatisfied.set(!appIdle); + } + } + + @Override + public void maybeStopTrackingJob(JobStatus jobStatus) { + synchronized (mTrackedTasks) { + mTrackedTasks.remove(jobStatus); + } + } + + @Override + public void dumpControllerState(PrintWriter pw) { + // TODO: + } + + @Override + public void onAppIdleStateChanged(String packageName, int userId, boolean idle) { + boolean changed = false; + synchronized (mTrackedTasks) { + // If currently plugged in, we don't care about app idle state + if (mPluggedIn) { + return; + } + for (JobStatus task : mTrackedTasks) { + if (task.job.getService().getPackageName().equals(packageName) + && task.getUserId() == userId) { + if (task.appNotIdleConstraintSatisfied.get() != !idle) { + if (DEBUG) { + Slog.d(LOG_TAG, "App Idle state changed, setting idle state of " + + packageName + " to " + idle); + } + task.appNotIdleConstraintSatisfied.set(!idle); + changed = true; + } + } + } + } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } + } + + void onPluggedIn(boolean pluggedIn) { + // Flag if any app's idle state has changed + boolean changed = false; + synchronized (mTrackedTasks) { + if (mPluggedIn == pluggedIn) { + return; + } + mPluggedIn = pluggedIn; + for (JobStatus task : mTrackedTasks) { + String packageName = task.job.getService().getPackageName(); + final boolean appIdle = !mPluggedIn && mUsageStatsInternal.isAppIdle(packageName, + task.getUserId()); + if (DEBUG) { + Slog.d(LOG_TAG, "Plugged in " + pluggedIn + ", setting idle state of " + + packageName + " to " + appIdle); + } + if (task.appNotIdleConstraintSatisfied.get() == appIdle) { + task.appNotIdleConstraintSatisfied.set(!appIdle); + changed = true; + } + } + } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } + } +} diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java index e3c55b6..69c63f3 100644 --- a/services/core/java/com/android/server/job/controllers/JobStatus.java +++ b/services/core/java/com/android/server/job/controllers/JobStatus.java @@ -54,6 +54,7 @@ public class JobStatus { final AtomicBoolean idleConstraintSatisfied = new AtomicBoolean(); final AtomicBoolean unmeteredConstraintSatisfied = new AtomicBoolean(); final AtomicBoolean connectivityConstraintSatisfied = new AtomicBoolean(); + final AtomicBoolean appNotIdleConstraintSatisfied = new AtomicBoolean(); /** * Earliest point in the future at which this job will be eligible to run. A value of 0 @@ -199,8 +200,11 @@ public class JobStatus { * the constraints are satisfied <strong>or</strong> the deadline on the job has expired. */ public synchronized boolean isReady() { - return isConstraintsSatisfied() - || (hasDeadlineConstraint() && deadlineConstraintSatisfied.get()); + // Deadline constraint trumps other constraints + // AppNotIdle implicit constraint trumps all! + return (isConstraintsSatisfied() + || (hasDeadlineConstraint() && deadlineConstraintSatisfied.get())) + && appNotIdleConstraintSatisfied.get(); } /** @@ -229,6 +233,7 @@ public class JobStatus { + ",N=" + job.getNetworkType() + ",C=" + job.isRequireCharging() + ",I=" + job.isRequireDeviceIdle() + ",F=" + numFailures + ",P=" + job.isPersisted() + + ",ANI=" + appNotIdleConstraintSatisfied.get() + (isReady() ? "(READY)" : "") + "]"; } diff --git a/services/core/java/com/android/server/job/controllers/StateController.java b/services/core/java/com/android/server/job/controllers/StateController.java index efd1928..cda7c32 100644 --- a/services/core/java/com/android/server/job/controllers/StateController.java +++ b/services/core/java/com/android/server/job/controllers/StateController.java @@ -44,7 +44,7 @@ public abstract class StateController { /** * Implement the logic here to decide whether a job should be tracked by this controller. - * This logic is put here so the JobManger can be completely agnostic of Controller logic. + * This logic is put here so the JobManager can be completely agnostic of Controller logic. * Also called when updating a task, so implementing controllers have to be aware of * preexisting tasks. */ diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 5eefe6a..f458dbc 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -21,8 +21,10 @@ import android.app.AppOpsManager; import android.app.usage.ConfigurationStats; import android.app.usage.IUsageStatsManager; import android.app.usage.UsageEvents; +import android.app.usage.UsageEvents.Event; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManagerInternal; +import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -32,6 +34,8 @@ import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; import android.content.res.Configuration; +import android.database.ContentObserver; +import android.net.Uri; import android.os.Binder; import android.os.Environment; import android.os.Handler; @@ -42,6 +46,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; @@ -53,6 +58,7 @@ import com.android.server.SystemService; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -74,6 +80,7 @@ public class UsageStatsService extends SystemService implements static final int MSG_REPORT_EVENT = 0; static final int MSG_FLUSH_TO_DISK = 1; static final int MSG_REMOVE_USER = 2; + static final int MSG_INFORM_LISTENERS = 3; private final Object mLock = new Object(); Handler mHandler; @@ -85,6 +92,12 @@ public class UsageStatsService extends SystemService implements long mRealTimeSnapshot; long mSystemTimeSnapshot; + private static final long DEFAULT_APP_IDLE_THRESHOLD_MILLIS = 3L * 24 * 60 * 60 * 1000; //3 days + private long mAppIdleDurationMillis; + + private ArrayList<UsageStatsManagerInternal.AppIdleStateChangeListener> + mPackageAccessListeners = new ArrayList<>(); + public UsageStatsService(Context context) { super(context); } @@ -112,11 +125,24 @@ public class UsageStatsService extends SystemService implements mRealTimeSnapshot = SystemClock.elapsedRealtime(); mSystemTimeSnapshot = System.currentTimeMillis(); + // Look at primary user's secure setting for this. TODO: Maybe apply different + // thresholds for different users. + mAppIdleDurationMillis = Settings.Secure.getLongForUser(getContext().getContentResolver(), + Settings.Secure.APP_IDLE_DURATION, DEFAULT_APP_IDLE_THRESHOLD_MILLIS, + UserHandle.USER_OWNER); publishLocalService(UsageStatsManagerInternal.class, new LocalService()); publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService()); } + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_SYSTEM_SERVICES_READY) { + // Observe changes to the threshold + new SettingsObserver(mHandler).registerObserver(); + } + } + private class UserRemovedReceiver extends BroadcastReceiver { @Override @@ -235,7 +261,19 @@ public class UsageStatsService extends SystemService implements final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow); + final long lastUsed = service.getLastPackageAccessTime(event.mPackage); + final boolean previouslyIdle = hasPassedIdleDuration(lastUsed); service.reportEvent(event); + // Inform listeners if necessary + if ((event.mEventType == Event.MOVE_TO_FOREGROUND + || event.mEventType == Event.MOVE_TO_BACKGROUND + || event.mEventType == Event.INTERACTION)) { + if (previouslyIdle) { + // Slog.d(TAG, "Informing listeners of out-of-idle " + event.mPackage); + mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, userId, + /* idle = */ 0, event.mPackage)); + } + } } } @@ -308,6 +346,53 @@ public class UsageStatsService extends SystemService implements } } + /** + * Called by LocalService stub. + */ + long getLastPackageAccessTime(String packageName, int userId) { + synchronized (mLock) { + final long timeNow = checkAndGetTimeLocked(); + // android package is always considered non-idle. + // TODO: Add a generic whitelisting mechanism + if (packageName.equals("android")) { + return timeNow; + } + final UserUsageStatsService service = + getUserDataAndInitializeIfNeededLocked(userId, timeNow); + return service.getLastPackageAccessTime(packageName); + } + } + + void addListener(AppIdleStateChangeListener listener) { + synchronized (mLock) { + if (!mPackageAccessListeners.contains(listener)) { + mPackageAccessListeners.add(listener); + } + } + } + + void removeListener(AppIdleStateChangeListener listener) { + synchronized (mLock) { + mPackageAccessListeners.remove(listener); + } + } + + private boolean hasPassedIdleDuration(long lastUsed) { + final long now = System.currentTimeMillis(); + return lastUsed < now - mAppIdleDurationMillis; + } + + boolean isAppIdle(String packageName, int userId) { + final long lastUsed = getLastPackageAccessTime(packageName, userId); + return hasPassedIdleDuration(lastUsed); + } + + void informListeners(String packageName, int userId, boolean isIdle) { + for (AppIdleStateChangeListener listener : mPackageAccessListeners) { + listener.onAppIdleStateChanged(packageName, userId, isIdle); + } + } + private static boolean validRange(long currentTime, long beginTime, long endTime) { return beginTime <= currentTime && beginTime < endTime; } @@ -366,6 +451,10 @@ public class UsageStatsService extends SystemService implements removeUser(msg.arg1); break; + case MSG_INFORM_LISTENERS: + informListeners((String) msg.obj, msg.arg1, msg.arg2 == 1); + break; + default: super.handleMessage(msg); break; @@ -373,6 +462,29 @@ public class UsageStatsService extends SystemService implements } } + /** + * Observe settings changes for Settings.Secure.APP_IDLE_DURATION. + */ + private class SettingsObserver extends ContentObserver { + + SettingsObserver(Handler handler) { + super(handler); + } + + void registerObserver() { + getContext().getContentResolver().registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.APP_IDLE_DURATION), false, this, UserHandle.USER_OWNER); + } + + @Override + public void onChange(boolean selfChange, Uri uri, int userId) { + mAppIdleDurationMillis = Settings.Secure.getLongForUser(getContext().getContentResolver(), + Settings.Secure.APP_IDLE_DURATION, DEFAULT_APP_IDLE_THRESHOLD_MILLIS, + UserHandle.USER_OWNER); + // TODO: Check if we need to update idle states of all the apps + } + } + private class BinderService extends IUsageStatsManager.Stub { private boolean hasPermission(String callingPackage) { @@ -523,11 +635,32 @@ public class UsageStatsService extends SystemService implements } @Override + public boolean isAppIdle(String packageName, int userId) { + return UsageStatsService.this.isAppIdle(packageName, userId); + } + + @Override + public long getLastPackageAccessTime(String packageName, int userId) { + return UsageStatsService.this.getLastPackageAccessTime(packageName, userId); + } + + @Override public void prepareShutdown() { // This method *WILL* do IO work, but we must block until it is finished or else // we might not shutdown cleanly. This is ok to do with the 'am' lock held, because // we are shutting down. shutdown(); } + + @Override + public void addAppIdleStateChangeListener(AppIdleStateChangeListener listener) { + UsageStatsService.this.addListener(listener); + } + + @Override + public void removeAppIdleStateChangeListener( + AppIdleStateChangeListener listener) { + UsageStatsService.this.removeListener(listener); + } } } diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java index 75fa030..afe27c7 100644 --- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java @@ -65,7 +65,8 @@ class UserUsageStatsService { void onStatsUpdated(); } - UserUsageStatsService(Context context, int userId, File usageStatsDir, StatsUpdatedListener listener) { + UserUsageStatsService(Context context, int userId, File usageStatsDir, + StatsUpdatedListener listener) { mContext = context; mDailyExpiryDate = new UnixCalendar(0); mDatabase = new UsageStatsDatabase(usageStatsDir); @@ -161,7 +162,9 @@ class UserUsageStatsService { if (currentDailyStats.events == null) { currentDailyStats.events = new TimeSparseArray<>(); } - currentDailyStats.events.put(event.mTimeStamp, event); + if (event.mEventType != UsageEvents.Event.INTERACTION) { + currentDailyStats.events.put(event.mTimeStamp, event); + } for (IntervalStats stats : mCurrentStats) { if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) { @@ -328,6 +331,16 @@ class UserUsageStatsService { return new UsageEvents(results, table); } + long getLastPackageAccessTime(String packageName) { + final IntervalStats yearly = mCurrentStats[UsageStatsManager.INTERVAL_YEARLY]; + UsageStats packageUsage; + if ((packageUsage = yearly.packageStats.get(packageName)) == null) { + return -1; + } else { + return packageUsage.getLastTimeUsed(); + } + } + void persistActiveStats() { if (mStatsChanged) { Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk"); |