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