DozeService.java revision 50a8ea6ec2a41cee05d1509f10c795d6b3bb90a8
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
53    private final String mTag = String.format(TAG + ".%08x", hashCode());
54    private final Context mContext = this;
55    private final Handler mHandler = new Handler();
56    private final DozeParameters mDozeParameters = new DozeParameters(mContext);
57
58    private Host mHost;
59    private SensorManager mSensors;
60    private TriggerSensor mSigMotionSensor;
61    private TriggerSensor mPickupSensor;
62    private PowerManager mPowerManager;
63    private PowerManager.WakeLock mWakeLock;
64    private AlarmManager mAlarmManager;
65    private boolean mDreaming;
66    private boolean mBroadcastReceiverRegistered;
67    private boolean mDisplayStateSupported;
68    private int mDisplayStateWhenOn;
69    private boolean mNotificationLightOn;
70    private PendingIntent mNotificationPulseIntent;
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        mNotificationPulseIntent = PendingIntent.getBroadcast(mContext, 0,
119                new Intent(NOTIFICATION_PULSE_ACTION).setPackage(getPackageName()),
120                PendingIntent.FLAG_UPDATE_CURRENT);
121        mDisplayStateWhenOn = mDisplayStateSupported ? Display.STATE_DOZE : Display.STATE_ON;
122        mDisplayOff.run();
123    }
124
125    @Override
126    public void onAttachedToWindow() {
127        if (DEBUG) Log.d(mTag, "onAttachedToWindow");
128        super.onAttachedToWindow();
129    }
130
131    @Override
132    public void onDreamingStarted() {
133        super.onDreamingStarted();
134        mPowerSaveActive = mHost != null && mHost.isPowerSaveActive();
135        if (DEBUG) Log.d(mTag, "onDreamingStarted canDoze=" + canDoze() + " mPowerSaveActive="
136                + mPowerSaveActive);
137        if (mPowerSaveActive) {
138            finishToSavePower();
139            return;
140        }
141        mDreaming = true;
142        listenForPulseSignals(true);
143        rescheduleNotificationPulse(false /*predicate*/);  // cancel any pending pulse alarms
144        requestDoze();
145    }
146
147    public void stayAwake(long millis) {
148        if (mDreaming && millis > 0) {
149            if (DEBUG) Log.d(mTag, "stayAwake millis=" + millis);
150            mWakeLock.acquire(millis);
151            setDozeScreenState(mDisplayStateWhenOn);
152            rescheduleOff(millis);
153        }
154    }
155
156    private void rescheduleOff(long millis) {
157        if (DEBUG) Log.d(TAG, "rescheduleOff millis=" + millis);
158        mHandler.removeCallbacks(mDisplayOff);
159        mHandler.postDelayed(mDisplayOff, millis);
160    }
161
162    @Override
163    public void onDreamingStopped() {
164        if (DEBUG) Log.d(mTag, "onDreamingStopped isDozing=" + isDozing());
165        super.onDreamingStopped();
166
167        mDreaming = false;
168        if (mWakeLock.isHeld()) {
169            mWakeLock.release();
170        }
171        listenForPulseSignals(false);
172        dozingStopped();
173        mHandler.removeCallbacks(mDisplayOff);
174    }
175
176    @Override
177    public void startDozing() {
178        if (DEBUG) Log.d(mTag, "startDozing");
179        super.startDozing();
180    }
181
182    private void requestDoze() {
183        if (mHost != null) {
184            mHost.requestDoze(this);
185        }
186    }
187
188    private void requestPulse() {
189        if (mHost != null) {
190            mHost.requestPulse(this);
191        }
192    }
193
194    private void dozingStopped() {
195        if (mHost != null) {
196            mHost.dozingStopped(this);
197        }
198    }
199
200    private void finishToSavePower() {
201        Log.w(mTag, "Exiting ambient mode due to low power battery saver");
202        finish();
203    }
204
205    private void listenForPulseSignals(boolean listen) {
206        if (DEBUG) Log.d(mTag, "listenForPulseSignals: " + listen);
207        mSigMotionSensor.setListening(listen);
208        mPickupSensor.setListening(listen);
209        listenForBroadcasts(listen);
210        listenForNotifications(listen);
211    }
212
213    private void listenForBroadcasts(boolean listen) {
214        if (listen) {
215            final IntentFilter filter = new IntentFilter(PULSE_ACTION);
216            filter.addAction(NOTIFICATION_PULSE_ACTION);
217            mContext.registerReceiver(mBroadcastReceiver, filter);
218            mBroadcastReceiverRegistered = true;
219        } else {
220            if (mBroadcastReceiverRegistered) {
221                mContext.unregisterReceiver(mBroadcastReceiver);
222            }
223            mBroadcastReceiverRegistered = false;
224        }
225    }
226
227    private void listenForNotifications(boolean listen) {
228        if (mHost == null) return;
229        if (listen) {
230            resetNotificationResets();
231            mHost.addCallback(mHostCallback);
232        } else {
233            mHost.removeCallback(mHostCallback);
234        }
235    }
236
237    private void resetNotificationResets() {
238        if (DEBUG) Log.d(TAG, "resetNotificationResets");
239        mScheduleResetsRemaining = mDozeParameters.getPulseScheduleResets();
240    }
241
242    private void updateNotificationPulse() {
243        if (DEBUG) Log.d(TAG, "updateNotificationPulse");
244        if (!mDozeParameters.getPulseOnNotifications()) return;
245        if (mScheduleResetsRemaining <= 0) {
246            if (DEBUG) Log.d(TAG, "No more schedule resets remaining");
247            return;
248        }
249        final long now = System.currentTimeMillis();
250        if ((now - mNotificationPulseTime) < mDozeParameters.getPulseDuration()) {
251            if (DEBUG) Log.d(TAG, "Recently updated, not resetting schedule");
252            return;
253        }
254        mScheduleResetsRemaining--;
255        if (DEBUG) Log.d(TAG, "mScheduleResetsRemaining = " + mScheduleResetsRemaining);
256        mNotificationPulseTime = now;
257        rescheduleNotificationPulse(true /*predicate*/);
258    }
259
260    private void rescheduleNotificationPulse(boolean predicate) {
261        if (DEBUG) Log.d(TAG, "rescheduleNotificationPulse predicate=" + predicate);
262        mAlarmManager.cancel(mNotificationPulseIntent);
263        if (!predicate) {
264            if (DEBUG) Log.d(TAG, "  don't reschedule: predicate is false");
265            return;
266        }
267        final PulseSchedule schedule = mDozeParameters.getPulseSchedule();
268        if (schedule == null) {
269            if (DEBUG) Log.d(TAG, "  don't reschedule: schedule is null");
270            return;
271        }
272        final long now = System.currentTimeMillis();
273        final long time = schedule.getNextTime(now, mNotificationPulseTime);
274        if (time <= 0) {
275            if (DEBUG) Log.d(TAG, "  don't reschedule: time is " + time);
276            return;
277        }
278        final long delta = time - now;
279        if (delta <= 0) {
280            if (DEBUG) Log.d(TAG, "  don't reschedule: delta is " + delta);
281            return;
282        }
283        if (DEBUG) Log.d(TAG, "Scheduling pulse in " + delta + "ms for " + new Date(time));
284        mAlarmManager.setExact(AlarmManager.RTC_WAKEUP, time, mNotificationPulseIntent);
285    }
286
287    private static String triggerEventToString(TriggerEvent event) {
288        if (event == null) return null;
289        final StringBuilder sb = new StringBuilder("TriggerEvent[")
290                .append(event.timestamp).append(',')
291                .append(event.sensor.getName());
292        if (event.values != null) {
293            for (int i = 0; i < event.values.length; i++) {
294                sb.append(',').append(event.values[i]);
295            }
296        }
297        return sb.append(']').toString();
298    }
299
300    private final Runnable mDisplayOff = new Runnable() {
301        @Override
302        public void run() {
303            if (DEBUG) Log.d(TAG, "Display off");
304            setDozeScreenState(Display.STATE_OFF);
305        }
306    };
307
308    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
309        @Override
310        public void onReceive(Context context, Intent intent) {
311            if (PULSE_ACTION.equals(intent.getAction())) {
312                if (DEBUG) Log.d(mTag, "Received pulse intent");
313                requestPulse();
314            }
315            if (NOTIFICATION_PULSE_ACTION.equals(intent.getAction())) {
316                if (DEBUG) Log.d(mTag, "Received notification pulse intent");
317                requestPulse();
318                rescheduleNotificationPulse(mNotificationLightOn);
319            }
320        }
321    };
322
323    private final Host.Callback mHostCallback = new Host.Callback() {
324        @Override
325        public void onNewNotifications() {
326            if (DEBUG) Log.d(mTag, "onNewNotifications");
327            // noop for now
328        }
329
330        @Override
331        public void onBuzzBeepBlinked() {
332            if (DEBUG) Log.d(mTag, "onBuzzBeepBlinked");
333            updateNotificationPulse();
334        }
335
336        @Override
337        public void onNotificationLight(boolean on) {
338            if (DEBUG) Log.d(mTag, "onNotificationLight on=" + on);
339            if (mNotificationLightOn == on) return;
340            mNotificationLightOn = on;
341            if (mNotificationLightOn) {
342                updateNotificationPulse();
343            }
344        }
345
346        @Override
347        public void onPowerSaveChanged(boolean active) {
348            mPowerSaveActive = active;
349            if (mPowerSaveActive && mDreaming) {
350                finishToSavePower();
351            }
352        }
353    };
354
355    public interface Host {
356        void addCallback(Callback callback);
357        void removeCallback(Callback callback);
358        void requestDoze(DozeService dozeService);
359        void requestPulse(DozeService dozeService);
360        void dozingStopped(DozeService dozeService);
361        boolean isPowerSaveActive();
362
363        public interface Callback {
364            void onNewNotifications();
365            void onBuzzBeepBlinked();
366            void onNotificationLight(boolean on);
367            void onPowerSaveChanged(boolean active);
368        }
369    }
370
371    private class TriggerSensor extends TriggerEventListener {
372        private final Sensor mSensor;
373        private final boolean mConfigured;
374        private final boolean mDebugVibrate;
375
376        private boolean mEnabled;
377
378        public TriggerSensor(int type, boolean configured, boolean debugVibrate) {
379            mSensor = mSensors.getDefaultSensor(type);
380            mConfigured = configured;
381            mDebugVibrate = debugVibrate;
382        }
383
384        public void setListening(boolean listen) {
385            if (!mConfigured || mSensor == null) return;
386            if (listen) {
387                mEnabled = mSensors.requestTriggerSensor(this, mSensor);
388            } else if (mEnabled) {
389                mSensors.cancelTriggerSensor(this, mSensor);
390                mEnabled = false;
391            }
392        }
393
394        @Override
395        public String toString() {
396            return new StringBuilder("{mEnabled=").append(mEnabled).append(", mConfigured=")
397                    .append(mConfigured).append(", mDebugVibrate=").append(mDebugVibrate)
398                    .append(", mSensor=").append(mSensor).append("}").toString();
399        }
400
401        @Override
402        public void onTrigger(TriggerEvent event) {
403            if (DEBUG) Log.d(mTag, "onTrigger: " + triggerEventToString(event));
404            if (mDebugVibrate) {
405                final Vibrator v = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
406                if (v != null) {
407                    v.vibrate(1000, new AudioAttributes.Builder()
408                            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
409                            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build());
410                }
411            }
412            requestPulse();
413            setListening(true);  // reregister, this sensor only fires once
414
415            // reset the notification pulse schedule, but only if we think we were not triggered
416            // by a notification-related vibration
417            final long timeSinceNotification = System.currentTimeMillis() - mNotificationPulseTime;
418            if (timeSinceNotification < mDozeParameters.getPickupVibrationThreshold()) {
419               if (DEBUG) Log.d(mTag, "Not resetting schedule, recent notification");
420            } else {
421                resetNotificationResets();
422            }
423        }
424    }
425}
426