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