Home | History | Annotate | Download | only in doze
      1 /*
      2  * Copyright (C) 2014 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 
     17 package com.android.systemui.doze;
     18 
     19 import android.app.AlarmManager;
     20 import android.app.PendingIntent;
     21 import android.app.UiModeManager;
     22 import android.content.BroadcastReceiver;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.IntentFilter;
     26 import android.content.res.Configuration;
     27 import android.hardware.Sensor;
     28 import android.hardware.SensorEvent;
     29 import android.hardware.SensorEventListener;
     30 import android.hardware.SensorManager;
     31 import android.hardware.TriggerEvent;
     32 import android.hardware.TriggerEventListener;
     33 import android.media.AudioAttributes;
     34 import android.os.Handler;
     35 import android.os.PowerManager;
     36 import android.os.SystemClock;
     37 import android.os.Vibrator;
     38 import android.service.dreams.DreamService;
     39 import android.util.Log;
     40 import android.view.Display;
     41 
     42 import com.android.systemui.SystemUIApplication;
     43 import com.android.systemui.statusbar.phone.DozeParameters;
     44 import com.android.systemui.statusbar.phone.DozeParameters.PulseSchedule;
     45 
     46 import java.io.FileDescriptor;
     47 import java.io.PrintWriter;
     48 import java.util.Date;
     49 
     50 public class DozeService extends DreamService {
     51     private static final String TAG = "DozeService";
     52     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     53 
     54     private static final String ACTION_BASE = "com.android.systemui.doze";
     55     private static final String PULSE_ACTION = ACTION_BASE + ".pulse";
     56     private static final String NOTIFICATION_PULSE_ACTION = ACTION_BASE + ".notification_pulse";
     57     private static final String EXTRA_INSTANCE = "instance";
     58 
     59     private final String mTag = String.format(TAG + ".%08x", hashCode());
     60     private final Context mContext = this;
     61     private final DozeParameters mDozeParameters = new DozeParameters(mContext);
     62     private final Handler mHandler = new Handler();
     63 
     64     private DozeHost mHost;
     65     private SensorManager mSensors;
     66     private TriggerSensor mSigMotionSensor;
     67     private TriggerSensor mPickupSensor;
     68     private PowerManager mPowerManager;
     69     private PowerManager.WakeLock mWakeLock;
     70     private AlarmManager mAlarmManager;
     71     private UiModeManager mUiModeManager;
     72     private boolean mDreaming;
     73     private boolean mPulsing;
     74     private boolean mBroadcastReceiverRegistered;
     75     private boolean mDisplayStateSupported;
     76     private boolean mNotificationLightOn;
     77     private boolean mPowerSaveActive;
     78     private boolean mCarMode;
     79     private long mNotificationPulseTime;
     80     private int mScheduleResetsRemaining;
     81 
     82     public DozeService() {
     83         if (DEBUG) Log.d(mTag, "new DozeService()");
     84         setDebug(DEBUG);
     85     }
     86 
     87     @Override
     88     protected void dumpOnHandler(FileDescriptor fd, PrintWriter pw, String[] args) {
     89         super.dumpOnHandler(fd, pw, args);
     90         pw.print("  mDreaming: "); pw.println(mDreaming);
     91         pw.print("  mPulsing: "); pw.println(mPulsing);
     92         pw.print("  mWakeLock: held="); pw.println(mWakeLock.isHeld());
     93         pw.print("  mHost: "); pw.println(mHost);
     94         pw.print("  mBroadcastReceiverRegistered: "); pw.println(mBroadcastReceiverRegistered);
     95         pw.print("  mSigMotionSensor: "); pw.println(mSigMotionSensor);
     96         pw.print("  mPickupSensor:"); pw.println(mPickupSensor);
     97         pw.print("  mDisplayStateSupported: "); pw.println(mDisplayStateSupported);
     98         pw.print("  mNotificationLightOn: "); pw.println(mNotificationLightOn);
     99         pw.print("  mPowerSaveActive: "); pw.println(mPowerSaveActive);
    100         pw.print("  mCarMode: "); pw.println(mCarMode);
    101         pw.print("  mNotificationPulseTime: "); pw.println(mNotificationPulseTime);
    102         pw.print("  mScheduleResetsRemaining: "); pw.println(mScheduleResetsRemaining);
    103         mDozeParameters.dump(pw);
    104     }
    105 
    106     @Override
    107     public void onCreate() {
    108         if (DEBUG) Log.d(mTag, "onCreate");
    109         super.onCreate();
    110 
    111         if (getApplication() instanceof SystemUIApplication) {
    112             final SystemUIApplication app = (SystemUIApplication) getApplication();
    113             mHost = app.getComponent(DozeHost.class);
    114         }
    115         if (mHost == null) Log.w(TAG, "No doze service host found.");
    116 
    117         setWindowless(true);
    118 
    119         mSensors = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
    120         mSigMotionSensor = new TriggerSensor(Sensor.TYPE_SIGNIFICANT_MOTION,
    121                 mDozeParameters.getPulseOnSigMotion(), mDozeParameters.getVibrateOnSigMotion());
    122         mPickupSensor = new TriggerSensor(Sensor.TYPE_PICK_UP_GESTURE,
    123                 mDozeParameters.getPulseOnPickup(), mDozeParameters.getVibrateOnPickup());
    124         mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
    125         mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
    126         mWakeLock.setReferenceCounted(true);
    127         mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
    128         mDisplayStateSupported = mDozeParameters.getDisplayStateSupported();
    129         mUiModeManager = (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
    130         turnDisplayOff();
    131     }
    132 
    133     @Override
    134     public void onAttachedToWindow() {
    135         if (DEBUG) Log.d(mTag, "onAttachedToWindow");
    136         super.onAttachedToWindow();
    137     }
    138 
    139     @Override
    140     public void onDreamingStarted() {
    141         super.onDreamingStarted();
    142 
    143         if (mHost == null) {
    144             finish();
    145             return;
    146         }
    147 
    148         mPowerSaveActive = mHost.isPowerSaveActive();
    149         mCarMode = mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
    150         if (DEBUG) Log.d(mTag, "onDreamingStarted canDoze=" + canDoze() + " mPowerSaveActive="
    151                 + mPowerSaveActive + " mCarMode=" + mCarMode);
    152         if (mPowerSaveActive) {
    153             finishToSavePower();
    154             return;
    155         }
    156         if (mCarMode) {
    157             finishForCarMode();
    158             return;
    159         }
    160 
    161         mDreaming = true;
    162         listenForPulseSignals(true);
    163         rescheduleNotificationPulse(false /*predicate*/);  // cancel any pending pulse alarms
    164 
    165         // Ask the host to get things ready to start dozing.
    166         // Once ready, we call startDozing() at which point the CPU may suspend
    167         // and we will need to acquire a wakelock to do work.
    168         mHost.startDozing(new Runnable() {
    169             @Override
    170             public void run() {
    171                 if (mDreaming) {
    172                     startDozing();
    173 
    174                     // From this point until onDreamingStopped we will need to hold a
    175                     // wakelock whenever we are doing work.  Note that we never call
    176                     // stopDozing because can we just keep dozing until the bitter end.
    177                 }
    178             }
    179         });
    180     }
    181 
    182     @Override
    183     public void onDreamingStopped() {
    184         if (DEBUG) Log.d(mTag, "onDreamingStopped isDozing=" + isDozing());
    185         super.onDreamingStopped();
    186 
    187         if (mHost == null) {
    188             return;
    189         }
    190 
    191         mDreaming = false;
    192         listenForPulseSignals(false);
    193 
    194         // Tell the host that it's over.
    195         mHost.stopDozing();
    196     }
    197 
    198     private void requestPulse() {
    199         if (mHost != null && mDreaming && !mPulsing) {
    200             // Let the host know we want to pulse.  Wait for it to be ready, then
    201             // turn the screen on.  When finished, turn the screen off again.
    202             // Here we need a wakelock to stay awake until the pulse is finished.
    203             mWakeLock.acquire();
    204             mPulsing = true;
    205             final long start = SystemClock.uptimeMillis();
    206             new ProximityCheck() {
    207                 @Override
    208                 public void onProximityResult(int result) {
    209                     // avoid pulsing in pockets
    210                     final boolean isNear = result == RESULT_NEAR;
    211                     DozeLog.traceProximityResult(isNear, SystemClock.uptimeMillis() - start);
    212                     if (isNear) {
    213                         mPulsing = false;
    214                         mWakeLock.release();
    215                         return;
    216                     }
    217 
    218                     // not in-pocket, continue pulsing
    219                     mHost.pulseWhileDozing(new DozeHost.PulseCallback() {
    220                         @Override
    221                         public void onPulseStarted() {
    222                             if (mPulsing && mDreaming) {
    223                                 turnDisplayOn();
    224                             }
    225                         }
    226 
    227                         @Override
    228                         public void onPulseFinished() {
    229                             if (mPulsing && mDreaming) {
    230                                 mPulsing = false;
    231                                 turnDisplayOff();
    232                             }
    233                             mWakeLock.release(); // needs to be unconditional to balance acquire
    234                         }
    235                     });
    236                 }
    237             }.check();
    238         }
    239     }
    240 
    241     private void turnDisplayOff() {
    242         if (DEBUG) Log.d(mTag, "Display off");
    243         setDozeScreenState(Display.STATE_OFF);
    244     }
    245 
    246     private void turnDisplayOn() {
    247         if (DEBUG) Log.d(mTag, "Display on");
    248         setDozeScreenState(mDisplayStateSupported ? Display.STATE_DOZE : Display.STATE_ON);
    249     }
    250 
    251     private void finishToSavePower() {
    252         Log.w(mTag, "Exiting ambient mode due to low power battery saver");
    253         finish();
    254     }
    255 
    256     private void finishForCarMode() {
    257         Log.w(mTag, "Exiting ambient mode, not allowed in car mode");
    258         finish();
    259     }
    260 
    261     private void listenForPulseSignals(boolean listen) {
    262         if (DEBUG) Log.d(mTag, "listenForPulseSignals: " + listen);
    263         mSigMotionSensor.setListening(listen);
    264         mPickupSensor.setListening(listen);
    265         listenForBroadcasts(listen);
    266         listenForNotifications(listen);
    267     }
    268 
    269     private void listenForBroadcasts(boolean listen) {
    270         if (listen) {
    271             final IntentFilter filter = new IntentFilter(PULSE_ACTION);
    272             filter.addAction(NOTIFICATION_PULSE_ACTION);
    273             filter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE);
    274             mContext.registerReceiver(mBroadcastReceiver, filter);
    275             mBroadcastReceiverRegistered = true;
    276         } else {
    277             if (mBroadcastReceiverRegistered) {
    278                 mContext.unregisterReceiver(mBroadcastReceiver);
    279             }
    280             mBroadcastReceiverRegistered = false;
    281         }
    282     }
    283 
    284     private void listenForNotifications(boolean listen) {
    285         if (listen) {
    286             resetNotificationResets();
    287             mHost.addCallback(mHostCallback);
    288         } else {
    289             mHost.removeCallback(mHostCallback);
    290         }
    291     }
    292 
    293     private void resetNotificationResets() {
    294         if (DEBUG) Log.d(mTag, "resetNotificationResets");
    295         mScheduleResetsRemaining = mDozeParameters.getPulseScheduleResets();
    296     }
    297 
    298     private void updateNotificationPulse() {
    299         if (DEBUG) Log.d(mTag, "updateNotificationPulse");
    300         if (!mDozeParameters.getPulseOnNotifications()) return;
    301         if (mScheduleResetsRemaining <= 0) {
    302             if (DEBUG) Log.d(mTag, "No more schedule resets remaining");
    303             return;
    304         }
    305         final long now = System.currentTimeMillis();
    306         if ((now - mNotificationPulseTime) < mDozeParameters.getPulseDuration()) {
    307             if (DEBUG) Log.d(mTag, "Recently updated, not resetting schedule");
    308             return;
    309         }
    310         mScheduleResetsRemaining--;
    311         if (DEBUG) Log.d(mTag, "mScheduleResetsRemaining = " + mScheduleResetsRemaining);
    312         mNotificationPulseTime = now;
    313         rescheduleNotificationPulse(true /*predicate*/);
    314     }
    315 
    316     private PendingIntent notificationPulseIntent(long instance) {
    317         return PendingIntent.getBroadcast(mContext, 0,
    318                 new Intent(NOTIFICATION_PULSE_ACTION)
    319                         .setPackage(getPackageName())
    320                         .putExtra(EXTRA_INSTANCE, instance)
    321                         .setFlags(Intent.FLAG_RECEIVER_FOREGROUND),
    322                 PendingIntent.FLAG_UPDATE_CURRENT);
    323     }
    324 
    325     private void rescheduleNotificationPulse(boolean predicate) {
    326         if (DEBUG) Log.d(mTag, "rescheduleNotificationPulse predicate=" + predicate);
    327         final PendingIntent notificationPulseIntent = notificationPulseIntent(0);
    328         mAlarmManager.cancel(notificationPulseIntent);
    329         if (!predicate) {
    330             if (DEBUG) Log.d(mTag, "  don't reschedule: predicate is false");
    331             return;
    332         }
    333         final PulseSchedule schedule = mDozeParameters.getPulseSchedule();
    334         if (schedule == null) {
    335             if (DEBUG) Log.d(mTag, "  don't reschedule: schedule is null");
    336             return;
    337         }
    338         final long now = System.currentTimeMillis();
    339         final long time = schedule.getNextTime(now, mNotificationPulseTime);
    340         if (time <= 0) {
    341             if (DEBUG) Log.d(mTag, "  don't reschedule: time is " + time);
    342             return;
    343         }
    344         final long delta = time - now;
    345         if (delta <= 0) {
    346             if (DEBUG) Log.d(mTag, "  don't reschedule: delta is " + delta);
    347             return;
    348         }
    349         final long instance = time - mNotificationPulseTime;
    350         if (DEBUG) Log.d(mTag, "Scheduling pulse " + instance + " in " + delta + "ms for "
    351                 + new Date(time));
    352         mAlarmManager.setExact(AlarmManager.RTC_WAKEUP, time, notificationPulseIntent(instance));
    353     }
    354 
    355     private static String triggerEventToString(TriggerEvent event) {
    356         if (event == null) return null;
    357         final StringBuilder sb = new StringBuilder("TriggerEvent[")
    358                 .append(event.timestamp).append(',')
    359                 .append(event.sensor.getName());
    360         if (event.values != null) {
    361             for (int i = 0; i < event.values.length; i++) {
    362                 sb.append(',').append(event.values[i]);
    363             }
    364         }
    365         return sb.append(']').toString();
    366     }
    367 
    368     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
    369         @Override
    370         public void onReceive(Context context, Intent intent) {
    371             if (PULSE_ACTION.equals(intent.getAction())) {
    372                 if (DEBUG) Log.d(mTag, "Received pulse intent");
    373                 requestPulse();
    374             }
    375             if (NOTIFICATION_PULSE_ACTION.equals(intent.getAction())) {
    376                 final long instance = intent.getLongExtra(EXTRA_INSTANCE, -1);
    377                 if (DEBUG) Log.d(mTag, "Received notification pulse intent instance=" + instance);
    378                 DozeLog.traceNotificationPulse(instance);
    379                 requestPulse();
    380                 rescheduleNotificationPulse(mNotificationLightOn);
    381             }
    382             if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(intent.getAction())) {
    383                 mCarMode = true;
    384                 if (mCarMode && mDreaming) {
    385                     finishForCarMode();
    386                 }
    387             }
    388         }
    389     };
    390 
    391     private final DozeHost.Callback mHostCallback = new DozeHost.Callback() {
    392         @Override
    393         public void onNewNotifications() {
    394             if (DEBUG) Log.d(mTag, "onNewNotifications");
    395             // noop for now
    396         }
    397 
    398         @Override
    399         public void onBuzzBeepBlinked() {
    400             if (DEBUG) Log.d(mTag, "onBuzzBeepBlinked");
    401             updateNotificationPulse();
    402         }
    403 
    404         @Override
    405         public void onNotificationLight(boolean on) {
    406             if (DEBUG) Log.d(mTag, "onNotificationLight on=" + on);
    407             if (mNotificationLightOn == on) return;
    408             mNotificationLightOn = on;
    409             if (mNotificationLightOn) {
    410                 updateNotificationPulse();
    411             }
    412         }
    413 
    414         @Override
    415         public void onPowerSaveChanged(boolean active) {
    416             mPowerSaveActive = active;
    417             if (mPowerSaveActive && mDreaming) {
    418                 finishToSavePower();
    419             }
    420         }
    421     };
    422 
    423     private class TriggerSensor extends TriggerEventListener {
    424         private final Sensor mSensor;
    425         private final boolean mConfigured;
    426         private final boolean mDebugVibrate;
    427 
    428         private boolean mRequested;
    429         private boolean mRegistered;
    430         private boolean mDisabled;
    431 
    432         public TriggerSensor(int type, boolean configured, boolean debugVibrate) {
    433             mSensor = mSensors.getDefaultSensor(type);
    434             mConfigured = configured;
    435             mDebugVibrate = debugVibrate;
    436         }
    437 
    438         public void setListening(boolean listen) {
    439             if (mRequested == listen) return;
    440             mRequested = listen;
    441             updateListener();
    442         }
    443 
    444         public void setDisabled(boolean disabled) {
    445             if (mDisabled == disabled) return;
    446             mDisabled = disabled;
    447             updateListener();
    448         }
    449 
    450         private void updateListener() {
    451             if (!mConfigured || mSensor == null) return;
    452             if (mRequested && !mDisabled) {
    453                 mRegistered = mSensors.requestTriggerSensor(this, mSensor);
    454             } else if (mRegistered) {
    455                 mSensors.cancelTriggerSensor(this, mSensor);
    456                 mRegistered = false;
    457             }
    458         }
    459 
    460         @Override
    461         public String toString() {
    462             return new StringBuilder("{mRegistered=").append(mRegistered)
    463                     .append(", mRequested=").append(mRequested)
    464                     .append(", mDisabled=").append(mDisabled)
    465                     .append(", mConfigured=").append(mConfigured)
    466                     .append(", mDebugVibrate=").append(mDebugVibrate)
    467                     .append(", mSensor=").append(mSensor).append("}").toString();
    468         }
    469 
    470         @Override
    471         public void onTrigger(TriggerEvent event) {
    472             mWakeLock.acquire();
    473             try {
    474                 if (DEBUG) Log.d(mTag, "onTrigger: " + triggerEventToString(event));
    475                 if (mDebugVibrate) {
    476                     final Vibrator v = (Vibrator) mContext.getSystemService(
    477                             Context.VIBRATOR_SERVICE);
    478                     if (v != null) {
    479                         v.vibrate(1000, new AudioAttributes.Builder()
    480                                 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
    481                                 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build());
    482                     }
    483                 }
    484 
    485                 requestPulse();
    486                 setListening(true);  // reregister, this sensor only fires once
    487 
    488                 // reset the notification pulse schedule, but only if we think we were not triggered
    489                 // by a notification-related vibration
    490                 final long timeSinceNotification = System.currentTimeMillis()
    491                         - mNotificationPulseTime;
    492                 final boolean withinVibrationThreshold =
    493                         timeSinceNotification < mDozeParameters.getPickupVibrationThreshold();
    494                 if (withinVibrationThreshold) {
    495                    if (DEBUG) Log.d(mTag, "Not resetting schedule, recent notification");
    496                 } else {
    497                     resetNotificationResets();
    498                 }
    499                 if (mSensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
    500                     DozeLog.tracePickupPulse(withinVibrationThreshold);
    501                 }
    502             } finally {
    503                 mWakeLock.release();
    504             }
    505         }
    506     }
    507 
    508     private abstract class ProximityCheck implements SensorEventListener, Runnable {
    509         private static final int TIMEOUT_DELAY_MS = 500;
    510 
    511         protected static final int RESULT_UNKNOWN = 0;
    512         protected static final int RESULT_NEAR = 1;
    513         protected static final int RESULT_FAR = 2;
    514 
    515         private final String mTag = DozeService.this.mTag + ".ProximityCheck";
    516 
    517         private boolean mRegistered;
    518         private boolean mFinished;
    519         private float mMaxRange;
    520 
    521         abstract public void onProximityResult(int result);
    522 
    523         public void check() {
    524             if (mFinished || mRegistered) return;
    525             final Sensor sensor = mSensors.getDefaultSensor(Sensor.TYPE_PROXIMITY);
    526             if (sensor == null) {
    527                 if (DEBUG) Log.d(mTag, "No sensor found");
    528                 finishWithResult(RESULT_UNKNOWN);
    529                 return;
    530             }
    531             // the pickup sensor interferes with the prox event, disable it until we have a result
    532             mPickupSensor.setDisabled(true);
    533 
    534             mMaxRange = sensor.getMaximumRange();
    535             mSensors.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, 0, mHandler);
    536             mHandler.postDelayed(this, TIMEOUT_DELAY_MS);
    537             mRegistered = true;
    538         }
    539 
    540         @Override
    541         public void onSensorChanged(SensorEvent event) {
    542             if (event.values.length == 0) {
    543                 if (DEBUG) Log.d(mTag, "Event has no values!");
    544                 finishWithResult(RESULT_UNKNOWN);
    545             } else {
    546                 if (DEBUG) Log.d(mTag, "Event: value=" + event.values[0] + " max=" + mMaxRange);
    547                 final boolean isNear = event.values[0] < mMaxRange;
    548                 finishWithResult(isNear ? RESULT_NEAR : RESULT_FAR);
    549             }
    550         }
    551 
    552         @Override
    553         public void run() {
    554             if (DEBUG) Log.d(mTag, "No event received before timeout");
    555             finishWithResult(RESULT_UNKNOWN);
    556         }
    557 
    558         private void finishWithResult(int result) {
    559             if (mFinished) return;
    560             if (mRegistered) {
    561                 mHandler.removeCallbacks(this);
    562                 mSensors.unregisterListener(this);
    563                 // we're done - reenable the pickup sensor
    564                 mPickupSensor.setDisabled(false);
    565                 mRegistered = false;
    566             }
    567             onProximityResult(result);
    568             mFinished = true;
    569         }
    570 
    571         @Override
    572         public void onAccuracyChanged(Sensor sensor, int accuracy) {
    573             // noop
    574         }
    575     }
    576 }
    577