| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.car; |
| |
| import android.content.Context; |
| import android.content.SharedPreferences; |
| import android.os.Handler; |
| import android.os.IDeviceIdleController; |
| import android.os.IMaintenanceActivityListener; |
| import android.os.Message; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.PrintWriter; |
| import java.util.Calendar; |
| |
| /** |
| * Controls car garage mode. |
| * |
| * Car garage mode is a time window for the car to do maintenance work when the car is not in use. |
| * The {@link com.android.car.GarageModeService} interacts with {@link com.android.car.CarPowerManagementService} |
| * to start and end garage mode. A {@link com.android.car.GarageModeService.GarageModePolicy} defines |
| * when the garage mode should start and how long it should last. |
| */ |
| public class GarageModeService implements CarServiceBase, |
| CarPowerManagementService.PowerEventProcessingHandler, |
| CarPowerManagementService.PowerServiceEventListener, |
| DeviceIdleControllerWrapper.DeviceMaintenanceActivityListener { |
| private static String TAG = "GarageModeService"; |
| private static String GARAGE_MODE_PREFERENCE_FILE = "com.android.car.PREFERENCE_FILE_KEY"; |
| private static String GARAGE_MODE_INDEX = "garage_mode_index"; |
| |
| private static final int MSG_EXIT_GARAGE_MODE_EARLY = 0; |
| private static final int MSG_WRITE_TO_PREF = 1; |
| |
| // TODO: move this to garage mode policy too. |
| @VisibleForTesting |
| protected static final int MAINTENANCE_WINDOW = 5 * 60 * 1000; // 5 minutes |
| // wait for 10 seconds to allow maintenance activities to start (e.g., connecting to wifi). |
| protected static final int MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD = 10 * 1000; |
| |
| private final CarPowerManagementService mPowerManagementService; |
| protected final Context mContext; |
| |
| @VisibleForTesting |
| @GuardedBy("this") |
| protected boolean mInGarageMode; |
| @VisibleForTesting |
| @GuardedBy("this") |
| protected boolean mMaintenanceActive; |
| @VisibleForTesting |
| @GuardedBy("this") |
| protected int mGarageModeIndex; |
| |
| private final Object mPolicyLock = new Object(); |
| @GuardedBy("mPolicyLock") |
| private GarageModePolicy mPolicy; |
| |
| private SharedPreferences mSharedPreferences; |
| |
| private DeviceIdleControllerWrapper mDeviceIdleController; |
| private GarageModeHandler mHandler = new GarageModeHandler(); |
| |
| private class GarageModeHandler extends Handler { |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_EXIT_GARAGE_MODE_EARLY: |
| mPowerManagementService.notifyPowerEventProcessingCompletion( |
| GarageModeService.this); |
| break; |
| case MSG_WRITE_TO_PREF: |
| writeToPref(msg.arg1); |
| break; |
| } |
| } |
| } |
| |
| public GarageModeService(Context context, CarPowerManagementService powerManagementService) { |
| this(context, powerManagementService, null); |
| } |
| |
| @VisibleForTesting |
| protected GarageModeService(Context context, CarPowerManagementService powerManagementService, |
| DeviceIdleControllerWrapper deviceIdleController) { |
| mContext = context; |
| mPowerManagementService = powerManagementService; |
| if (deviceIdleController == null) { |
| mDeviceIdleController = new DefaultDeviceIdleController(); |
| } else { |
| mDeviceIdleController = deviceIdleController; |
| } |
| } |
| |
| @Override |
| public void init() { |
| Log.d(TAG, "init GarageMode"); |
| mSharedPreferences = mContext.getSharedPreferences( |
| GARAGE_MODE_PREFERENCE_FILE, Context.MODE_PRIVATE); |
| synchronized (mPolicyLock) { |
| readPolicyLocked(); |
| } |
| |
| final int index = mSharedPreferences.getInt(GARAGE_MODE_INDEX, 0); |
| synchronized (this) { |
| mMaintenanceActive = mDeviceIdleController.startTracking(this); |
| mGarageModeIndex = index; |
| } |
| mPowerManagementService.registerPowerEventProcessingHandler(this); |
| } |
| |
| @Override |
| public void release() { |
| Log.d(TAG, "release GarageModeService"); |
| mDeviceIdleController.stopTracking(); |
| } |
| |
| @Override |
| public void dump(PrintWriter writer) { |
| writer.println("mGarageModeIndex: " + mGarageModeIndex); |
| writer.println("inGarageMode? " + mInGarageMode); |
| } |
| |
| @Override |
| public long onPrepareShutdown(boolean shuttingDown) { |
| // this is the beginning of each garage mode. |
| synchronized (this) { |
| Log.d(TAG, "onPrePowerEvent " + shuttingDown); |
| mInGarageMode = true; |
| mGarageModeIndex++; |
| mHandler.removeMessages(MSG_EXIT_GARAGE_MODE_EARLY); |
| if (!mMaintenanceActive) { |
| mHandler.sendMessageDelayed( |
| mHandler.obtainMessage(MSG_EXIT_GARAGE_MODE_EARLY), |
| MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD); |
| } |
| // We always reserve the maintenance window first. If later, we found no |
| // maintenance work active, we will exit garage mode early after |
| // MAINTENANCE_ACTIVITY_START_GRACE_PERIOUD |
| return MAINTENANCE_WINDOW; |
| } |
| } |
| |
| @Override |
| public void onPowerOn(boolean displayOn) { |
| synchronized (this) { |
| Log.d(TAG, "onPowerOn: " + displayOn); |
| if (displayOn) { |
| // the car is use now. reset the garage mode counter. |
| mGarageModeIndex = 0; |
| } |
| } |
| } |
| |
| @Override |
| public int getWakeupTime() { |
| final int index; |
| synchronized (this) { |
| index = mGarageModeIndex; |
| } |
| synchronized (mPolicyLock) { |
| return mPolicy.getNextWakeUpTime(index); |
| } |
| } |
| |
| @Override |
| public void onSleepExit() { |
| // ignored |
| } |
| |
| @Override |
| public void onSleepEntry() { |
| synchronized (this) { |
| mInGarageMode = false; |
| } |
| } |
| |
| @Override |
| public void onShutdown() { |
| synchronized (this) { |
| mHandler.sendMessage( |
| mHandler.obtainMessage(MSG_WRITE_TO_PREF, mGarageModeIndex, 0)); |
| } |
| } |
| |
| private void readPolicyLocked() { |
| Log.d(TAG, "readPolicyLocked"); |
| // TODO: define a xml schema for garage mode policy and read it from system dir. |
| mPolicy = new DefaultGarageModePolicy(); |
| } |
| |
| private void writeToPref(int index) { |
| SharedPreferences.Editor editor = mSharedPreferences.edit(); |
| editor.putInt(GARAGE_MODE_INDEX, index); |
| editor.commit(); |
| } |
| |
| @Override |
| public void onMaintenanceActivityChanged(boolean active) { |
| boolean shouldReportCompletion = false; |
| synchronized (this) { |
| Log.d(TAG, "onMaintenanceActivityChanged: " + active); |
| mMaintenanceActive = active; |
| if (!mInGarageMode) { |
| return; |
| } |
| |
| if (!active) { |
| shouldReportCompletion = true; |
| mInGarageMode = false; |
| } else { |
| // we are in garage mode, and maintenance work has just begun. |
| mHandler.removeMessages(MSG_EXIT_GARAGE_MODE_EARLY); |
| } |
| } |
| if (shouldReportCompletion) { |
| // we are in garage mode, and maintenance work has finished. |
| mPowerManagementService.notifyPowerEventProcessingCompletion(this); |
| } |
| } |
| |
| public abstract static class GarageModePolicy { |
| abstract public int getNextWakeUpTime(int index); |
| /** |
| * Returns number of seconds between now to 1am {@param numDays} days later. |
| */ |
| public static int nextWakeUpSeconds(int numDays) { |
| // TODO: Should select a random time within a window to avoid all cars update at the |
| // same time. |
| Calendar next = Calendar.getInstance(); |
| next.add(Calendar.DATE, numDays); |
| next.set(Calendar.HOUR_OF_DAY, 1); |
| next.set(Calendar.MINUTE, 0); |
| next.set(Calendar.SECOND, 0); |
| |
| Calendar now = Calendar.getInstance(); |
| return (next.get(Calendar.MILLISECOND) - now.get(Calendar.MILLISECOND)) / 1000; |
| } |
| } |
| |
| /** |
| * Default garage mode policy. |
| * |
| * The first wake up time is set to be 1am the next day. And it keeps waking up every day for a |
| * week. After that, wake up every 7 days for a month, and wake up every 30 days thereafter. |
| */ |
| private static class DefaultGarageModePolicy extends GarageModePolicy { |
| private static final int COL_INDEX = 0; |
| private static final int COL_WAKEUP_TIME = 1; |
| |
| private static final int[][] WAKE_UP_TIME = new int[][] { |
| {7 /*index <= 7*/, 1 /* wake up the next day */}, |
| {11 /* 7 < index <= 11 */, 7 /* wake up the next week */}, |
| {Integer.MAX_VALUE /* index > 11 */, 30 /* wake up the next month */} |
| }; |
| |
| @Override |
| public int getNextWakeUpTime(int index) { |
| for (int i = 0; i < WAKE_UP_TIME.length; i++) { |
| if (index <= WAKE_UP_TIME[i][COL_INDEX]) { |
| return nextWakeUpSeconds(WAKE_UP_TIME[i][COL_WAKEUP_TIME]); |
| } |
| } |
| |
| Log.w(TAG, "Integer.MAX number of wake ups... How long have we been sleeping? "); |
| return 0; |
| } |
| } |
| |
| private static class DefaultDeviceIdleController extends DeviceIdleControllerWrapper { |
| private IDeviceIdleController mDeviceIdleController; |
| private MaintenanceActivityListener mMaintenanceActivityListener |
| = new MaintenanceActivityListener(); |
| |
| @Override |
| public boolean startLocked() { |
| mDeviceIdleController = IDeviceIdleController.Stub.asInterface( |
| ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); |
| boolean active = false; |
| try { |
| active = mDeviceIdleController |
| .registerMaintenanceActivityListener(mMaintenanceActivityListener); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Unable to register listener with DeviceIdleController", e); |
| } |
| return active; |
| } |
| |
| @Override |
| public void stopTracking() { |
| try { |
| if (mDeviceIdleController != null) { |
| mDeviceIdleController.unregisterMaintenanceActivityListener( |
| mMaintenanceActivityListener); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Fail to unregister listener.", e); |
| } |
| } |
| |
| private final class MaintenanceActivityListener extends IMaintenanceActivityListener.Stub { |
| @Override |
| public void onMaintenanceActivityChanged(final boolean active) { |
| DefaultDeviceIdleController.this.setMaintenanceActivity(active); |
| } |
| } |
| } |
| } |