blob: 801b3003e7532761b3ac2bef29d2b6f92f6c9838 [file] [log] [blame]
/*
* 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);
}
}
}
}