DozeService.java revision 813552cc27cbeac366166cdda82fd813c8458bbf
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.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.hardware.Sensor;
26import android.hardware.SensorManager;
27import android.hardware.TriggerEvent;
28import android.hardware.TriggerEventListener;
29import android.media.AudioAttributes;
30import android.os.Handler;
31import android.os.PowerManager;
32import android.os.Vibrator;
33import android.service.dreams.DreamService;
34import android.util.Log;
35import android.view.Display;
36
37import com.android.systemui.SystemUIApplication;
38import com.android.systemui.statusbar.phone.DozeParameters;
39import com.android.systemui.statusbar.phone.DozeParameters.PulseSchedule;
40
41import java.io.FileDescriptor;
42import java.io.PrintWriter;
43import java.util.Date;
44
45public class DozeService extends DreamService {
46    private static final String TAG = "DozeService";
47    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
48
49    private static final String ACTION_BASE = "com.android.systemui.doze";
50    private static final String PULSE_ACTION = ACTION_BASE + ".pulse";
51    private static final String NOTIFICATION_PULSE_ACTION = ACTION_BASE + ".notification_pulse";
52    private static final String EXTRA_INSTANCE = "instance";
53
54    private final String mTag = String.format(TAG + ".%08x", hashCode());
55    private final Context mContext = this;
56    private final Handler mHandler = new Handler();
57    private final DozeParameters mDozeParameters = new DozeParameters(mContext);
58
59    private Host mHost;
60    private SensorManager mSensors;
61    private TriggerSensor mSigMotionSensor;
62    private TriggerSensor mPickupSensor;
63    private PowerManager mPowerManager;
64    private PowerManager.WakeLock mWakeLock;
65    private AlarmManager mAlarmManager;
66    private boolean mDreaming;
67    private boolean mBroadcastReceiverRegistered;
68    private boolean mDisplayStateSupported;
69    private int mDisplayStateWhenOn;
70    private boolean mNotificationLightOn;
71    private boolean mPowerSaveActive;
72    private long mNotificationPulseTime;
73    private int mScheduleResetsRemaining;
74
75    public DozeService() {
76        if (DEBUG) Log.d(mTag, "new DozeService()");
77        setDebug(DEBUG);
78    }
79
80    @Override
81    protected void dumpOnHandler(FileDescriptor fd, PrintWriter pw, String[] args) {
82        super.dumpOnHandler(fd, pw, args);
83        pw.print("  mDreaming: "); pw.println(mDreaming);
84        pw.print("  mHost: "); pw.println(mHost);
85        pw.print("  mBroadcastReceiverRegistered: "); pw.println(mBroadcastReceiverRegistered);
86        pw.print("  mSigMotionSensor: "); pw.println(mSigMotionSensor);
87        pw.print("  mPickupSensor:"); pw.println(mPickupSensor);
88        pw.print("  mDisplayStateSupported: "); pw.println(mDisplayStateSupported);
89        pw.print("  mNotificationLightOn: "); pw.println(mNotificationLightOn);
90        pw.print("  mPowerSaveActive: "); pw.println(mPowerSaveActive);
91        pw.print("  mNotificationPulseTime: "); pw.println(mNotificationPulseTime);
92        pw.print("  mScheduleResetsRemaining: "); pw.println(mScheduleResetsRemaining);
93        mDozeParameters.dump(pw);
94    }
95
96    @Override
97    public void onCreate() {
98        if (DEBUG) Log.d(mTag, "onCreate");
99        super.onCreate();
100
101        if (getApplication() instanceof SystemUIApplication) {
102            final SystemUIApplication app = (SystemUIApplication) getApplication();
103            mHost = app.getComponent(Host.class);
104        }
105        if (mHost == null) Log.w(TAG, "No doze service host found.");
106
107        setWindowless(true);
108
109        mSensors = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
110        mSigMotionSensor = new TriggerSensor(Sensor.TYPE_SIGNIFICANT_MOTION,
111                mDozeParameters.getPulseOnSigMotion(), mDozeParameters.getVibrateOnSigMotion());
112        mPickupSensor = new TriggerSensor(Sensor.TYPE_PICK_UP_GESTURE,
113                mDozeParameters.getPulseOnPickup(), mDozeParameters.getVibrateOnPickup());
114        mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
115        mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
116        mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
117        mDisplayStateSupported = mDozeParameters.getDisplayStateSupported();
118        mDisplayStateWhenOn = mDisplayStateSupported ? Display.STATE_DOZE : Display.STATE_ON;
119        mDisplayOff.run();
120    }
121
122    @Override
123    public void onAttachedToWindow() {
124        if (DEBUG) Log.d(mTag, "onAttachedToWindow");
125        super.onAttachedToWindow();
126    }
127
128    @Override
129    public void onDreamingStarted() {
130        super.onDreamingStarted();
131        mPowerSaveActive = mHost != null && mHost.isPowerSaveActive();
132        if (DEBUG) Log.d(mTag, "onDreamingStarted canDoze=" + canDoze() + " mPowerSaveActive="
133                + mPowerSaveActive);
134        if (mPowerSaveActive) {
135            finishToSavePower();
136            return;
137        }
138        mDreaming = true;
139        listenForPulseSignals(true);
140        rescheduleNotificationPulse(false /*predicate*/);  // cancel any pending pulse alarms
141        requestDoze();
142    }
143
144    public void stayAwake(long millis) {
145        if (mDreaming && millis > 0) {
146            if (DEBUG) Log.d(mTag, "stayAwake millis=" + millis);
147            mWakeLock.acquire(millis);
148            setDozeScreenState(mDisplayStateWhenOn);
149            rescheduleOff(millis);
150        }
151    }
152
153    private void rescheduleOff(long millis) {
154        if (DEBUG) Log.d(TAG, "rescheduleOff millis=" + millis);
155        mHandler.removeCallbacks(mDisplayOff);
156        mHandler.postDelayed(mDisplayOff, millis);
157    }
158
159    @Override
160    public void onDreamingStopped() {
161        if (DEBUG) Log.d(mTag, "onDreamingStopped isDozing=" + isDozing());
162        super.onDreamingStopped();
163
164        mDreaming = false;
165        if (mWakeLock.isHeld()) {
166            mWakeLock.release();
167        }
168        listenForPulseSignals(false);
169        dozingStopped();
170        mHandler.removeCallbacks(mDisplayOff);
171    }
172
173    @Override
174    public void startDozing() {
175        if (DEBUG) Log.d(mTag, "startDozing");
176        super.startDozing();
177    }
178
179    private void requestDoze() {
180        if (mHost != null) {
181            mHost.requestDoze(this);
182        }
183    }
184
185    private void requestPulse() {
186        if (mHost != null) {
187            mHost.requestPulse(this);
188        }
189    }
190
191    private void dozingStopped() {
192        if (mHost != null) {
193            mHost.dozingStopped(this);
194        }
195    }
196
197    private void finishToSavePower() {
198        Log.w(mTag, "Exiting ambient mode due to low power battery saver");
199        finish();
200    }
201
202    private void listenForPulseSignals(boolean listen) {
203        if (DEBUG) Log.d(mTag, "listenForPulseSignals: " + listen);
204        mSigMotionSensor.setListening(listen);
205        mPickupSensor.setListening(listen);
206        listenForBroadcasts(listen);
207        listenForNotifications(listen);
208    }
209
210    private void listenForBroadcasts(boolean listen) {
211        if (listen) {
212            final IntentFilter filter = new IntentFilter(PULSE_ACTION);
213            filter.addAction(NOTIFICATION_PULSE_ACTION);
214            mContext.registerReceiver(mBroadcastReceiver, filter);
215            mBroadcastReceiverRegistered = true;
216        } else {
217            if (mBroadcastReceiverRegistered) {
218                mContext.unregisterReceiver(mBroadcastReceiver);
219            }
220            mBroadcastReceiverRegistered = false;
221        }
222    }
223
224    private void listenForNotifications(boolean listen) {
225        if (mHost == null) return;
226        if (listen) {
227            resetNotificationResets();
228            mHost.addCallback(mHostCallback);
229        } else {
230            mHost.removeCallback(mHostCallback);
231        }
232    }
233
234    private void resetNotificationResets() {
235        if (DEBUG) Log.d(TAG, "resetNotificationResets");
236        mScheduleResetsRemaining = mDozeParameters.getPulseScheduleResets();
237    }
238
239    private void updateNotificationPulse() {
240        if (DEBUG) Log.d(TAG, "updateNotificationPulse");
241        if (!mDozeParameters.getPulseOnNotifications()) return;
242        if (mScheduleResetsRemaining <= 0) {
243            if (DEBUG) Log.d(TAG, "No more schedule resets remaining");
244            return;
245        }
246        final long now = System.currentTimeMillis();
247        if ((now - mNotificationPulseTime) < mDozeParameters.getPulseDuration()) {
248            if (DEBUG) Log.d(TAG, "Recently updated, not resetting schedule");
249            return;
250        }
251        mScheduleResetsRemaining--;
252        if (DEBUG) Log.d(TAG, "mScheduleResetsRemaining = " + mScheduleResetsRemaining);
253        mNotificationPulseTime = now;
254        rescheduleNotificationPulse(true /*predicate*/);
255    }
256
257    private PendingIntent notificationPulseIntent(long instance) {
258        return PendingIntent.getBroadcast(mContext, 0,
259                new Intent(NOTIFICATION_PULSE_ACTION).setPackage(getPackageName())
260                        .putExtra(EXTRA_INSTANCE, instance),
261                PendingIntent.FLAG_UPDATE_CURRENT);
262    }
263
264    private void rescheduleNotificationPulse(boolean predicate) {
265        if (DEBUG) Log.d(TAG, "rescheduleNotificationPulse predicate=" + predicate);
266        final PendingIntent notificationPulseIntent = notificationPulseIntent(0);
267        mAlarmManager.cancel(notificationPulseIntent);
268        if (!predicate) {
269            if (DEBUG) Log.d(TAG, "  don't reschedule: predicate is false");
270            return;
271        }
272        final PulseSchedule schedule = mDozeParameters.getPulseSchedule();
273        if (schedule == null) {
274            if (DEBUG) Log.d(TAG, "  don't reschedule: schedule is null");
275            return;
276        }
277        final long now = System.currentTimeMillis();
278        final long time = schedule.getNextTime(now, mNotificationPulseTime);
279        if (time <= 0) {
280            if (DEBUG) Log.d(TAG, "  don't reschedule: time is " + time);
281            return;
282        }
283        final long delta = time - now;
284        if (delta <= 0) {
285            if (DEBUG) Log.d(TAG, "  don't reschedule: delta is " + delta);
286            return;
287        }
288        final long instance = time - mNotificationPulseTime;
289        if (DEBUG) Log.d(TAG, "Scheduling pulse " + instance + " in " + delta + "ms for "
290                + new Date(time));
291        mAlarmManager.setExact(AlarmManager.RTC_WAKEUP, time, notificationPulseIntent(instance));
292    }
293
294    private static String triggerEventToString(TriggerEvent event) {
295        if (event == null) return null;
296        final StringBuilder sb = new StringBuilder("TriggerEvent[")
297                .append(event.timestamp).append(',')
298                .append(event.sensor.getName());
299        if (event.values != null) {
300            for (int i = 0; i < event.values.length; i++) {
301                sb.append(',').append(event.values[i]);
302            }
303        }
304        return sb.append(']').toString();
305    }
306
307    private final Runnable mDisplayOff = new Runnable() {
308        @Override
309        public void run() {
310            if (DEBUG) Log.d(TAG, "Display off");
311            setDozeScreenState(Display.STATE_OFF);
312        }
313    };
314
315    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
316        @Override
317        public void onReceive(Context context, Intent intent) {
318            if (PULSE_ACTION.equals(intent.getAction())) {
319                if (DEBUG) Log.d(mTag, "Received pulse intent");
320                requestPulse();
321            }
322            if (NOTIFICATION_PULSE_ACTION.equals(intent.getAction())) {
323                final long instance = intent.getLongExtra(EXTRA_INSTANCE, -1);
324                if (DEBUG) Log.d(mTag, "Received notification pulse intent instance=" + instance);
325                DozeLog.traceNotificationPulse(instance);
326                requestPulse();
327                rescheduleNotificationPulse(mNotificationLightOn);
328            }
329        }
330    };
331
332    private final Host.Callback mHostCallback = new Host.Callback() {
333        @Override
334        public void onNewNotifications() {
335            if (DEBUG) Log.d(mTag, "onNewNotifications");
336            // noop for now
337        }
338
339        @Override
340        public void onBuzzBeepBlinked() {
341            if (DEBUG) Log.d(mTag, "onBuzzBeepBlinked");
342            updateNotificationPulse();
343        }
344
345        @Override
346        public void onNotificationLight(boolean on) {
347            if (DEBUG) Log.d(mTag, "onNotificationLight on=" + on);
348            if (mNotificationLightOn == on) return;
349            mNotificationLightOn = on;
350            if (mNotificationLightOn) {
351                updateNotificationPulse();
352            }
353        }
354
355        @Override
356        public void onPowerSaveChanged(boolean active) {
357            mPowerSaveActive = active;
358            if (mPowerSaveActive && mDreaming) {
359                finishToSavePower();
360            }
361        }
362    };
363
364    public interface Host {
365        void addCallback(Callback callback);
366        void removeCallback(Callback callback);
367        void requestDoze(DozeService dozeService);
368        void requestPulse(DozeService dozeService);
369        void dozingStopped(DozeService dozeService);
370        boolean isPowerSaveActive();
371
372        public interface Callback {
373            void onNewNotifications();
374            void onBuzzBeepBlinked();
375            void onNotificationLight(boolean on);
376            void onPowerSaveChanged(boolean active);
377        }
378    }
379
380    private class TriggerSensor extends TriggerEventListener {
381        private final Sensor mSensor;
382        private final boolean mConfigured;
383        private final boolean mDebugVibrate;
384
385        private boolean mEnabled;
386
387        public TriggerSensor(int type, boolean configured, boolean debugVibrate) {
388            mSensor = mSensors.getDefaultSensor(type);
389            mConfigured = configured;
390            mDebugVibrate = debugVibrate;
391        }
392
393        public void setListening(boolean listen) {
394            if (!mConfigured || mSensor == null) return;
395            if (listen) {
396                mEnabled = mSensors.requestTriggerSensor(this, mSensor);
397            } else if (mEnabled) {
398                mSensors.cancelTriggerSensor(this, mSensor);
399                mEnabled = false;
400            }
401        }
402
403        @Override
404        public String toString() {
405            return new StringBuilder("{mEnabled=").append(mEnabled).append(", mConfigured=")
406                    .append(mConfigured).append(", mDebugVibrate=").append(mDebugVibrate)
407                    .append(", mSensor=").append(mSensor).append("}").toString();
408        }
409
410        @Override
411        public void onTrigger(TriggerEvent event) {
412            if (DEBUG) Log.d(mTag, "onTrigger: " + triggerEventToString(event));
413            if (mDebugVibrate) {
414                final Vibrator v = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
415                if (v != null) {
416                    v.vibrate(1000, new AudioAttributes.Builder()
417                            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
418                            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build());
419                }
420            }
421            requestPulse();
422            setListening(true);  // reregister, this sensor only fires once
423
424            // reset the notification pulse schedule, but only if we think we were not triggered
425            // by a notification-related vibration
426            final long timeSinceNotification = System.currentTimeMillis() - mNotificationPulseTime;
427            final boolean withinVibrationThreshold =
428                    timeSinceNotification < mDozeParameters.getPickupVibrationThreshold();
429            if (withinVibrationThreshold) {
430               if (DEBUG) Log.d(mTag, "Not resetting schedule, recent notification");
431            } else {
432                resetNotificationResets();
433            }
434            if (mSensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
435                DozeLog.tracePickupPulse(withinVibrationThreshold);
436            }
437        }
438    }
439}
440