/* * 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 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 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 mFuture; @GuardedBy("mLock") private ArraySet mStartedBackgroundUsers = new ArraySet<>(); /** * True when stopping of the background users is in process. * *

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 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 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 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 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 jobList) { if (jobList != null) { jobList.clear(); } List 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 jobList) { if (jobList != null) { jobList.clear(); } List 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; } }