2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.systemui.doze;
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;
42 import com.android.systemui.SystemUIApplication;
43 import com.android.systemui.statusbar.phone.DozeParameters;
44 import com.android.systemui.statusbar.phone.DozeParameters.PulseSchedule;
46 import java.io.FileDescriptor;
47 import java.io.PrintWriter;
48 import java.util.Date;
50 public class DozeService extends DreamService {
51 private static final String TAG = "DozeService";
52 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
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";
60 * Earliest time we pulse due to a notification light after the service started.
62 * <p>Incoming notification light events during the blackout period are
63 * delayed to the earliest time defined by this constant.</p>
65 * <p>This delay avoids a pulse immediately after screen off, at which
66 * point the notification light is re-enabled again by NoMan.</p>
68 private static final int EARLIEST_LIGHT_PULSE_AFTER_START_MS = 10 * 1000;
70 private final String mTag = String.format(TAG + ".%08x", hashCode());
71 private final Context mContext = this;
72 private final DozeParameters mDozeParameters = new DozeParameters(mContext);
73 private final Handler mHandler = new Handler();
75 private DozeHost mHost;
76 private SensorManager mSensors;
77 private TriggerSensor mSigMotionSensor;
78 private TriggerSensor mPickupSensor;
79 private PowerManager mPowerManager;
80 private PowerManager.WakeLock mWakeLock;
81 private AlarmManager mAlarmManager;
82 private UiModeManager mUiModeManager;
83 private boolean mDreaming;
84 private boolean mPulsing;
85 private boolean mBroadcastReceiverRegistered;
86 private boolean mDisplayStateSupported;
87 private boolean mNotificationLightOn;
88 private boolean mPowerSaveActive;
89 private boolean mCarMode;
90 private long mNotificationPulseTime;
91 private long mLastScheduleResetTime;
92 private long mEarliestPulseDueToLight;
93 private int mScheduleResetsRemaining;
95 public DozeService() {
96 if (DEBUG) Log.d(mTag, "new DozeService()");
101 protected void dumpOnHandler(FileDescriptor fd, PrintWriter pw, String[] args) {
102 super.dumpOnHandler(fd, pw, args);
103 pw.print(" mDreaming: "); pw.println(mDreaming);
104 pw.print(" mPulsing: "); pw.println(mPulsing);
105 pw.print(" mWakeLock: held="); pw.println(mWakeLock.isHeld());
106 pw.print(" mHost: "); pw.println(mHost);
107 pw.print(" mBroadcastReceiverRegistered: "); pw.println(mBroadcastReceiverRegistered);
108 pw.print(" mSigMotionSensor: "); pw.println(mSigMotionSensor);
109 pw.print(" mPickupSensor:"); pw.println(mPickupSensor);
110 pw.print(" mDisplayStateSupported: "); pw.println(mDisplayStateSupported);
111 pw.print(" mNotificationLightOn: "); pw.println(mNotificationLightOn);
112 pw.print(" mPowerSaveActive: "); pw.println(mPowerSaveActive);
113 pw.print(" mCarMode: "); pw.println(mCarMode);
114 pw.print(" mNotificationPulseTime: "); pw.println(mNotificationPulseTime);
115 pw.print(" mScheduleResetsRemaining: "); pw.println(mScheduleResetsRemaining);
116 mDozeParameters.dump(pw);
120 public void onCreate() {
121 if (DEBUG) Log.d(mTag, "onCreate");
124 if (getApplication() instanceof SystemUIApplication) {
125 final SystemUIApplication app = (SystemUIApplication) getApplication();
126 mHost = app.getComponent(DozeHost.class);
128 if (mHost == null) Log.w(TAG, "No doze service host found.");
132 mSensors = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
133 mSigMotionSensor = new TriggerSensor(Sensor.TYPE_SIGNIFICANT_MOTION,
134 mDozeParameters.getPulseOnSigMotion(), mDozeParameters.getVibrateOnSigMotion(),
135 DozeLog.PULSE_REASON_SENSOR_SIGMOTION);
136 mPickupSensor = new TriggerSensor(Sensor.TYPE_PICK_UP_GESTURE,
137 mDozeParameters.getPulseOnPickup(), mDozeParameters.getVibrateOnPickup(),
138 DozeLog.PULSE_REASON_SENSOR_PICKUP);
139 mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
140 mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
141 mWakeLock.setReferenceCounted(true);
142 mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
143 mDisplayStateSupported = mDozeParameters.getDisplayStateSupported();
144 mUiModeManager = (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
149 public void onAttachedToWindow() {
150 if (DEBUG) Log.d(mTag, "onAttachedToWindow");
151 super.onAttachedToWindow();
155 public void onDreamingStarted() {
156 super.onDreamingStarted();
163 mPowerSaveActive = mHost.isPowerSaveActive();
164 mCarMode = mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
165 if (DEBUG) Log.d(mTag, "onDreamingStarted canDoze=" + canDoze() + " mPowerSaveActive="
166 + mPowerSaveActive + " mCarMode=" + mCarMode);
167 if (mPowerSaveActive) {
177 rescheduleNotificationPulse(false /*predicate*/); // cancel any pending pulse alarms
178 mEarliestPulseDueToLight = System.currentTimeMillis() + EARLIEST_LIGHT_PULSE_AFTER_START_MS;
179 listenForPulseSignals(true);
181 // Ask the host to get things ready to start dozing.
182 // Once ready, we call startDozing() at which point the CPU may suspend
183 // and we will need to acquire a wakelock to do work.
184 mHost.startDozing(new Runnable() {
190 // From this point until onDreamingStopped we will need to hold a
191 // wakelock whenever we are doing work. Note that we never call
192 // stopDozing because can we just keep dozing until the bitter end.
199 public void onDreamingStopped() {
200 if (DEBUG) Log.d(mTag, "onDreamingStopped isDozing=" + isDozing());
201 super.onDreamingStopped();
208 listenForPulseSignals(false);
210 // Tell the host that it's over.
214 private void requestPulse(final int reason) {
215 if (mHost != null && mDreaming && !mPulsing) {
216 // Let the host know we want to pulse. Wait for it to be ready, then
217 // turn the screen on. When finished, turn the screen off again.
218 // Here we need a wakelock to stay awake until the pulse is finished.
221 if (!mDozeParameters.getProxCheckBeforePulse()) {
222 // skip proximity check
223 continuePulsing(reason);
226 final long start = SystemClock.uptimeMillis();
227 final boolean nonBlocking = reason == DozeLog.PULSE_REASON_SENSOR_PICKUP
228 && mDozeParameters.getPickupPerformsProxCheck();
230 // proximity check is only done to capture statistics, continue pulsing
231 continuePulsing(reason);
233 // perform a proximity check
234 new ProximityCheck() {
236 public void onProximityResult(int result) {
237 final boolean isNear = result == RESULT_NEAR;
238 final long end = SystemClock.uptimeMillis();
239 DozeLog.traceProximityResult(mContext, isNear, end - start, reason);
241 // we already continued
244 // avoid pulsing in pockets
251 // not in-pocket, continue pulsing
252 continuePulsing(reason);
258 private void continuePulsing(int reason) {
259 if (mHost.isPulsingBlocked()) {
264 mHost.pulseWhileDozing(new DozeHost.PulseCallback() {
266 public void onPulseStarted() {
267 if (mPulsing && mDreaming) {
273 public void onPulseFinished() {
274 if (mPulsing && mDreaming) {
278 mWakeLock.release(); // needs to be unconditional to balance acquire
283 private void turnDisplayOff() {
284 if (DEBUG) Log.d(mTag, "Display off");
285 setDozeScreenState(Display.STATE_OFF);
288 private void turnDisplayOn() {
289 if (DEBUG) Log.d(mTag, "Display on");
290 setDozeScreenState(mDisplayStateSupported ? Display.STATE_DOZE : Display.STATE_ON);
293 private void finishToSavePower() {
294 Log.w(mTag, "Exiting ambient mode due to low power battery saver");
298 private void finishForCarMode() {
299 Log.w(mTag, "Exiting ambient mode, not allowed in car mode");
303 private void listenForPulseSignals(boolean listen) {
304 if (DEBUG) Log.d(mTag, "listenForPulseSignals: " + listen);
305 mSigMotionSensor.setListening(listen);
306 mPickupSensor.setListening(listen);
307 listenForBroadcasts(listen);
308 listenForNotifications(listen);
311 private void listenForBroadcasts(boolean listen) {
313 final IntentFilter filter = new IntentFilter(PULSE_ACTION);
314 filter.addAction(NOTIFICATION_PULSE_ACTION);
315 filter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE);
316 mContext.registerReceiver(mBroadcastReceiver, filter);
317 mBroadcastReceiverRegistered = true;
319 if (mBroadcastReceiverRegistered) {
320 mContext.unregisterReceiver(mBroadcastReceiver);
322 mBroadcastReceiverRegistered = false;
326 private void listenForNotifications(boolean listen) {
328 resetNotificationResets();
329 mHost.addCallback(mHostCallback);
331 // Continue to pulse for existing LEDs.
332 mNotificationLightOn = mHost.isNotificationLightOn();
333 if (mNotificationLightOn) {
334 updateNotificationPulseDueToLight();
337 mHost.removeCallback(mHostCallback);
341 private void resetNotificationResets() {
342 if (DEBUG) Log.d(mTag, "resetNotificationResets");
343 mScheduleResetsRemaining = mDozeParameters.getPulseScheduleResets();
346 private void updateNotificationPulseDueToLight() {
347 long timeMs = System.currentTimeMillis();
348 timeMs = Math.max(timeMs, mEarliestPulseDueToLight);
349 updateNotificationPulse(timeMs);
352 private void updateNotificationPulse(long notificationTimeMs) {
353 if (DEBUG) Log.d(mTag, "updateNotificationPulse notificationTimeMs=" + notificationTimeMs);
354 if (!mDozeParameters.getPulseOnNotifications()) return;
355 if (mScheduleResetsRemaining <= 0) {
356 if (DEBUG) Log.d(mTag, "No more schedule resets remaining");
359 final long pulseDuration = mDozeParameters.getPulseDuration(false /*pickup*/);
360 boolean pulseImmediately = System.currentTimeMillis() >= notificationTimeMs;
361 if ((notificationTimeMs - mLastScheduleResetTime) >= pulseDuration) {
362 mScheduleResetsRemaining--;
363 mLastScheduleResetTime = notificationTimeMs;
364 } else if (!pulseImmediately){
365 if (DEBUG) Log.d(mTag, "Recently updated, not resetting schedule");
368 if (DEBUG) Log.d(mTag, "mScheduleResetsRemaining = " + mScheduleResetsRemaining);
369 mNotificationPulseTime = notificationTimeMs;
370 if (pulseImmediately) {
371 DozeLog.traceNotificationPulse(0);
372 requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
374 // schedule the rest of the pulses
375 rescheduleNotificationPulse(true /*predicate*/);
378 private PendingIntent notificationPulseIntent(long instance) {
379 return PendingIntent.getBroadcast(mContext, 0,
380 new Intent(NOTIFICATION_PULSE_ACTION)
381 .setPackage(getPackageName())
382 .putExtra(EXTRA_INSTANCE, instance)
383 .setFlags(Intent.FLAG_RECEIVER_FOREGROUND),
384 PendingIntent.FLAG_UPDATE_CURRENT);
387 private void rescheduleNotificationPulse(boolean predicate) {
388 if (DEBUG) Log.d(mTag, "rescheduleNotificationPulse predicate=" + predicate);
389 final PendingIntent notificationPulseIntent = notificationPulseIntent(0);
390 mAlarmManager.cancel(notificationPulseIntent);
392 if (DEBUG) Log.d(mTag, " don't reschedule: predicate is false");
395 final PulseSchedule schedule = mDozeParameters.getPulseSchedule();
396 if (schedule == null) {
397 if (DEBUG) Log.d(mTag, " don't reschedule: schedule is null");
400 final long now = System.currentTimeMillis();
401 final long time = schedule.getNextTime(now, mNotificationPulseTime);
403 if (DEBUG) Log.d(mTag, " don't reschedule: time is " + time);
406 final long delta = time - now;
408 if (DEBUG) Log.d(mTag, " don't reschedule: delta is " + delta);
411 final long instance = time - mNotificationPulseTime;
412 if (DEBUG) Log.d(mTag, "Scheduling pulse " + instance + " in " + delta + "ms for "
414 mAlarmManager.setExact(AlarmManager.RTC_WAKEUP, time, notificationPulseIntent(instance));
417 private static String triggerEventToString(TriggerEvent event) {
418 if (event == null) return null;
419 final StringBuilder sb = new StringBuilder("TriggerEvent[")
420 .append(event.timestamp).append(',')
421 .append(event.sensor.getName());
422 if (event.values != null) {
423 for (int i = 0; i < event.values.length; i++) {
424 sb.append(',').append(event.values[i]);
427 return sb.append(']').toString();
430 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
432 public void onReceive(Context context, Intent intent) {
433 if (PULSE_ACTION.equals(intent.getAction())) {
434 if (DEBUG) Log.d(mTag, "Received pulse intent");
435 requestPulse(DozeLog.PULSE_REASON_INTENT);
437 if (NOTIFICATION_PULSE_ACTION.equals(intent.getAction())) {
438 final long instance = intent.getLongExtra(EXTRA_INSTANCE, -1);
439 if (DEBUG) Log.d(mTag, "Received notification pulse intent instance=" + instance);
440 DozeLog.traceNotificationPulse(instance);
441 requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
442 rescheduleNotificationPulse(mNotificationLightOn);
444 if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(intent.getAction())) {
446 if (mCarMode && mDreaming) {
453 private final DozeHost.Callback mHostCallback = new DozeHost.Callback() {
455 public void onNewNotifications() {
456 if (DEBUG) Log.d(mTag, "onNewNotifications (noop)");
461 public void onBuzzBeepBlinked() {
462 if (DEBUG) Log.d(mTag, "onBuzzBeepBlinked");
463 updateNotificationPulse(System.currentTimeMillis());
467 public void onNotificationLight(boolean on) {
468 if (DEBUG) Log.d(mTag, "onNotificationLight on=" + on);
469 if (mNotificationLightOn == on) return;
470 mNotificationLightOn = on;
471 if (mNotificationLightOn) {
472 updateNotificationPulseDueToLight();
477 public void onPowerSaveChanged(boolean active) {
478 mPowerSaveActive = active;
479 if (mPowerSaveActive && mDreaming) {
485 private class TriggerSensor extends TriggerEventListener {
486 private final Sensor mSensor;
487 private final boolean mConfigured;
488 private final boolean mDebugVibrate;
489 private final int mPulseReason;
491 private boolean mRequested;
492 private boolean mRegistered;
493 private boolean mDisabled;
495 public TriggerSensor(int type, boolean configured, boolean debugVibrate, int pulseReason) {
496 mSensor = mSensors.getDefaultSensor(type);
497 mConfigured = configured;
498 mDebugVibrate = debugVibrate;
499 mPulseReason = pulseReason;
502 public void setListening(boolean listen) {
503 if (mRequested == listen) return;
508 public void setDisabled(boolean disabled) {
509 if (mDisabled == disabled) return;
510 mDisabled = disabled;
514 private void updateListener() {
515 if (!mConfigured || mSensor == null) return;
516 if (mRequested && !mDisabled && !mRegistered) {
517 mRegistered = mSensors.requestTriggerSensor(this, mSensor);
518 if (DEBUG) Log.d(mTag, "requestTriggerSensor " + mRegistered);
519 } else if (mRegistered) {
520 final boolean rt = mSensors.cancelTriggerSensor(this, mSensor);
521 if (DEBUG) Log.d(mTag, "cancelTriggerSensor " + rt);
527 public String toString() {
528 return new StringBuilder("{mRegistered=").append(mRegistered)
529 .append(", mRequested=").append(mRequested)
530 .append(", mDisabled=").append(mDisabled)
531 .append(", mConfigured=").append(mConfigured)
532 .append(", mDebugVibrate=").append(mDebugVibrate)
533 .append(", mSensor=").append(mSensor).append("}").toString();
537 public void onTrigger(TriggerEvent event) {
540 if (DEBUG) Log.d(mTag, "onTrigger: " + triggerEventToString(event));
542 final Vibrator v = (Vibrator) mContext.getSystemService(
543 Context.VIBRATOR_SERVICE);
545 v.vibrate(1000, new AudioAttributes.Builder()
546 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
547 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build());
552 requestPulse(mPulseReason);
553 updateListener(); // reregister, this sensor only fires once
555 // reset the notification pulse schedule, but only if we think we were not triggered
556 // by a notification-related vibration
557 final long timeSinceNotification = System.currentTimeMillis()
558 - mNotificationPulseTime;
559 final boolean withinVibrationThreshold =
560 timeSinceNotification < mDozeParameters.getPickupVibrationThreshold();
561 if (withinVibrationThreshold) {
562 if (DEBUG) Log.d(mTag, "Not resetting schedule, recent notification");
564 resetNotificationResets();
566 if (mSensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
567 DozeLog.tracePickupPulse(withinVibrationThreshold);
575 private abstract class ProximityCheck implements SensorEventListener, Runnable {
576 private static final int TIMEOUT_DELAY_MS = 500;
578 protected static final int RESULT_UNKNOWN = 0;
579 protected static final int RESULT_NEAR = 1;
580 protected static final int RESULT_FAR = 2;
582 private final String mTag = DozeService.this.mTag + ".ProximityCheck";
584 private boolean mRegistered;
585 private boolean mFinished;
586 private float mMaxRange;
588 abstract public void onProximityResult(int result);
590 public void check() {
591 if (mFinished || mRegistered) return;
592 final Sensor sensor = mSensors.getDefaultSensor(Sensor.TYPE_PROXIMITY);
593 if (sensor == null) {
594 if (DEBUG) Log.d(mTag, "No sensor found");
595 finishWithResult(RESULT_UNKNOWN);
598 // the pickup sensor interferes with the prox event, disable it until we have a result
599 mPickupSensor.setDisabled(true);
601 mMaxRange = sensor.getMaximumRange();
602 mSensors.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, 0, mHandler);
603 mHandler.postDelayed(this, TIMEOUT_DELAY_MS);
608 public void onSensorChanged(SensorEvent event) {
609 if (event.values.length == 0) {
610 if (DEBUG) Log.d(mTag, "Event has no values!");
611 finishWithResult(RESULT_UNKNOWN);
613 if (DEBUG) Log.d(mTag, "Event: value=" + event.values[0] + " max=" + mMaxRange);
614 final boolean isNear = event.values[0] < mMaxRange;
615 finishWithResult(isNear ? RESULT_NEAR : RESULT_FAR);
621 if (DEBUG) Log.d(mTag, "No event received before timeout");
622 finishWithResult(RESULT_UNKNOWN);
625 private void finishWithResult(int result) {
626 if (mFinished) return;
628 mHandler.removeCallbacks(this);
629 mSensors.unregisterListener(this);
630 // we're done - reenable the pickup sensor
631 mPickupSensor.setDisabled(false);
634 onProximityResult(result);
639 public void onAccuracyChanged(Sensor sensor, int accuracy) {