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