• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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