blob: 59363919821c1bfbf3b3c153b5f091b597ae0a3a [file] [log] [blame]
/*
* Copyright (C) 2018 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.car.garagemode;
import static com.android.car.internal.testing.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.app.job.JobSnapshot;
import android.car.user.CarUserManager;
import android.car.user.CarUserManager.UserLifecycleEvent;
import android.car.user.CarUserManager.UserLifecycleListener;
import android.content.Intent;
import android.os.Handler;
import android.os.UserHandle;
import android.util.ArraySet;
import com.android.car.CarLocalServices;
import com.android.car.CarLog;
import com.android.car.CarStatsLogHelper;
import com.android.car.internal.testing.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.power.CarPowerManagementService;
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.utils.Slogf;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
/**
* Class that interacts with JobScheduler, controls system idleness and monitor jobs which are
* in GarageMode interest
*/
class GarageMode {
private static final String TAG = CarLog.tagFor(GarageMode.class) + "_"
+ GarageMode.class.getSimpleName();
/**
* When changing this field value, please update
* {@link com.android.server.job.controllers.idle.CarIdlenessTracker} as well.
*/
public static final String ACTION_GARAGE_MODE_ON =
"com.android.server.jobscheduler.GARAGE_MODE_ON";
/**
* When changing this field value, please update
* {@link com.android.server.job.controllers.idle.CarIdlenessTracker} as well.
*/
public static final String ACTION_GARAGE_MODE_OFF =
"com.android.server.jobscheduler.GARAGE_MODE_OFF";
@VisibleForTesting
static final long JOB_SNAPSHOT_INITIAL_UPDATE_MS = 10_000; // 10 seconds
private static final long JOB_SNAPSHOT_UPDATE_FREQUENCY_MS = 1_000; // 1 second
private static final long USER_STOP_CHECK_INTERVAL_MS = 100; // 100 milliseconds
private static final int ADDITIONAL_CHECKS_TO_DO = 1;
private final Controller mController;
private final JobScheduler mJobScheduler;
private final Object mLock = new Object();
private final Handler mHandler;
private CarPowerManagementService mCarPowerManagementService;
@GuardedBy("mLock")
private boolean mGarageModeActive;
@GuardedBy("mLock")
private int mAdditionalChecksToDo = ADDITIONAL_CHECKS_TO_DO;
@GuardedBy("mLock")
private boolean mIdleCheckerIsRunning;
@GuardedBy("mLock")
private List<String> mPendingJobs = new ArrayList<>();
private final Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (!mGarageModeActive) {
Slogf.d(TAG, "Garage Mode is inactive. Stopping the idle-job checker.");
finish();
return;
}
int numberRunning = numberOfIdleJobsRunning();
if (numberRunning > 0) {
Slogf.d(TAG, "%d jobs are still running. Need to wait more ...", numberRunning);
synchronized (mLock) {
mAdditionalChecksToDo = ADDITIONAL_CHECKS_TO_DO;
}
} else {
// No idle-mode jobs are running.
// Are there any scheduled idle jobs that could run now?
int numberReadyToRun = numberOfPendingJobs();
if (numberReadyToRun == 0) {
Slogf.d(TAG, "No jobs are running. No jobs are pending. Exiting Garage Mode.");
finish();
return;
}
int numAdditionalChecks;
synchronized (mLock) {
numAdditionalChecks = mAdditionalChecksToDo;
if (mAdditionalChecksToDo > 0) {
mAdditionalChecksToDo--;
}
}
if (numAdditionalChecks == 0) {
Slogf.d(TAG, "No jobs are running. Waited too long for %d pending jobs. Exiting"
+ " Garage Mode.", numberReadyToRun);
finish();
return;
}
Slogf.d(TAG, "No jobs are running. Waiting %d more cycles for %d pending jobs.",
numAdditionalChecks, numberReadyToRun);
}
mHandler.postDelayed(mRunnable, JOB_SNAPSHOT_UPDATE_FREQUENCY_MS);
}
};
private final Runnable mStartBackgroundUsers = new Runnable() {
@Override
public void run() {
ArrayList<Integer> startedUsers = CarLocalServices.getService(CarUserService.class)
.startAllBackgroundUsersInGarageMode();
Slogf.i(TAG, "Started background user during garage mode: %s", startedUsers);
synchronized (mLock) {
// Stop stopping background users if there is any users left from last Garage mode,
// they would be stopped later.
mBackgroundUserStopInProcess = false;
mStartedBackgroundUsers.addAll(startedUsers);
}
}
};
private final Runnable mStopUserCheckRunnable = new Runnable() {
@Override
public void run() {
int userToStop = UserHandle.USER_SYSTEM; // BG user never becomes system user.
synchronized (mLock) {
if (mStartedBackgroundUsers.isEmpty() || !mBackgroundUserStopInProcess) return;
userToStop = mStartedBackgroundUsers.valueAt(0);
}
if (numberOfIdleJobsRunning() == 0) { // all jobs done or stopped.
// Keep user until job scheduling is stopped. Otherwise, it can crash jobs.
if (userToStop != UserHandle.USER_SYSTEM) {
CarLocalServices.getService(CarUserService.class)
.stopBackgroundUserInGagageMode(userToStop);
Slogf.i(TAG, "Stopping background user:%d remaining users:%d", userToStop,
mStartedBackgroundUsers.size() - 1);
}
synchronized (mLock) {
mStartedBackgroundUsers.remove(userToStop);
if (mStartedBackgroundUsers.isEmpty()) {
Slogf.i(TAG, "All background users have stopped");
mBackgroundUserStopInProcess = false;
return;
}
}
} else {
// Poll again later
mHandler.postDelayed(mStopUserCheckRunnable, USER_STOP_CHECK_INTERVAL_MS);
}
}
};
@GuardedBy("mLock")
private CompletableFuture<Void> mFuture;
@GuardedBy("mLock")
private ArraySet<Integer> mStartedBackgroundUsers = new ArraySet<>();
/**
* True when stopping of the background users is in process.
*
* <p> When garage mode exits, all background users started during GarageMode would be stopped
* one by one. mBackgroundUserStopInProcess would be true when stopping of the background users
* is in process.
*/
@GuardedBy("mLock")
private boolean mBackgroundUserStopInProcess;
GarageMode(Controller controller) {
mGarageModeActive = false;
mController = controller;
mJobScheduler = controller.getJobSchedulerService();
mHandler = controller.getHandler();
}
void init() {
CarLocalServices.getService(CarUserService.class)
.addUserLifecycleListener(mUserLifecycleListener);
}
void release() {
CarLocalServices.getService(CarUserService.class)
.removeUserLifecycleListener(mUserLifecycleListener);
}
/**
* When background users are queued to stop, this user lifecycle listener will ensure to stop
* them one by one by queuing next user when previous user is stopped.
*/
private final UserLifecycleListener mUserLifecycleListener = new UserLifecycleListener() {
@Override
public void onEvent(UserLifecycleEvent event) {
if (event.getEventType() != CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STOPPED) return;
synchronized (mLock) {
if (mBackgroundUserStopInProcess) {
mHandler.removeCallbacks(mStopUserCheckRunnable);
Slogf.i(TAG, "Background user stopped event received. User Id: %d. Queueing to "
+ "stop next background user.", event.getUserId());
mHandler.post(mStopUserCheckRunnable);
}
}
}
};
boolean isGarageModeActive() {
synchronized (mLock) {
return mGarageModeActive;
}
}
@VisibleForTesting
ArraySet<Integer> getStartedBackgroundUsers() {
synchronized (mLock) {
return mStartedBackgroundUsers;
}
}
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
void dump(PrintWriter writer) {
if (!mGarageModeActive) {
return;
}
writer.printf("GarageMode idle checker is %srunning\n",
(mIdleCheckerIsRunning ? "" : "not "));
List<String> jobList = new ArrayList<>();
int numJobs = getListOfIdleJobsRunning(jobList);
if (numJobs > 0) {
writer.printf("GarageMode is waiting for %d jobs:\n", numJobs);
// Dump the names of the jobs that we are waiting for
for (int idx = 0; idx < jobList.size(); idx++) {
writer.printf(" %d: %s\n", idx + 1, jobList.get(idx));
}
} else {
// Dump the names of the pending jobs that we are waiting for
numJobs = getListOfPendingJobs(jobList);
writer.printf("GarageMode is waiting for %d pending idle jobs:\n", jobList.size());
for (int idx = 0; idx < jobList.size(); idx++) {
writer.printf(" %d: %s\n", idx + 1, jobList.get(idx));
}
}
}
void enterGarageMode(CompletableFuture<Void> future) {
Slogf.d(TAG, "Entering GarageMode");
if (mCarPowerManagementService == null) {
mCarPowerManagementService = CarLocalServices.getService(
CarPowerManagementService.class);
}
if (mCarPowerManagementService != null
&& mCarPowerManagementService.garageModeShouldExitImmediately()) {
if (future != null && !future.isDone()) {
future.complete(null);
}
synchronized (mLock) {
mGarageModeActive = false;
}
return;
}
synchronized (mLock) {
mGarageModeActive = true;
}
updateFuture(future);
broadcastSignalToJobScheduler(true);
CarStatsLogHelper.logGarageModeStart();
startMonitoringThread();
mHandler.post(mStartBackgroundUsers);
}
void cancel() {
broadcastSignalToJobScheduler(false);
synchronized (mLock) {
if (mFuture == null) {
cleanupGarageMode();
} else if (!mFuture.isDone()) {
mFuture.cancel(true);
}
mFuture = null;
startBackgroundUserStoppingLocked();
}
}
void finish() {
synchronized (mLock) {
if (!mIdleCheckerIsRunning) {
Slogf.i(TAG, "Finishing Garage Mode. Idle checker is not running.");
return;
}
mIdleCheckerIsRunning = false;
}
broadcastSignalToJobScheduler(false);
CarStatsLogHelper.logGarageModeStop();
mController.scheduleNextWakeup();
synchronized (mLock) {
if (mFuture == null) {
cleanupGarageMode();
} else if (!mFuture.isDone()) {
mFuture.complete(null);
}
mFuture = null;
startBackgroundUserStoppingLocked();
}
}
private void cleanupGarageMode() {
Slogf.d(TAG, "Cleaning up GarageMode");
synchronized (mLock) {
mGarageModeActive = false;
stopMonitoringThread();
if (mIdleCheckerIsRunning) {
// The idle checker has not completed.
// Schedule it now so it completes promptly.
mHandler.post(mRunnable);
}
startBackgroundUserStoppingLocked();
}
}
private void startBackgroundUserStoppingLocked() {
synchronized (mLock) {
if (!mStartedBackgroundUsers.isEmpty() && !mBackgroundUserStopInProcess) {
Slogf.i(TAG, "Stopping of background user queued. Total background users to stop: "
+ "%d", mStartedBackgroundUsers.size());
mHandler.post(mStopUserCheckRunnable);
mBackgroundUserStopInProcess = true;
}
}
}
private void updateFuture(CompletableFuture<Void> future) {
synchronized (mLock) {
mFuture = future;
if (mFuture != null) {
mFuture.whenComplete((result, exception) -> {
if (exception == null) {
Slogf.d(TAG, "GarageMode completed normally");
} else if (exception instanceof CancellationException) {
Slogf.d(TAG, "GarageMode was canceled");
} else {
Slogf.e(TAG, "GarageMode ended due to exception", exception);
}
cleanupGarageMode();
});
}
}
}
private void broadcastSignalToJobScheduler(boolean enableGarageMode) {
Intent i = new Intent();
i.setAction(enableGarageMode ? ACTION_GARAGE_MODE_ON : ACTION_GARAGE_MODE_OFF);
i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_NO_ABORT);
mController.sendBroadcast(i);
}
private void startMonitoringThread() {
synchronized (mLock) {
mIdleCheckerIsRunning = true;
mAdditionalChecksToDo = ADDITIONAL_CHECKS_TO_DO;
}
mHandler.postDelayed(mRunnable, JOB_SNAPSHOT_INITIAL_UPDATE_MS);
}
private void stopMonitoringThread() {
mHandler.removeCallbacks(mRunnable);
}
private int numberOfIdleJobsRunning() {
return getListOfIdleJobsRunning(null);
}
private int getListOfIdleJobsRunning(List<String> jobList) {
if (jobList != null) {
jobList.clear();
}
List<JobInfo> startedJobs = mJobScheduler.getStartedJobs();
if (startedJobs == null) {
return 0;
}
int count = 0;
for (int idx = 0; idx < startedJobs.size(); idx++) {
JobInfo jobInfo = startedJobs.get(idx);
if (jobInfo.isRequireDeviceIdle()) {
count++;
if (jobList != null) {
jobList.add(jobInfo.toString());
}
}
}
return count;
}
private int numberOfPendingJobs() {
return getListOfPendingJobs(null);
}
private int getListOfPendingJobs(List<String> jobList) {
if (jobList != null) {
jobList.clear();
}
List<JobSnapshot> allScheduledJobs = mJobScheduler.getAllJobSnapshots();
if (allScheduledJobs == null) {
return 0;
}
int numberPending = 0;
for (int idx = 0; idx < allScheduledJobs.size(); idx++) {
JobSnapshot scheduledJob = allScheduledJobs.get(idx);
JobInfo jobInfo = scheduledJob.getJobInfo();
if (scheduledJob.isRunnable() && jobInfo.isRequireDeviceIdle()) {
numberPending++;
if (jobList != null) {
jobList.add(jobInfo.toString());
}
}
}
return numberPending;
}
}