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.Message;
28 import android.os.RemoteException;
29 import android.os.ServiceManager;
30 import android.preference.PreferenceManager;
31 import android.provider.Settings;
32 import android.util.Log;
33 
34 import com.android.internal.annotations.GuardedBy;
35 import com.android.internal.annotations.VisibleForTesting;
36 
37 import java.io.PrintWriter;
38 import java.util.Calendar;
39 
40 import static android.car.settings.GarageModeSettingsObserver.GARAGE_MODE_ENABLED_URI;
41 import static android.car.settings.GarageModeSettingsObserver.GARAGE_MODE_MAINTENANCE_WINDOW_URI;
42 import static android.car.settings.GarageModeSettingsObserver.GARAGE_MODE_WAKE_UP_TIME_URI;
43 
44 /**
45  * Controls car garage mode.
46  *
47  * Car garage mode is a time window for the car to do maintenance work when the car is not in use.
48  * The {@link com.android.car.GarageModeService} interacts with {@link com.android.car.CarPowerManagementService}
49  * to start and end garage mode. A {@link com.android.car.GarageModeService.GarageModePolicy} defines
50  * when the garage mode should start and how long it should last.
51  */
52 public class GarageModeService implements CarServiceBase,
53         CarPowerManagementService.PowerEventProcessingHandler,
54         CarPowerManagementService.PowerServiceEventListener,
55         DeviceIdleControllerWrapper.DeviceMaintenanceActivityListener {
56     private static String TAG = "GarageModeService";
57     private static final boolean DBG = false;
58 
59     private static final int MSG_EXIT_GARAGE_MODE_EARLY = 0;
60     private static final int MSG_WRITE_TO_PREF = 1;
61 
62     private static final String KEY_GARAGE_MODE_INDEX = "garage_mode_index";
63 
64     // wait for 10 seconds to allow maintenance activities to start (e.g., connecting to wifi).
65     protected static final int MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD = 10 * 1000;
66 
67     private final CarPowerManagementService mPowerManagementService;
68     protected final Context mContext;
69 
70     @VisibleForTesting
71     @GuardedBy("this")
72     protected boolean mInGarageMode;
73     @VisibleForTesting
74     @GuardedBy("this")
75     protected boolean mMaintenanceActive;
76     @VisibleForTesting
77     @GuardedBy("this")
78     protected int mGarageModeIndex;
79 
80     @GuardedBy("this")
81     @VisibleForTesting
82     protected int mMaintenanceWindow;
83     @GuardedBy("this")
84     private GarageModePolicy mPolicy;
85     @GuardedBy("this")
86     private int mWakeUpHour = 0;
87     @GuardedBy("this")
88     private int mWakeUpMin = 0;
89     @GuardedBy("this")
90     private boolean mGarageModeEnabled;
91 
92 
93     private SharedPreferences mSharedPreferences;
94     private final GarageModeSettingsObserver mContentObserver;
95 
96     private DeviceIdleControllerWrapper mDeviceIdleController;
97     private final GarageModeHandler mHandler = new GarageModeHandler();
98 
99     private class GarageModeHandler extends Handler {
100         @Override
handleMessage(Message msg)101         public void handleMessage(Message msg) {
102             switch (msg.what) {
103                 case MSG_EXIT_GARAGE_MODE_EARLY:
104                     mPowerManagementService.notifyPowerEventProcessingCompletion(
105                             GarageModeService.this);
106                     break;
107                 case MSG_WRITE_TO_PREF:
108                     writeToPref(msg.arg1);
109                     break;
110             }
111         }
112     }
113 
GarageModeService(Context context, CarPowerManagementService powerManagementService)114     public GarageModeService(Context context, CarPowerManagementService powerManagementService) {
115         this(context, powerManagementService, null);
116     }
117 
118     @VisibleForTesting
GarageModeService(Context context, CarPowerManagementService powerManagementService, DeviceIdleControllerWrapper deviceIdleController)119     protected GarageModeService(Context context, CarPowerManagementService powerManagementService,
120             DeviceIdleControllerWrapper deviceIdleController) {
121         mContext = context;
122         mPowerManagementService = powerManagementService;
123         if (deviceIdleController == null) {
124             mDeviceIdleController = new DefaultDeviceIdleController();
125         } else {
126             mDeviceIdleController = deviceIdleController;
127         }
128         mContentObserver = new GarageModeSettingsObserver(mContext, mHandler) {
129             @Override
130             public void onChange(boolean selfChange, Uri uri) {
131                 onSettingsChangedInternal(uri);
132             }
133         };
134     }
135 
136     @Override
init()137     public void init() {
138         logd("init GarageMode");
139         mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(
140                 mContext.createDeviceProtectedStorageContext());
141         final int index = mSharedPreferences.getInt(KEY_GARAGE_MODE_INDEX, 0);
142         synchronized (this) {
143             mMaintenanceActive = mDeviceIdleController.startTracking(this);
144             mGarageModeIndex = index;
145             readPolicyLocked();
146             readFromSettingsLocked(CarSettings.Global.KEY_GARAGE_MODE_MAINTENANCE_WINDOW,
147                     CarSettings.Global.KEY_GARAGE_MODE_ENABLED,
148                     CarSettings.Global.KEY_GARAGE_MODE_WAKE_UP_TIME);
149         }
150         mContentObserver.register();
151         mPowerManagementService.registerPowerEventProcessingHandler(this);
152     }
153 
154     @Override
release()155     public void release() {
156         logd("release GarageModeService");
157         mDeviceIdleController.stopTracking();
158         mContentObserver.unregister();
159     }
160 
161     @Override
dump(PrintWriter writer)162     public void dump(PrintWriter writer) {
163         writer.println("mGarageModeIndex: " + mGarageModeIndex);
164         writer.println("inGarageMode? " + mInGarageMode);
165         writer.println("GarageModeTime: " + mWakeUpHour + ":" + mWakeUpMin);
166         writer.println("GarageModeEnabled " + mGarageModeEnabled);
167         writer.println("GarageModeTimeWindow " + mMaintenanceWindow + " ms");
168     }
169 
170     @Override
onPrepareShutdown(boolean shuttingDown)171     public long onPrepareShutdown(boolean shuttingDown) {
172         // this is the beginning of each garage mode.
173         synchronized (this) {
174             logd("onPrePowerEvent " + shuttingDown);
175             mInGarageMode = true;
176             mGarageModeIndex++;
177             mHandler.removeMessages(MSG_EXIT_GARAGE_MODE_EARLY);
178             if (!mMaintenanceActive) {
179                 mHandler.sendMessageDelayed(
180                         mHandler.obtainMessage(MSG_EXIT_GARAGE_MODE_EARLY),
181                         MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD);
182             }
183             // We always reserve the maintenance window first. If later, we found no
184             // maintenance work active, we will exit garage mode early after
185             // MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD
186             return mMaintenanceWindow;
187         }
188     }
189 
190     @Override
onPowerOn(boolean displayOn)191     public void onPowerOn(boolean displayOn) {
192         synchronized (this) {
193             logd("onPowerOn: " + displayOn);
194             if (displayOn) {
195                 // the car is use now. reset the garage mode counter.
196                 mGarageModeIndex = 0;
197             }
198         }
199     }
200 
201     @Override
getWakeupTime()202     public int getWakeupTime() {
203         synchronized (this) {
204             if (!mGarageModeEnabled) {
205                 return 0;
206             }
207             return mPolicy.getNextWakeUpTime(mGarageModeIndex, mWakeUpHour, mWakeUpMin);
208         }
209     }
210 
211     @Override
onSleepExit()212     public void onSleepExit() {
213         // ignored
214     }
215 
216     @Override
onSleepEntry()217     public void onSleepEntry() {
218         synchronized (this) {
219             mInGarageMode = false;
220         }
221     }
222 
223     @Override
onShutdown()224     public void onShutdown() {
225         synchronized (this) {
226             mHandler.sendMessage(
227                     mHandler.obtainMessage(MSG_WRITE_TO_PREF, mGarageModeIndex, 0));
228         }
229     }
230 
readPolicyLocked()231     private void readPolicyLocked() {
232         logd("readPolicy");
233         // TODO: define a xml schema for policy and read it from system dir. bug: 32096969
234         mPolicy = new DefaultGarageModePolicy();
235     }
236 
writeToPref(int index)237     private void writeToPref(int index) {
238         SharedPreferences.Editor editor = mSharedPreferences.edit();
239         editor.putInt(KEY_GARAGE_MODE_INDEX, index);
240         editor.commit();
241     }
242 
243     @Override
onMaintenanceActivityChanged(boolean active)244     public void onMaintenanceActivityChanged(boolean active) {
245         boolean shouldReportCompletion = false;
246         synchronized (this) {
247             logd("onMaintenanceActivityChanged: " + active);
248             mMaintenanceActive = active;
249             if (!mInGarageMode) {
250                 return;
251             }
252 
253             if (!active) {
254                 shouldReportCompletion = true;
255                 mInGarageMode = false;
256             } else {
257                 // we are in garage mode, and maintenance work has just begun.
258                 mHandler.removeMessages(MSG_EXIT_GARAGE_MODE_EARLY);
259             }
260         }
261         if (shouldReportCompletion) {
262             // we are in garage mode, and maintenance work has finished.
263             mPowerManagementService.notifyPowerEventProcessingCompletion(this);
264         }
265     }
266 
267     public abstract static class GarageModePolicy {
getNextWakeUpTime(int index, int hour, int min)268         abstract public int getNextWakeUpTime(int index, int hour, int min);
269         /**
270          * Returns number of seconds between now to 1am {@param numDays} days later.
271          */
nextWakeUpSeconds(int numDays, int hour, int min)272         public static int nextWakeUpSeconds(int numDays, int hour, int min) {
273             // TODO: Should select a random time within a window. bug: 32096386
274             // This is to avoid all cars update at the same time.
275             Calendar next = Calendar.getInstance();
276             next.add(Calendar.DATE, numDays);
277             next.set(Calendar.HOUR_OF_DAY, hour);
278             next.set(Calendar.MINUTE, min);
279             next.set(Calendar.SECOND, 0);
280 
281             Calendar now = Calendar.getInstance();
282             return (next.get(Calendar.MILLISECOND) - now.get(Calendar.MILLISECOND)) / 1000;
283         }
284     }
285 
286     /**
287      * Default garage mode policy.
288      *
289      * The first wake up time is set to be 1am the next day. And it keeps waking up every day for a
290      * week. After that, wake up every 7 days for a month, and wake up every 30 days thereafter.
291      */
292     private static class DefaultGarageModePolicy extends GarageModePolicy {
293         private static final int COL_INDEX = 0;
294         private static final int COL_WAKEUP_TIME = 1;
295 
296         private static final int[][] WAKE_UP_TIME = new int[][] {
297                 {7 /*index <= 7*/, 1 /* wake up the next day */},
298                 {11 /* 7 < index <= 11 */, 7 /* wake up the next week */},
299                 {Integer.MAX_VALUE /* index > 11 */, 30 /* wake up the next month */}
300         };
301 
302         @Override
getNextWakeUpTime(int index, int hour, int min)303         public int getNextWakeUpTime(int index, int hour, int min) {
304             for (int i = 0; i < WAKE_UP_TIME.length; i++) {
305                 if (index <= WAKE_UP_TIME[i][COL_INDEX]) {
306                     return nextWakeUpSeconds(WAKE_UP_TIME[i][COL_WAKEUP_TIME], hour, min);
307                 }
308             }
309 
310             Log.w(TAG, "Integer.MAX number of wake ups... How long have we been sleeping? ");
311             return 0;
312         }
313     }
314 
315     private static class DefaultDeviceIdleController extends DeviceIdleControllerWrapper {
316         private IDeviceIdleController mDeviceIdleController;
317         private MaintenanceActivityListener mMaintenanceActivityListener
318                 = new MaintenanceActivityListener();
319 
320         @Override
startLocked()321         public boolean startLocked() {
322             mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
323                     ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
324             boolean active = false;
325             try {
326                 active = mDeviceIdleController
327                         .registerMaintenanceActivityListener(mMaintenanceActivityListener);
328             } catch (RemoteException e) {
329                 Log.e(TAG, "Unable to register listener with DeviceIdleController", e);
330             }
331             return active;
332         }
333 
334         @Override
stopTracking()335         public void stopTracking() {
336             try {
337                 if (mDeviceIdleController != null) {
338                     mDeviceIdleController.unregisterMaintenanceActivityListener(
339                             mMaintenanceActivityListener);
340                 }
341             } catch (RemoteException e) {
342                 Log.e(TAG, "Fail to unregister listener.", e);
343             }
344         }
345 
346         private final class MaintenanceActivityListener extends IMaintenanceActivityListener.Stub {
347             @Override
onMaintenanceActivityChanged(final boolean active)348             public void onMaintenanceActivityChanged(final boolean active) {
349                 DefaultDeviceIdleController.this.setMaintenanceActivity(active);
350             }
351         }
352     }
353 
logd(String msg)354     private void logd(String msg) {
355         if (DBG) {
356             Log.d(TAG, msg);
357         }
358     }
359 
readFromSettingsLocked(String... keys)360     private void readFromSettingsLocked(String... keys) {
361         for (String key : keys) {
362             switch (key) {
363                 case CarSettings.Global.KEY_GARAGE_MODE_ENABLED:
364                     mGarageModeEnabled =
365                             Settings.Global.getInt(mContext.getContentResolver(), key, 1) == 1;
366                     break;
367                 case CarSettings.Global.KEY_GARAGE_MODE_WAKE_UP_TIME:
368                     int time[] = CarApiUtil.decodeGarageTimeSetting(
369                             Settings.Global.getString(mContext.getContentResolver(),
370                                     CarSettings.Global.KEY_GARAGE_MODE_WAKE_UP_TIME));
371                     mWakeUpHour = time[0];
372                     mWakeUpMin = time[1];
373                     break;
374                 case CarSettings.Global.KEY_GARAGE_MODE_MAINTENANCE_WINDOW:
375                     mMaintenanceWindow = Settings.Global.getInt(
376                             mContext.getContentResolver(), key,
377                             CarSettings.DEFAULT_GARAGE_MODE_MAINTENANCE_WINDOW);
378                     break;
379                 default:
380                     Log.e(TAG, "Unknown setting key " + key);
381             }
382         }
383     }
384 
onSettingsChangedInternal(Uri uri)385     private void onSettingsChangedInternal(Uri uri) {
386         synchronized (this) {
387             logd("Content Observer onChange: " + uri);
388             if (uri.equals(GARAGE_MODE_ENABLED_URI)) {
389                 readFromSettingsLocked(CarSettings.Global.KEY_GARAGE_MODE_ENABLED);
390             } else if (uri.equals(GARAGE_MODE_WAKE_UP_TIME_URI)) {
391                 readFromSettingsLocked(CarSettings.Global.KEY_GARAGE_MODE_WAKE_UP_TIME);
392             } else if (uri.equals(GARAGE_MODE_MAINTENANCE_WINDOW_URI)) {
393                 readFromSettingsLocked(CarSettings.Global.KEY_GARAGE_MODE_MAINTENANCE_WINDOW);
394             }
395             logd(String.format(
396                     "onSettingsChanged %s. enabled: %s, wakeUpTime: %d:%d, windowSize: %d",
397                     uri, mGarageModeEnabled, mWakeUpHour, mWakeUpMin, mMaintenanceWindow));
398         }
399     }
400 }
401