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