1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.garagemode; 18 19 import android.app.job.JobInfo; 20 import android.app.job.JobScheduler; 21 import android.app.job.JobSnapshot; 22 import android.content.Intent; 23 import android.os.Handler; 24 import android.os.UserHandle; 25 import android.util.ArraySet; 26 27 import com.android.car.CarLocalServices; 28 import com.android.car.CarStatsLog; 29 import com.android.car.user.CarUserService; 30 import com.android.internal.annotations.VisibleForTesting; 31 32 import java.util.ArrayList; 33 import java.util.List; 34 import java.util.concurrent.CancellationException; 35 import java.util.concurrent.CompletableFuture; 36 37 /** 38 * Class that interacts with JobScheduler, controls system idleness and monitor jobs which are 39 * in GarageMode interest 40 */ 41 42 class GarageMode { 43 private static final Logger LOG = new Logger("GarageMode"); 44 45 /** 46 * When changing this field value, please update 47 * {@link com.android.server.job.controllers.idle.CarIdlenessTracker} as well. 48 */ 49 public static final String ACTION_GARAGE_MODE_ON = 50 "com.android.server.jobscheduler.GARAGE_MODE_ON"; 51 52 /** 53 * When changing this field value, please update 54 * {@link com.android.server.job.controllers.idle.CarIdlenessTracker} as well. 55 */ 56 public static final String ACTION_GARAGE_MODE_OFF = 57 "com.android.server.jobscheduler.GARAGE_MODE_OFF"; 58 59 @VisibleForTesting 60 static final long JOB_SNAPSHOT_INITIAL_UPDATE_MS = 10_000; // 10 seconds 61 62 private static final long JOB_SNAPSHOT_UPDATE_FREQUENCY_MS = 1_000; // 1 second 63 private static final long USER_STOP_CHECK_INTERVAL = 10_000; // 10 secs 64 private static final int ADDITIONAL_CHECKS_TO_DO = 1; 65 66 private final Controller mController; 67 68 private boolean mGarageModeActive; 69 private int mAdditionalChecksToDo = ADDITIONAL_CHECKS_TO_DO; 70 private JobScheduler mJobScheduler; 71 private Handler mHandler; 72 private Runnable mRunnable = new Runnable() { 73 @Override 74 public void run() { 75 int numberRunning = numberOfIdleJobsRunning(); 76 if (numberRunning > 0) { 77 LOG.d("" + numberRunning + " jobs are still running. Need to wait more ..."); 78 mAdditionalChecksToDo = ADDITIONAL_CHECKS_TO_DO; 79 } else { 80 // No idle-mode jobs are running. 81 // Are there any scheduled idle jobs that could run now? 82 int numberReadyToRun = numberOfJobsPending(); 83 if (numberReadyToRun == 0) { 84 LOG.d("No jobs are running. No jobs are pending. Exiting Garage Mode."); 85 finish(); 86 return; 87 } 88 if (mAdditionalChecksToDo == 0) { 89 LOG.d("No jobs are running. Waited too long for " 90 + numberReadyToRun + " pending jobs. Exiting Garage Mode."); 91 finish(); 92 return; 93 } 94 LOG.d("No jobs are running. Waiting " + mAdditionalChecksToDo 95 + " more cycles for " + numberReadyToRun + " pending jobs."); 96 mAdditionalChecksToDo--; 97 } 98 mHandler.postDelayed(mRunnable, JOB_SNAPSHOT_UPDATE_FREQUENCY_MS); 99 } 100 }; 101 102 private final Runnable mStopUserCheckRunnable = new Runnable() { 103 @Override 104 public void run() { 105 int userToStop = UserHandle.USER_SYSTEM; // BG user never becomes system user. 106 int remainingUsersToStop = 0; 107 synchronized (this) { 108 remainingUsersToStop = mStartedBackgroundUsers.size(); 109 if (remainingUsersToStop > 0) { 110 userToStop = mStartedBackgroundUsers.valueAt(0); 111 } else { 112 return; 113 } 114 } 115 if (numberOfIdleJobsRunning() == 0) { // all jobs done or stopped. 116 // Keep user until job scheduling is stopped. Otherwise, it can crash jobs. 117 if (userToStop != UserHandle.USER_SYSTEM) { 118 CarLocalServices.getService(CarUserService.class).stopBackgroundUser( 119 userToStop); 120 LOG.i("Stopping background user:" + userToStop + " remaining users:" 121 + (remainingUsersToStop - 1)); 122 } 123 synchronized (this) { 124 mStartedBackgroundUsers.remove(userToStop); 125 if (mStartedBackgroundUsers.size() == 0) { 126 LOG.i("all background users stopped"); 127 return; 128 } 129 } 130 } else { 131 LOG.i("Waiting for jobs to finish, remaining users:" + remainingUsersToStop); 132 } 133 // Poll again 134 mHandler.postDelayed(mStopUserCheckRunnable, USER_STOP_CHECK_INTERVAL); 135 } 136 }; 137 138 139 private CompletableFuture<Void> mFuture; 140 private ArraySet<Integer> mStartedBackgroundUsers = new ArraySet<>(); 141 GarageMode(Controller controller)142 GarageMode(Controller controller) { 143 mGarageModeActive = false; 144 mController = controller; 145 mJobScheduler = controller.getJobSchedulerService(); 146 mHandler = controller.getHandler(); 147 } 148 isGarageModeActive()149 boolean isGarageModeActive() { 150 return mGarageModeActive; 151 } 152 dump()153 List<String> dump() { 154 List<String> outString = new ArrayList<>(); 155 if (!mGarageModeActive) { 156 return outString; 157 } 158 List<String> jobList = new ArrayList<>(); 159 int numJobs = getListOfIdleJobsRunning(jobList); 160 if (numJobs > 0) { 161 outString.add("GarageMode is waiting for " + numJobs + " jobs:"); 162 // Dump the names of the jobs that we are waiting for 163 for (int idx = 0; idx < jobList.size(); idx++) { 164 outString.add(" " + (idx + 1) + ": " + jobList.get(idx)); 165 } 166 } else { 167 // Dump the names of the pending jobs that we are waiting for 168 numJobs = getListOfPendingJobs(jobList); 169 outString.add("GarageMode is waiting for " + jobList.size() + " pending idle jobs:"); 170 for (int idx = 0; idx < jobList.size(); idx++) { 171 outString.add(" " + (idx + 1) + ": " + jobList.get(idx)); 172 } 173 } 174 return outString; 175 } 176 enterGarageMode(CompletableFuture<Void> future)177 void enterGarageMode(CompletableFuture<Void> future) { 178 LOG.d("Entering GarageMode"); 179 synchronized (this) { 180 mGarageModeActive = true; 181 } 182 updateFuture(future); 183 broadcastSignalToJobSchedulerTo(true); 184 CarStatsLog.logGarageModeStart(); 185 startMonitoringThread(); 186 ArrayList<Integer> startedUsers = 187 CarLocalServices.getService(CarUserService.class).startAllBackgroundUsers(); 188 synchronized (this) { 189 mStartedBackgroundUsers.addAll(startedUsers); 190 } 191 } 192 cancel()193 synchronized void cancel() { 194 broadcastSignalToJobSchedulerTo(false); 195 if (mFuture != null && !mFuture.isDone()) { 196 mFuture.cancel(true); 197 } 198 mFuture = null; 199 startBackgroundUserStopping(); 200 } 201 finish()202 synchronized void finish() { 203 broadcastSignalToJobSchedulerTo(false); 204 CarStatsLog.logGarageModeStop(); 205 mController.scheduleNextWakeup(); 206 synchronized (this) { 207 if (mFuture != null && !mFuture.isDone()) { 208 mFuture.complete(null); 209 } 210 mFuture = null; 211 } 212 startBackgroundUserStopping(); 213 } 214 cleanupGarageMode()215 private void cleanupGarageMode() { 216 LOG.d("Cleaning up GarageMode"); 217 synchronized (this) { 218 mGarageModeActive = false; 219 } 220 stopMonitoringThread(); 221 mHandler.removeCallbacks(mRunnable); 222 startBackgroundUserStopping(); 223 } 224 startBackgroundUserStopping()225 private void startBackgroundUserStopping() { 226 synchronized (this) { 227 if (mStartedBackgroundUsers.size() > 0) { 228 mHandler.postDelayed(mStopUserCheckRunnable, USER_STOP_CHECK_INTERVAL); 229 } 230 } 231 } 232 updateFuture(CompletableFuture<Void> future)233 private void updateFuture(CompletableFuture<Void> future) { 234 synchronized (this) { 235 mFuture = future; 236 } 237 if (mFuture != null) { 238 mFuture.whenComplete((result, exception) -> { 239 if (exception == null) { 240 LOG.d("GarageMode completed normally"); 241 } else if (exception instanceof CancellationException) { 242 LOG.d("GarageMode was canceled"); 243 } else { 244 LOG.e("GarageMode ended due to exception: ", exception); 245 } 246 cleanupGarageMode(); 247 }); 248 } 249 } 250 broadcastSignalToJobSchedulerTo(boolean enableGarageMode)251 private void broadcastSignalToJobSchedulerTo(boolean enableGarageMode) { 252 Intent i = new Intent(); 253 if (enableGarageMode) { 254 i.setAction(ACTION_GARAGE_MODE_ON); 255 } else { 256 i.setAction(ACTION_GARAGE_MODE_OFF); 257 } 258 i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_NO_ABORT); 259 mController.sendBroadcast(i); 260 } 261 startMonitoringThread()262 private synchronized void startMonitoringThread() { 263 mAdditionalChecksToDo = ADDITIONAL_CHECKS_TO_DO; 264 mHandler.postDelayed(mRunnable, JOB_SNAPSHOT_INITIAL_UPDATE_MS); 265 } 266 stopMonitoringThread()267 private synchronized void stopMonitoringThread() { 268 mHandler.removeCallbacks(mRunnable); 269 } 270 numberOfIdleJobsRunning()271 private int numberOfIdleJobsRunning() { 272 return getListOfIdleJobsRunning(null); 273 } 274 getListOfIdleJobsRunning(List<String> jobList)275 private int getListOfIdleJobsRunning(List<String> jobList) { 276 if (jobList != null) { 277 jobList.clear(); 278 } 279 List<JobInfo> startedJobs = mJobScheduler.getStartedJobs(); 280 if (startedJobs == null) { 281 return 0; 282 } 283 int count = 0; 284 for (int idx = 0; idx < startedJobs.size(); idx++) { 285 JobInfo jobInfo = startedJobs.get(idx); 286 if (jobInfo.isRequireDeviceIdle()) { 287 count++; 288 if (jobList != null) { 289 jobList.add(jobInfo.toString()); 290 } 291 } 292 } 293 return count; 294 } 295 numberOfJobsPending()296 private int numberOfJobsPending() { 297 return getListOfPendingJobs(null); 298 } 299 getListOfPendingJobs(List<String> jobList)300 private int getListOfPendingJobs(List<String> jobList) { 301 if (jobList != null) { 302 jobList.clear(); 303 } 304 List<JobSnapshot> allScheduledJobs = mJobScheduler.getAllJobSnapshots(); 305 if (allScheduledJobs == null) { 306 return 0; 307 } 308 int numberPending = 0; 309 for (int idx = 0; idx < allScheduledJobs.size(); idx++) { 310 JobSnapshot scheduledJob = allScheduledJobs.get(idx); 311 JobInfo jobInfo = scheduledJob.getJobInfo(); 312 if (scheduledJob.isRunnable() && jobInfo.isRequireDeviceIdle()) { 313 numberPending++; 314 if (jobList != null) { 315 jobList.add(jobInfo.toString()); 316 } 317 } 318 } 319 return numberPending; 320 } 321 } 322