1 /* 2 * Copyright (C) 2015 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 package com.android.car; 17 18 import android.car.CarApiUtil; 19 import android.car.settings.CarSettings; 20 import android.car.settings.GarageModeSettingsObserver; 21 import android.content.Context; 22 import android.content.SharedPreferences; 23 import android.net.Uri; 24 import android.os.Handler; 25 import android.os.IDeviceIdleController; 26 import android.os.IMaintenanceActivityListener; 27 import android.os.Looper; 28 import android.os.Message; 29 import android.os.RemoteException; 30 import android.os.ServiceManager; 31 import android.preference.PreferenceManager; 32 import android.provider.Settings; 33 import android.util.Log; 34 35 import com.android.internal.annotations.GuardedBy; 36 import com.android.internal.annotations.VisibleForTesting; 37 38 import java.io.PrintWriter; 39 import java.lang.RuntimeException; 40 import java.util.Map; 41 import java.util.HashMap; 42 43 import static android.car.settings.GarageModeSettingsObserver.GARAGE_MODE_ENABLED_URI; 44 import static android.car.settings.GarageModeSettingsObserver.GARAGE_MODE_MAINTENANCE_WINDOW_URI; 45 import static android.car.settings.GarageModeSettingsObserver.GARAGE_MODE_WAKE_UP_TIME_URI; 46 47 /** 48 * Controls car garage mode. 49 * 50 * Car garage mode is a time window for the car to do maintenance work when the car is not in use. 51 * The {@link com.android.car.GarageModeService} interacts with {@link com.android.car.CarPowerManagementService} 52 * to start and end garage mode. A {@link com.android.car.GarageModeService.GarageModePolicy} defines 53 * when the garage mode should start and how long it should last. 54 */ 55 public class GarageModeService implements CarServiceBase, 56 CarPowerManagementService.PowerEventProcessingHandler, 57 CarPowerManagementService.PowerServiceEventListener, 58 DeviceIdleControllerWrapper.DeviceMaintenanceActivityListener { 59 private static String TAG = "GarageModeService"; 60 private static final boolean DBG = false; 61 62 private static final int MSG_EXIT_GARAGE_MODE_EARLY = 0; 63 private static final int MSG_WRITE_TO_PREF = 1; 64 65 private static final String KEY_GARAGE_MODE_INDEX = "garage_mode_index"; 66 67 // wait for 10 seconds to allow maintenance activities to start (e.g., connecting to wifi). 68 protected static final int MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD = 10 * 1000; 69 70 private final CarPowerManagementService mPowerManagementService; 71 protected final Context mContext; 72 73 @VisibleForTesting 74 @GuardedBy("this") 75 protected boolean mInGarageMode; 76 @VisibleForTesting 77 @GuardedBy("this") 78 protected boolean mMaintenanceActive; 79 @VisibleForTesting 80 @GuardedBy("this") 81 protected int mGarageModeIndex; 82 83 @GuardedBy("this") 84 @VisibleForTesting 85 protected int mMaintenanceWindow; 86 @GuardedBy("this") 87 private GarageModePolicy mPolicy; 88 @GuardedBy("this") 89 private boolean mGarageModeEnabled; 90 91 92 private SharedPreferences mSharedPreferences; 93 private final GarageModeSettingsObserver mContentObserver; 94 95 private DeviceIdleControllerWrapper mDeviceIdleController; 96 private final GarageModeHandler mHandler; 97 98 private class GarageModeHandler extends Handler { GarageModeHandler(Looper looper)99 public GarageModeHandler(Looper looper) { 100 super(looper); 101 } 102 103 @Override handleMessage(Message msg)104 public void handleMessage(Message msg) { 105 switch (msg.what) { 106 case MSG_EXIT_GARAGE_MODE_EARLY: 107 mPowerManagementService.notifyPowerEventProcessingCompletion( 108 GarageModeService.this); 109 break; 110 case MSG_WRITE_TO_PREF: 111 writeToPref(msg.arg1); 112 break; 113 } 114 } 115 } 116 GarageModeService(Context context, CarPowerManagementService powerManagementService)117 public GarageModeService(Context context, CarPowerManagementService powerManagementService) { 118 this(context, powerManagementService, null, Looper.myLooper()); 119 } 120 121 @VisibleForTesting GarageModeService(Context context, CarPowerManagementService powerManagementService, DeviceIdleControllerWrapper deviceIdleController, Looper looper)122 protected GarageModeService(Context context, CarPowerManagementService powerManagementService, 123 DeviceIdleControllerWrapper deviceIdleController, Looper looper) { 124 mContext = context; 125 mPowerManagementService = powerManagementService; 126 mHandler = new GarageModeHandler(looper); 127 if (deviceIdleController == null) { 128 mDeviceIdleController = new DefaultDeviceIdleController(); 129 } else { 130 mDeviceIdleController = deviceIdleController; 131 } 132 mContentObserver = new GarageModeSettingsObserver(mContext, mHandler) { 133 @Override 134 public void onChange(boolean selfChange, Uri uri) { 135 onSettingsChangedInternal(uri); 136 } 137 }; 138 } 139 140 @Override init()141 public void init() { 142 init(mContext.getResources().getStringArray(R.array.config_garageModeCadence), 143 PreferenceManager.getDefaultSharedPreferences( 144 mContext.createDeviceProtectedStorageContext())); 145 } 146 147 @VisibleForTesting init(String[] policyArray, SharedPreferences prefs)148 protected void init(String[] policyArray, SharedPreferences prefs) { 149 logd("init GarageMode"); 150 mSharedPreferences = prefs; 151 final int index = mSharedPreferences.getInt(KEY_GARAGE_MODE_INDEX, 0); 152 synchronized (this) { 153 mMaintenanceActive = mDeviceIdleController.startTracking(this); 154 mGarageModeIndex = index; 155 readPolicyLocked(policyArray); 156 readFromSettingsLocked(CarSettings.Global.KEY_GARAGE_MODE_MAINTENANCE_WINDOW, 157 CarSettings.Global.KEY_GARAGE_MODE_ENABLED, 158 CarSettings.Global.KEY_GARAGE_MODE_WAKE_UP_TIME); 159 } 160 mContentObserver.register(); 161 mPowerManagementService.registerPowerEventProcessingHandler(this); 162 } 163 isInGarageMode()164 public boolean isInGarageMode() { 165 return mInGarageMode; 166 } 167 168 @Override release()169 public void release() { 170 logd("release GarageModeService"); 171 mDeviceIdleController.stopTracking(); 172 mContentObserver.unregister(); 173 } 174 175 @Override dump(PrintWriter writer)176 public void dump(PrintWriter writer) { 177 writer.println("mGarageModeIndex: " + mGarageModeIndex); 178 writer.println("inGarageMode? " + mInGarageMode); 179 writer.println("GarageModeEnabled " + mGarageModeEnabled); 180 writer.println("GarageModeTimeWindow " + mMaintenanceWindow + " ms"); 181 } 182 183 @Override onPrepareShutdown(boolean shuttingDown)184 public long onPrepareShutdown(boolean shuttingDown) { 185 // this is the beginning of each garage mode. 186 synchronized (this) { 187 logd("onPrePowerEvent " + shuttingDown); 188 mInGarageMode = true; 189 mGarageModeIndex++; 190 mHandler.removeMessages(MSG_EXIT_GARAGE_MODE_EARLY); 191 if (!mMaintenanceActive) { 192 mHandler.sendMessageDelayed( 193 mHandler.obtainMessage(MSG_EXIT_GARAGE_MODE_EARLY), 194 MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD); 195 } 196 // We always reserve the maintenance window first. If later, we found no 197 // maintenance work active, we will exit garage mode early after 198 // MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD 199 return mMaintenanceWindow; 200 } 201 } 202 203 @Override onPowerOn(boolean displayOn)204 public void onPowerOn(boolean displayOn) { 205 synchronized (this) { 206 logd("onPowerOn: " + displayOn); 207 if (displayOn) { 208 // the car is use now. reset the garage mode counter. 209 mGarageModeIndex = 0; 210 } 211 } 212 } 213 214 @Override getWakeupTime()215 public int getWakeupTime() { 216 synchronized (this) { 217 if (!mGarageModeEnabled) { 218 return 0; 219 } 220 return mPolicy.getNextWakeUpTime(mGarageModeIndex); 221 } 222 } 223 224 @Override onSleepExit()225 public void onSleepExit() { 226 // ignored 227 } 228 229 @Override onSleepEntry()230 public void onSleepEntry() { 231 synchronized (this) { 232 mInGarageMode = false; 233 } 234 } 235 236 @Override onShutdown()237 public void onShutdown() { 238 synchronized (this) { 239 mHandler.sendMessage( 240 mHandler.obtainMessage(MSG_WRITE_TO_PREF, mGarageModeIndex, 0)); 241 } 242 } 243 244 @GuardedBy("this") readPolicyLocked(String[] policyArray)245 private void readPolicyLocked(String[] policyArray) { 246 logd("readPolicy"); 247 mPolicy = new GarageModePolicy(policyArray); 248 } 249 writeToPref(int index)250 private void writeToPref(int index) { 251 SharedPreferences.Editor editor = mSharedPreferences.edit(); 252 editor.putInt(KEY_GARAGE_MODE_INDEX, index); 253 editor.commit(); 254 } 255 256 @Override onMaintenanceActivityChanged(boolean active)257 public void onMaintenanceActivityChanged(boolean active) { 258 boolean shouldReportCompletion = false; 259 synchronized (this) { 260 logd("onMaintenanceActivityChanged: " + active); 261 mMaintenanceActive = active; 262 if (!mInGarageMode) { 263 return; 264 } 265 266 if (!active) { 267 shouldReportCompletion = true; 268 mInGarageMode = false; 269 } else { 270 // we are in garage mode, and maintenance work has just begun. 271 mHandler.removeMessages(MSG_EXIT_GARAGE_MODE_EARLY); 272 } 273 } 274 if (shouldReportCompletion) { 275 // we are in garage mode, and maintenance work has finished. 276 mPowerManagementService.notifyPowerEventProcessingCompletion(this); 277 } 278 } 279 280 static final class WakeupTime { 281 int mWakeupTime; 282 int mNumAttempts; 283 WakeupTime(int wakeupTime, int numAttempts)284 WakeupTime(int wakeupTime, int numAttempts) { 285 mWakeupTime = wakeupTime; 286 mNumAttempts = numAttempts; 287 } 288 }; 289 290 /** 291 * Default garage mode policy. 292 * 293 * The first wake up time is set to be 1am the next day. And it keeps waking up every day for a 294 * week. After that, wake up every 7 days for a month, and wake up every 30 days thereafter. 295 */ 296 @VisibleForTesting 297 static final class GarageModePolicy { 298 private static final Map<String, Integer> TIME_UNITS_LOOKUP; 299 static { 300 TIME_UNITS_LOOKUP = new HashMap<String, Integer>(); 301 TIME_UNITS_LOOKUP.put("m", 60); 302 TIME_UNITS_LOOKUP.put("h", 3600); 303 TIME_UNITS_LOOKUP.put("d", 86400); 304 } 305 protected WakeupTime[] mWakeupTime; 306 GarageModePolicy(String[] policy)307 public GarageModePolicy(String[] policy) { 308 if (policy == null || policy.length == 0) { 309 throw new RuntimeException("Must include valid policy."); 310 } 311 312 WakeupTime[] parsedWakeupTime = new WakeupTime[policy.length]; 313 for (int i = 0; i < policy.length; i++) { 314 String[] splitString = policy[i].split(","); 315 if (splitString.length != 2) { 316 throw new RuntimeException("Bad policy format: " + policy[i]); 317 } 318 319 int wakeupTimeMs = 0; 320 int numAttempts = 0; 321 String wakeupTime = splitString[0]; 322 if (wakeupTime.isEmpty()) { 323 throw new RuntimeException("Missing wakeup time: " + policy[i]); 324 } 325 String wakeupTimeValue = wakeupTime.substring(0, wakeupTime.length() - 1); 326 327 if (wakeupTimeValue.isEmpty()) { 328 throw new RuntimeException("Missing wakeup time value: " + wakeupTime); 329 } 330 try { 331 int timeCount = Integer.parseInt(wakeupTimeValue); 332 if (timeCount <= 0) { 333 throw new RuntimeException("Wakeup time must be > 0: " + timeCount); 334 } 335 336 // Last character indicates minutes, hours, or days. 337 Integer multiplier = TIME_UNITS_LOOKUP.get( 338 wakeupTime.substring(wakeupTime.length() - 1)); 339 if (multiplier == null) { 340 throw new RuntimeException("Bad wakeup time units: " + wakeupTime); 341 } 342 343 wakeupTimeMs = timeCount * multiplier; 344 } catch (NumberFormatException e) { 345 throw new RuntimeException("Bad wakeup time value: " + wakeupTimeValue); 346 } 347 try { 348 numAttempts = Integer.parseInt(splitString[1]); 349 if (numAttempts <= 0) { 350 throw new RuntimeException( 351 "Number of attempts must be > 0: " + numAttempts); 352 } 353 } catch (NumberFormatException e) { 354 throw new RuntimeException("Bad number of attempts: " + splitString[1]); 355 } 356 357 parsedWakeupTime[i] = new WakeupTime(wakeupTimeMs, numAttempts); 358 } 359 mWakeupTime = parsedWakeupTime; 360 } 361 getNextWakeUpTime(int index)362 public int getNextWakeUpTime(int index) { 363 if (mWakeupTime == null) { 364 Log.e(TAG, "Could not parse policy."); 365 return 0; 366 } 367 368 for (int i = 0; i < mWakeupTime.length; i++) { 369 if (index < mWakeupTime[i].mNumAttempts) { 370 return mWakeupTime[i].mWakeupTime; 371 } else { 372 index -= mWakeupTime[i].mNumAttempts; 373 } 374 } 375 376 Log.w(TAG, "No more garage mode wake ups scheduled; been sleeping too long."); 377 return 0; 378 } 379 } 380 381 private static class DefaultDeviceIdleController extends DeviceIdleControllerWrapper { 382 private IDeviceIdleController mDeviceIdleController; 383 private MaintenanceActivityListener mMaintenanceActivityListener 384 = new MaintenanceActivityListener(); 385 386 @Override startLocked()387 public boolean startLocked() { 388 mDeviceIdleController = IDeviceIdleController.Stub.asInterface( 389 ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); 390 boolean active = false; 391 try { 392 active = mDeviceIdleController 393 .registerMaintenanceActivityListener(mMaintenanceActivityListener); 394 } catch (RemoteException e) { 395 Log.e(TAG, "Unable to register listener with DeviceIdleController", e); 396 } 397 return active; 398 } 399 400 @Override stopTracking()401 public void stopTracking() { 402 try { 403 if (mDeviceIdleController != null) { 404 mDeviceIdleController.unregisterMaintenanceActivityListener( 405 mMaintenanceActivityListener); 406 } 407 } catch (RemoteException e) { 408 Log.e(TAG, "Fail to unregister listener.", e); 409 } 410 } 411 412 private final class MaintenanceActivityListener extends IMaintenanceActivityListener.Stub { 413 @Override onMaintenanceActivityChanged(final boolean active)414 public void onMaintenanceActivityChanged(final boolean active) { 415 DefaultDeviceIdleController.this.setMaintenanceActivity(active); 416 } 417 } 418 } 419 logd(String msg)420 private void logd(String msg) { 421 if (DBG) { 422 Log.d(TAG, msg); 423 } 424 } 425 426 @GuardedBy("this") readFromSettingsLocked(String... keys)427 private void readFromSettingsLocked(String... keys) { 428 for (String key : keys) { 429 switch (key) { 430 case CarSettings.Global.KEY_GARAGE_MODE_ENABLED: 431 mGarageModeEnabled = 432 Settings.Global.getInt(mContext.getContentResolver(), key, 1) == 1; 433 break; 434 case CarSettings.Global.KEY_GARAGE_MODE_MAINTENANCE_WINDOW: 435 mMaintenanceWindow = Settings.Global.getInt( 436 mContext.getContentResolver(), key, 437 CarSettings.DEFAULT_GARAGE_MODE_MAINTENANCE_WINDOW); 438 break; 439 default: 440 Log.e(TAG, "Unknown setting key " + key); 441 } 442 } 443 } 444 onSettingsChangedInternal(Uri uri)445 private void onSettingsChangedInternal(Uri uri) { 446 synchronized (this) { 447 logd("Content Observer onChange: " + uri); 448 if (uri.equals(GARAGE_MODE_ENABLED_URI)) { 449 readFromSettingsLocked(CarSettings.Global.KEY_GARAGE_MODE_ENABLED); 450 } else if (uri.equals(GARAGE_MODE_WAKE_UP_TIME_URI)) { 451 readFromSettingsLocked(CarSettings.Global.KEY_GARAGE_MODE_WAKE_UP_TIME); 452 } else if (uri.equals(GARAGE_MODE_MAINTENANCE_WINDOW_URI)) { 453 readFromSettingsLocked(CarSettings.Global.KEY_GARAGE_MODE_MAINTENANCE_WINDOW); 454 } 455 logd(String.format( 456 "onSettingsChanged %s. enabled: %s, windowSize: %d", 457 uri, mGarageModeEnabled, mMaintenanceWindow)); 458 } 459 } 460 } 461