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.content.Context; 19 import android.content.SharedPreferences; 20 import android.os.Handler; 21 import android.os.IDeviceIdleController; 22 import android.os.IMaintenanceActivityListener; 23 import android.os.Message; 24 import android.os.RemoteException; 25 import android.os.ServiceManager; 26 import android.util.Log; 27 28 import com.android.internal.annotations.GuardedBy; 29 import com.android.internal.annotations.VisibleForTesting; 30 31 import java.io.PrintWriter; 32 import java.util.Calendar; 33 34 /** 35 * Controls car garage mode. 36 * 37 * Car garage mode is a time window for the car to do maintenance work when the car is not in use. 38 * The {@link com.android.car.GarageModeService} interacts with {@link com.android.car.CarPowerManagementService} 39 * to start and end garage mode. A {@link com.android.car.GarageModeService.GarageModePolicy} defines 40 * when the garage mode should start and how long it should last. 41 */ 42 public class GarageModeService implements CarServiceBase, 43 CarPowerManagementService.PowerEventProcessingHandler, 44 CarPowerManagementService.PowerServiceEventListener, 45 DeviceIdleControllerWrapper.DeviceMaintenanceActivityListener { 46 private static String TAG = "GarageModeService"; 47 private static String GARAGE_MODE_PREFERENCE_FILE = "com.android.car.PREFERENCE_FILE_KEY"; 48 private static String GARAGE_MODE_INDEX = "garage_mode_index"; 49 50 private static final int MSG_EXIT_GARAGE_MODE_EARLY = 0; 51 private static final int MSG_WRITE_TO_PREF = 1; 52 53 // TODO: move this to garage mode policy too. 54 @VisibleForTesting 55 protected static final int MAINTENANCE_WINDOW = 5 * 60 * 1000; // 5 minutes 56 // wait for 10 seconds to allow maintenance activities to start (e.g., connecting to wifi). 57 protected static final int MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD = 10 * 1000; 58 59 private final CarPowerManagementService mPowerManagementService; 60 protected final Context mContext; 61 62 @VisibleForTesting 63 @GuardedBy("this") 64 protected boolean mInGarageMode; 65 @VisibleForTesting 66 @GuardedBy("this") 67 protected boolean mMaintenanceActive; 68 @VisibleForTesting 69 @GuardedBy("this") 70 protected int mGarageModeIndex; 71 72 private final Object mPolicyLock = new Object(); 73 @GuardedBy("mPolicyLock") 74 private GarageModePolicy mPolicy; 75 76 private SharedPreferences mSharedPreferences; 77 78 private DeviceIdleControllerWrapper mDeviceIdleController; 79 private GarageModeHandler mHandler = new GarageModeHandler(); 80 81 private class GarageModeHandler extends Handler { 82 @Override handleMessage(Message msg)83 public void handleMessage(Message msg) { 84 switch (msg.what) { 85 case MSG_EXIT_GARAGE_MODE_EARLY: 86 mPowerManagementService.notifyPowerEventProcessingCompletion( 87 GarageModeService.this); 88 break; 89 case MSG_WRITE_TO_PREF: 90 writeToPref(msg.arg1); 91 break; 92 } 93 } 94 } 95 GarageModeService(Context context, CarPowerManagementService powerManagementService)96 public GarageModeService(Context context, CarPowerManagementService powerManagementService) { 97 this(context, powerManagementService, null); 98 } 99 100 @VisibleForTesting GarageModeService(Context context, CarPowerManagementService powerManagementService, DeviceIdleControllerWrapper deviceIdleController)101 protected GarageModeService(Context context, CarPowerManagementService powerManagementService, 102 DeviceIdleControllerWrapper deviceIdleController) { 103 mContext = context; 104 mPowerManagementService = powerManagementService; 105 if (deviceIdleController == null) { 106 mDeviceIdleController = new DefaultDeviceIdleController(); 107 } else { 108 mDeviceIdleController = deviceIdleController; 109 } 110 } 111 112 @Override init()113 public void init() { 114 Log.d(TAG, "init GarageMode"); 115 mSharedPreferences = mContext.getSharedPreferences( 116 GARAGE_MODE_PREFERENCE_FILE, Context.MODE_PRIVATE); 117 synchronized (mPolicyLock) { 118 readPolicyLocked(); 119 } 120 121 final int index = mSharedPreferences.getInt(GARAGE_MODE_INDEX, 0); 122 synchronized (this) { 123 mMaintenanceActive = mDeviceIdleController.startTracking(this); 124 mGarageModeIndex = index; 125 } 126 mPowerManagementService.registerPowerEventProcessingHandler(this); 127 } 128 129 @Override release()130 public void release() { 131 Log.d(TAG, "release GarageModeService"); 132 mDeviceIdleController.stopTracking(); 133 } 134 135 @Override dump(PrintWriter writer)136 public void dump(PrintWriter writer) { 137 writer.println("mGarageModeIndex: " + mGarageModeIndex); 138 writer.println("inGarageMode? " + mInGarageMode); 139 } 140 141 @Override onPrepareShutdown(boolean shuttingDown)142 public long onPrepareShutdown(boolean shuttingDown) { 143 // this is the beginning of each garage mode. 144 synchronized (this) { 145 Log.d(TAG, "onPrePowerEvent " + shuttingDown); 146 mInGarageMode = true; 147 mGarageModeIndex++; 148 mHandler.removeMessages(MSG_EXIT_GARAGE_MODE_EARLY); 149 if (!mMaintenanceActive) { 150 mHandler.sendMessageDelayed( 151 mHandler.obtainMessage(MSG_EXIT_GARAGE_MODE_EARLY), 152 MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD); 153 } 154 // We always reserve the maintenance window first. If later, we found no 155 // maintenance work active, we will exit garage mode early after 156 // MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD 157 return MAINTENANCE_WINDOW; 158 } 159 } 160 161 @Override onPowerOn(boolean displayOn)162 public void onPowerOn(boolean displayOn) { 163 synchronized (this) { 164 Log.d(TAG, "onPowerOn: " + displayOn); 165 if (displayOn) { 166 // the car is use now. reset the garage mode counter. 167 mGarageModeIndex = 0; 168 } 169 } 170 } 171 172 @Override getWakeupTime()173 public int getWakeupTime() { 174 final int index; 175 synchronized (this) { 176 index = mGarageModeIndex; 177 } 178 synchronized (mPolicyLock) { 179 return mPolicy.getNextWakeUpTime(index); 180 } 181 } 182 183 @Override onSleepExit()184 public void onSleepExit() { 185 // ignored 186 } 187 188 @Override onSleepEntry()189 public void onSleepEntry() { 190 synchronized (this) { 191 mInGarageMode = false; 192 } 193 } 194 195 @Override onShutdown()196 public void onShutdown() { 197 synchronized (this) { 198 mHandler.sendMessage( 199 mHandler.obtainMessage(MSG_WRITE_TO_PREF, mGarageModeIndex, 0)); 200 } 201 } 202 readPolicyLocked()203 private void readPolicyLocked() { 204 Log.d(TAG, "readPolicyLocked"); 205 // TODO: define a xml schema for garage mode policy and read it from system dir. 206 mPolicy = new DefaultGarageModePolicy(); 207 } 208 writeToPref(int index)209 private void writeToPref(int index) { 210 SharedPreferences.Editor editor = mSharedPreferences.edit(); 211 editor.putInt(GARAGE_MODE_INDEX, index); 212 editor.commit(); 213 } 214 215 @Override onMaintenanceActivityChanged(boolean active)216 public void onMaintenanceActivityChanged(boolean active) { 217 boolean shouldReportCompletion = false; 218 synchronized (this) { 219 Log.d(TAG, "onMaintenanceActivityChanged: " + active); 220 mMaintenanceActive = active; 221 if (!mInGarageMode) { 222 return; 223 } 224 225 if (!active) { 226 shouldReportCompletion = true; 227 mInGarageMode = false; 228 } else { 229 // we are in garage mode, and maintenance work has just begun. 230 mHandler.removeMessages(MSG_EXIT_GARAGE_MODE_EARLY); 231 } 232 } 233 if (shouldReportCompletion) { 234 // we are in garage mode, and maintenance work has finished. 235 mPowerManagementService.notifyPowerEventProcessingCompletion(this); 236 } 237 } 238 239 public abstract static class GarageModePolicy { getNextWakeUpTime(int index)240 abstract public int getNextWakeUpTime(int index); 241 /** 242 * Returns number of seconds between now to 1am {@param numDays} days later. 243 */ nextWakeUpSeconds(int numDays)244 public static int nextWakeUpSeconds(int numDays) { 245 // TODO: Should select a random time within a window to avoid all cars update at the 246 // same time. 247 Calendar next = Calendar.getInstance(); 248 next.add(Calendar.DATE, numDays); 249 next.set(Calendar.HOUR_OF_DAY, 1); 250 next.set(Calendar.MINUTE, 0); 251 next.set(Calendar.SECOND, 0); 252 253 Calendar now = Calendar.getInstance(); 254 return (next.get(Calendar.MILLISECOND) - now.get(Calendar.MILLISECOND)) / 1000; 255 } 256 } 257 258 /** 259 * Default garage mode policy. 260 * 261 * The first wake up time is set to be 1am the next day. And it keeps waking up every day for a 262 * week. After that, wake up every 7 days for a month, and wake up every 30 days thereafter. 263 */ 264 private static class DefaultGarageModePolicy extends GarageModePolicy { 265 private static final int COL_INDEX = 0; 266 private static final int COL_WAKEUP_TIME = 1; 267 268 private static final int[][] WAKE_UP_TIME = new int[][] { 269 {7 /*index <= 7*/, 1 /* wake up the next day */}, 270 {11 /* 7 < index <= 11 */, 7 /* wake up the next week */}, 271 {Integer.MAX_VALUE /* index > 11 */, 30 /* wake up the next month */} 272 }; 273 274 @Override getNextWakeUpTime(int index)275 public int getNextWakeUpTime(int index) { 276 for (int i = 0; i < WAKE_UP_TIME.length; i++) { 277 if (index <= WAKE_UP_TIME[i][COL_INDEX]) { 278 return nextWakeUpSeconds(WAKE_UP_TIME[i][COL_WAKEUP_TIME]); 279 } 280 } 281 282 Log.w(TAG, "Integer.MAX number of wake ups... How long have we been sleeping? "); 283 return 0; 284 } 285 } 286 287 private static class DefaultDeviceIdleController extends DeviceIdleControllerWrapper { 288 private IDeviceIdleController mDeviceIdleController; 289 private MaintenanceActivityListener mMaintenanceActivityListener 290 = new MaintenanceActivityListener(); 291 292 @Override startLocked()293 public boolean startLocked() { 294 mDeviceIdleController = IDeviceIdleController.Stub.asInterface( 295 ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); 296 boolean active = false; 297 try { 298 active = mDeviceIdleController 299 .registerMaintenanceActivityListener(mMaintenanceActivityListener); 300 } catch (RemoteException e) { 301 Log.e(TAG, "Unable to register listener with DeviceIdleController", e); 302 } 303 return active; 304 } 305 306 @Override stopTracking()307 public void stopTracking() { 308 try { 309 if (mDeviceIdleController != null) { 310 mDeviceIdleController.unregisterMaintenanceActivityListener( 311 mMaintenanceActivityListener); 312 } 313 } catch (RemoteException e) { 314 Log.e(TAG, "Fail to unregister listener.", e); 315 } 316 } 317 318 private final class MaintenanceActivityListener extends IMaintenanceActivityListener.Stub { 319 @Override onMaintenanceActivityChanged(final boolean active)320 public void onMaintenanceActivityChanged(final boolean active) { 321 DefaultDeviceIdleController.this.setMaintenanceActivity(active); 322 } 323 } 324 } 325 } 326