1 /*
2  * Copyright (C) 2020 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.cts.verifier.car;
18 
19 import android.app.job.JobInfo;
20 import android.app.job.JobParameters;
21 import android.app.job.JobScheduler;
22 import android.app.job.JobService;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.SharedPreferences;
26 import android.net.ConnectivityManager;
27 import android.net.NetworkInfo;
28 import android.os.AsyncTask;
29 import android.os.Handler;
30 import android.os.Message;
31 import android.os.PersistableBundle;
32 import android.provider.Settings;
33 import android.util.Log;
34 import android.util.SparseArray;
35 
36 import java.lang.ref.WeakReference;
37 
38 public final class GarageModeChecker extends JobService {
39     private static final String TAG = GarageModeChecker.class.getSimpleName();
40 
41     static final String PREFS_FILE_NAME = "GarageModeChecker_prefs";
42     static final String PREFS_INITIATION = "test-initiation";
43     static final String PREFS_GARAGE_MODE_START = "garage-mode-start";
44     static final String PREFS_GARAGE_MODE_END = "garage-mode-end";
45     static final String PREFS_TERMINATION = "termination-time";
46     static final String PREFS_JOB_UPDATE = "job-update-time";
47     static final String PREFS_HAD_CONNECTIVITY = "had-connectivity";
48     static final String PREFS_START_BOOT_COUNT = "start-boot-count";
49 
50     static final int SECONDS_PER_ITERATION = 10;
51     static final int MS_PER_ITERATION = SECONDS_PER_ITERATION * 1000;
52 
53     private static final int GARAGE_JOB_ID = GarageModeTestActivity.class.hashCode();
54     private static final String JOB_NUMBER = "job_number";
55     private static final String REMAINING_SECONDS = "remaining_seconds";
56     // JobScheduler allows a maximum of 10 minutes for a job, but depending on vendor implementation
57     // Garage Mode may not last that long. So, max job duration is set to 60 seconds.
58     private static final int MAX_SECONDS_PER_JOB = 60;
59 
60     private static final int MSG_FINISHED = 0;
61     private static final int MSG_RUN_JOB = 1;
62     private static final int MSG_CANCEL_JOB = 2;
63 
64     private Context mIdleJobContext;
65 
66     private boolean mReportFirstExecution = true;
67 
scheduleAnIdleJob(Context context, int durationSeconds)68     public static void scheduleAnIdleJob(Context context, int durationSeconds) {
69         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
70         // Get the next available job ID
71         int highestJobNumber = 0;
72         for (JobInfo jobInfo : jobScheduler.getAllPendingJobs()) {
73             int jobId = jobInfo.getId();
74             if (highestJobNumber < jobId) {
75                 highestJobNumber = jobId;
76             }
77         }
78         scheduleJob(context, highestJobNumber + 1, durationSeconds);
79     }
80 
scheduleJob(Context context, int jobNumber, int durationSeconds)81     private static void scheduleJob(Context context, int jobNumber, int durationSeconds) {
82         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
83         ComponentName jobComponentName = new ComponentName(context, GarageModeChecker.class);
84         JobInfo.Builder builder = new JobInfo.Builder(jobNumber, jobComponentName);
85 
86         PersistableBundle bundle = new PersistableBundle();
87         bundle.putInt(JOB_NUMBER, jobNumber);
88         bundle.putInt(REMAINING_SECONDS, durationSeconds);
89 
90         JobInfo jobInfo = builder
91                 .setRequiresDeviceIdle(true)
92                 .setExtras(bundle)
93                 .build();
94         jobScheduler.schedule(jobInfo);
95     }
96 
97     private final Handler mHandler = new Handler() {
98         private SparseArray<JobParameters> mTaskMap = new SparseArray<>();
99         private boolean mHaveContinuousConnectivity = true; // Assume true initially
100 
101         @Override
102         public void handleMessage(Message msg) {
103             JobParameters job = (JobParameters) msg.obj;
104             switch (msg.what) {
105                 case MSG_FINISHED:
106                     mTaskMap.remove(job.getJobId());
107                     jobFinished(job, false);
108                     break;
109                 case MSG_RUN_JOB:
110                     checkConnectivity(msg.arg1);
111                     GarageModeCheckerTask task = new GarageModeCheckerTask(this, job, msg.arg1);
112                     task.execute();
113                     mTaskMap.put(job.getJobId(), job);
114                     break;
115                 case MSG_CANCEL_JOB:
116                     JobParameters runningJob = mTaskMap.get(job.getJobId());
117                     if (runningJob != null) {
118                         removeMessages(MSG_RUN_JOB, runningJob);
119                         mTaskMap.remove(job.getJobId());
120                     }
121                     SharedPreferences prefs = mIdleJobContext.getSharedPreferences(PREFS_FILE_NAME,
122                             Context.MODE_PRIVATE);
123                     SharedPreferences.Editor editor = prefs.edit();
124                     editor.putLong(PREFS_TERMINATION, System.currentTimeMillis());
125                     editor.commit();
126                     Log.v(TAG, "Idle job was terminated");
127                     break;
128             }
129         }
130 
131         private void checkConnectivity(int iteration) {
132             if (mHaveContinuousConnectivity) {
133                 // Check if we still have internet connectivity
134                 NetworkInfo networkInfo = getApplicationContext()
135                         .getSystemService(ConnectivityManager.class).getActiveNetworkInfo();
136                 mHaveContinuousConnectivity = networkInfo != null
137                         && networkInfo.isAvailable() && networkInfo.isConnected();
138                 if (iteration == 0 || !mHaveContinuousConnectivity) {
139                     // Save the connectivity state on the first pass and
140                     // the first time we lose connectivity
141                     SharedPreferences prefs = mIdleJobContext.getSharedPreferences(PREFS_FILE_NAME,
142                             Context.MODE_PRIVATE);
143                     SharedPreferences.Editor editor = prefs.edit();
144                     editor.putBoolean(PREFS_HAD_CONNECTIVITY, mHaveContinuousConnectivity);
145                     editor.commit();
146                 }
147             }
148         }
149     };
150 
151     @Override
onStopJob(JobParameters jobParameters)152     public boolean onStopJob(JobParameters jobParameters) {
153         Message msg = mHandler.obtainMessage(MSG_CANCEL_JOB, 0, 0, jobParameters);
154         mHandler.sendMessage(msg);
155         return false;
156     }
157 
158     @Override
onStartJob(final JobParameters jobParameters)159     public boolean onStartJob(final JobParameters jobParameters) {
160         mIdleJobContext = getApplicationContext();
161         if (mReportFirstExecution) {
162             mReportFirstExecution = false;
163             // Remember the start time
164             SharedPreferences prefs = mIdleJobContext.getSharedPreferences(PREFS_FILE_NAME,
165                     Context.MODE_PRIVATE);
166             SharedPreferences.Editor editor = prefs.edit();
167             editor.putLong(PREFS_GARAGE_MODE_START, System.currentTimeMillis());
168             editor.commit();
169             Log.v(TAG, "Starting idle job");
170         }
171         Message msg = mHandler.obtainMessage(MSG_RUN_JOB, 0, 0, jobParameters);
172         mHandler.sendMessage(msg);
173         return true;
174     }
175 
getBootCount(Context context)176     static int getBootCount(Context context) {
177         int bootCount = 0;
178         try {
179             bootCount = Settings.Global.getInt(context.getContentResolver(),
180                     Settings.Global.BOOT_COUNT);
181         } catch (Settings.SettingNotFoundException e) {
182             Log.e(TAG, "Could not get Settings.Global.BOOT_COUNT: ", e);
183         }
184         return bootCount;
185     }
186 
187     private final class GarageModeCheckerTask extends AsyncTask<Void, Void, Boolean> {
188         private final WeakReference<Handler> mHandler;
189         private final JobParameters mJobParameter;
190         private final int mIteration;
191 
GarageModeCheckerTask(Handler handler, JobParameters jobParameters, int iteration)192         GarageModeCheckerTask(Handler handler, JobParameters jobParameters, int iteration) {
193             mHandler = new WeakReference<Handler>(handler);
194             mJobParameter = jobParameters;
195             mIteration = iteration;
196         }
197 
198         @Override
doInBackground(Void... infos)199         protected Boolean doInBackground(Void... infos) {
200             int remainingSeconds = mJobParameter.getExtras().getInt(REMAINING_SECONDS);
201             int myMaxTime = Math.min(remainingSeconds, MAX_SECONDS_PER_JOB);
202             int elapsedSeconds = SECONDS_PER_ITERATION * mIteration;
203             long now = System.currentTimeMillis();
204             SharedPreferences prefs = mIdleJobContext.getSharedPreferences(PREFS_FILE_NAME,
205                     Context.MODE_PRIVATE);
206             SharedPreferences.Editor editor = null;
207 
208             if (elapsedSeconds >= myMaxTime + SECONDS_PER_ITERATION) {
209                 // This job is done
210                 if (myMaxTime == remainingSeconds) {
211                     // This is the final job. Note the completion time.
212                     editor = prefs.edit();
213                     editor.putLong(PREFS_GARAGE_MODE_END, now);
214                     editor.commit();
215                     Log.v(TAG, "Idle job is finished");
216                 }
217                 return false;
218             }
219 
220             editor = prefs.edit();
221             editor.putLong(PREFS_JOB_UPDATE, now);
222             editor.commit();
223             if (elapsedSeconds >= myMaxTime && (myMaxTime < remainingSeconds)) {
224                 // This job is about to finish and there is more time remaining.
225                 // Schedule another job.
226                 scheduleJob(mIdleJobContext, mJobParameter.getJobId() + 1,
227                         remainingSeconds - elapsedSeconds);
228             }
229             return true;
230         }
231 
232         @Override
onPostExecute(Boolean result)233         protected void onPostExecute(Boolean result) {
234             final Handler handler = mHandler.get();
235             if (handler == null) {
236                 return;
237             }
238             if (result) {
239                 Message msg = handler.obtainMessage(MSG_RUN_JOB, mIteration + 1, 0, mJobParameter);
240                 handler.sendMessageDelayed(msg, MS_PER_ITERATION);
241             } else {
242                 Message msg = handler.obtainMessage(MSG_FINISHED, 0, 0, mJobParameter);
243                 handler.sendMessage(msg);
244             }
245         }
246     }
247 }
248