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 mLastScheduleResetTime;
92    private long mEarliestPulseDueToLight;
93    private int mScheduleResetsRemaining;
94
95    public DozeService() {
96        if (DEBUG) Log.d(mTag, "new DozeService()");
97        setDebug(DEBUG);
98    }
99
100    @Override
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);
117    }
118
119    @Override
120    public void onCreate() {
121        if (DEBUG) Log.d(mTag, "onCreate");
122        super.onCreate();
123
124        if (getApplication() instanceof SystemUIApplication) {
125            final SystemUIApplication app = (SystemUIApplication) getApplication();
126            mHost = app.getComponent(DozeHost.class);
127        }
128        if (mHost == null) Log.w(TAG, "No doze service host found.");
129
130        setWindowless(true);
131
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);
145        turnDisplayOff();
146    }
147
148    @Override
149    public void onAttachedToWindow() {
150        if (DEBUG) Log.d(mTag, "onAttachedToWindow");
151        super.onAttachedToWindow();
152    }
153
154    @Override
155    public void onDreamingStarted() {
156        super.onDreamingStarted();
157
158        if (mHost == null) {
159            finish();
160            return;
161        }
162
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) {
168            finishToSavePower();
169            return;
170        }
171        if (mCarMode) {
172            finishForCarMode();
173            return;
174        }
175
176        mDreaming = true;
177        rescheduleNotificationPulse(false /*predicate*/);  // cancel any pending pulse alarms
178        mEarliestPulseDueToLight = System.currentTimeMillis() + EARLIEST_LIGHT_PULSE_AFTER_START_MS;
179        listenForPulseSignals(true);
180
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() {
185            @Override
186            public void run() {
187                if (mDreaming) {
188                    startDozing();
189
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.
193                }
194            }
195        });
196    }
197
198    @Override
199    public void onDreamingStopped() {
200        if (DEBUG) Log.d(mTag, "onDreamingStopped isDozing=" + isDozing());
201        super.onDreamingStopped();
202
203        if (mHost == null) {
204            return;
205        }
206
207        mDreaming = false;
208        listenForPulseSignals(false);
209
210        // Tell the host that it's over.
211        mHost.stopDozing();
212    }
213
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.
219            mWakeLock.acquire();
220            mPulsing = true;
221            if (!mDozeParameters.getProxCheckBeforePulse()) {
222                // skip proximity check
223                continuePulsing(reason);
224                return;
225            }
226            final long start = SystemClock.uptimeMillis();
227            final boolean nonBlocking = reason == DozeLog.PULSE_REASON_SENSOR_PICKUP
228                    && mDozeParameters.getPickupPerformsProxCheck();
229            if (nonBlocking) {
230                // proximity check is only done to capture statistics, continue pulsing
231                continuePulsing(reason);
232            }
233            // perform a proximity check
234            new ProximityCheck() {
235                @Override
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);
240                    if (nonBlocking) {
241                        // we already continued
242                        return;
243                    }
244                    // avoid pulsing in pockets
245                    if (isNear) {
246                        mPulsing = false;
247                        mWakeLock.release();
248                        return;
249                    }
250
251                    // not in-pocket, continue pulsing
252                    continuePulsing(reason);
253                }
254            }.check();
255        }
256    }
257
258    private void continuePulsing(int reason) {
259        if (mHost.isPulsingBlocked()) {
260            mPulsing = false;
261            mWakeLock.release();
262            return;
263        }
264        mHost.pulseWhileDozing(new DozeHost.PulseCallback() {
265            @Override
266            public void onPulseStarted() {
267                if (mPulsing && mDreaming) {
268                    turnDisplayOn();
269                }
270            }
271
272            @Override
273            public void onPulseFinished() {
274                if (mPulsing && mDreaming) {
275                    mPulsing = false;
276                    turnDisplayOff();
277                }
278                mWakeLock.release(); // needs to be unconditional to balance acquire
279            }
280        }, reason);
281    }
282
283    private void turnDisplayOff() {
284        if (DEBUG) Log.d(mTag, "Display off");
285        setDozeScreenState(Display.STATE_OFF);
286    }
287
288    private void turnDisplayOn() {
289        if (DEBUG) Log.d(mTag, "Display on");
290        setDozeScreenState(mDisplayStateSupported ? Display.STATE_DOZE : Display.STATE_ON);
291    }
292
293    private void finishToSavePower() {
294        Log.w(mTag, "Exiting ambient mode due to low power battery saver");
295        finish();
296    }
297
298    private void finishForCarMode() {
299        Log.w(mTag, "Exiting ambient mode, not allowed in car mode");
300        finish();
301    }
302
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);
309    }
310
311    private void listenForBroadcasts(boolean listen) {
312        if (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;
318        } else {
319            if (mBroadcastReceiverRegistered) {
320                mContext.unregisterReceiver(mBroadcastReceiver);
321            }
322            mBroadcastReceiverRegistered = false;
323        }
324    }
325
326    private void listenForNotifications(boolean listen) {
327        if (listen) {
328            resetNotificationResets();
329            mHost.addCallback(mHostCallback);
330
331            // Continue to pulse for existing LEDs.
332            mNotificationLightOn = mHost.isNotificationLightOn();
333            if (mNotificationLightOn) {
334                updateNotificationPulseDueToLight();
335            }
336        } else {
337            mHost.removeCallback(mHostCallback);
338        }
339    }
340
341    private void resetNotificationResets() {
342        if (DEBUG) Log.d(mTag, "resetNotificationResets");
343        mScheduleResetsRemaining = mDozeParameters.getPulseScheduleResets();
344    }
345
346    private void updateNotificationPulseDueToLight() {
347        long timeMs = System.currentTimeMillis();
348        timeMs = Math.max(timeMs, mEarliestPulseDueToLight);
349        updateNotificationPulse(timeMs);
350    }
351
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");
357            return;
358        }
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");
366            return;
367        }
368        if (DEBUG) Log.d(mTag, "mScheduleResetsRemaining = " + mScheduleResetsRemaining);
369        mNotificationPulseTime = notificationTimeMs;
370        if (pulseImmediately) {
371            DozeLog.traceNotificationPulse(0);
372            requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
373        }
374        // schedule the rest of the pulses
375        rescheduleNotificationPulse(true /*predicate*/);
376    }
377
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);
385    }
386
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);
391        if (!predicate) {
392            if (DEBUG) Log.d(mTag, "  don't reschedule: predicate is false");
393            return;
394        }
395        final PulseSchedule schedule = mDozeParameters.getPulseSchedule();
396        if (schedule == null) {
397            if (DEBUG) Log.d(mTag, "  don't reschedule: schedule is null");
398            return;
399        }
400        final long now = System.currentTimeMillis();
401        final long time = schedule.getNextTime(now, mNotificationPulseTime);
402        if (time <= 0) {
403            if (DEBUG) Log.d(mTag, "  don't reschedule: time is " + time);
404            return;
405        }
406        final long delta = time - now;
407        if (delta <= 0) {
408            if (DEBUG) Log.d(mTag, "  don't reschedule: delta is " + delta);
409            return;
410        }
411        final long instance = time - mNotificationPulseTime;
412        if (DEBUG) Log.d(mTag, "Scheduling pulse " + instance + " in " + delta + "ms for "
413                + new Date(time));
414        mAlarmManager.setExact(AlarmManager.RTC_WAKEUP, time, notificationPulseIntent(instance));
415    }
416
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]);
425            }
426        }
427        return sb.append(']').toString();
428    }
429
430    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
431        @Override
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);
436            }
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);
443            }
444            if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(intent.getAction())) {
445                mCarMode = true;
446                if (mCarMode && mDreaming) {
447                    finishForCarMode();
448                }
449            }
450        }
451    };
452
453    private final DozeHost.Callback mHostCallback = new DozeHost.Callback() {
454        @Override
455        public void onNewNotifications() {
456            if (DEBUG) Log.d(mTag, "onNewNotifications (noop)");
457            // noop for now
458        }
459
460        @Override
461        public void onBuzzBeepBlinked() {
462            if (DEBUG) Log.d(mTag, "onBuzzBeepBlinked");
463            updateNotificationPulse(System.currentTimeMillis());
464        }
465
466        @Override
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();
473            }
474        }
475
476        @Override
477        public void onPowerSaveChanged(boolean active) {
478            mPowerSaveActive = active;
479            if (mPowerSaveActive && mDreaming) {
480                finishToSavePower();
481            }
482        }
483    };
484
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;
490
491        private boolean mRequested;
492        private boolean mRegistered;
493        private boolean mDisabled;
494
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;
500        }
501
502        public void setListening(boolean listen) {
503            if (mRequested == listen) return;
504            mRequested = listen;
505            updateListener();
506        }
507
508        public void setDisabled(boolean disabled) {
509            if (mDisabled == disabled) return;
510            mDisabled = disabled;
511            updateListener();
512        }
513
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);
522                mRegistered = false;
523            }
524        }
525
526        @Override
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();
534        }
535
536        @Override
537        public void onTrigger(TriggerEvent event) {
538            mWakeLock.acquire();
539            try {
540                if (DEBUG) Log.d(mTag, "onTrigger: " + triggerEventToString(event));
541                if (mDebugVibrate) {
542                    final Vibrator v = (Vibrator) mContext.getSystemService(
543                            Context.VIBRATOR_SERVICE);
544                    if (v != null) {
545                        v.vibrate(1000, new AudioAttributes.Builder()
546                                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
547                                .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build());
548                    }
549                }
550
551                mRegistered = false;
552                requestPulse(mPulseReason);
553                updateListener();  // reregister, this sensor only fires once
554
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");
563                } else {
564                    resetNotificationResets();
565                }
566                if (mSensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
567                    DozeLog.tracePickupPulse(withinVibrationThreshold);
568                }
569            } finally {
570                mWakeLock.release();
571            }
572        }
573    }
574
575    private abstract class ProximityCheck implements SensorEventListener, Runnable {
576        private static final int TIMEOUT_DELAY_MS = 500;
577
578        protected static final int RESULT_UNKNOWN = 0;
579        protected static final int RESULT_NEAR = 1;
580        protected static final int RESULT_FAR = 2;
581
582        private final String mTag = DozeService.this.mTag + ".ProximityCheck";
583
584        private boolean mRegistered;
585        private boolean mFinished;
586        private float mMaxRange;
587
588        abstract public void onProximityResult(int result);
589
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);
596                return;
597            }
598            // the pickup sensor interferes with the prox event, disable it until we have a result
599            mPickupSensor.setDisabled(true);
600
601            mMaxRange = sensor.getMaximumRange();
602            mSensors.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, 0, mHandler);
603            mHandler.postDelayed(this, TIMEOUT_DELAY_MS);
604            mRegistered = true;
605        }
606
607        @Override
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);
612            } else {
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);
616            }
617        }
618
619        @Override
620        public void run() {
621            if (DEBUG) Log.d(mTag, "No event received before timeout");
622            finishWithResult(RESULT_UNKNOWN);
623        }
624
625        private void finishWithResult(int result) {
626            if (mFinished) return;
627            if (mRegistered) {
628                mHandler.removeCallbacks(this);
629                mSensors.unregisterListener(this);
630                // we're done - reenable the pickup sensor
631                mPickupSensor.setDisabled(false);
632                mRegistered = false;
633            }
634            onProximityResult(result);
635            mFinished = true;
636        }
637
638        @Override
639        public void onAccuracyChanged(Sensor sensor, int accuracy) {
640            // noop
641        }
642    }
643}
644