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 
17 package com.android.systemui.doze;
18 
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;
41 
42 import com.android.systemui.SystemUIApplication;
43 import com.android.systemui.statusbar.phone.DozeParameters;
44 import com.android.systemui.statusbar.phone.DozeParameters.PulseSchedule;
45 
46 import java.io.FileDescriptor;
47 import java.io.PrintWriter;
48 import java.util.Date;
49 
50 public 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 
DozeService()95     public DozeService() {
96         if (DEBUG) Log.d(mTag, "new DozeService()");
97         setDebug(DEBUG);
98     }
99 
100     @Override
dumpOnHandler(FileDescriptor fd, PrintWriter pw, String[] args)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
onCreate()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
onAttachedToWindow()149     public void onAttachedToWindow() {
150         if (DEBUG) Log.d(mTag, "onAttachedToWindow");
151         super.onAttachedToWindow();
152     }
153 
154     @Override
onDreamingStarted()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
onDreamingStopped()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 
requestPulse(final int reason)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 
continuePulsing(int reason)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 
turnDisplayOff()283     private void turnDisplayOff() {
284         if (DEBUG) Log.d(mTag, "Display off");
285         setDozeScreenState(Display.STATE_OFF);
286     }
287 
turnDisplayOn()288     private void turnDisplayOn() {
289         if (DEBUG) Log.d(mTag, "Display on");
290         setDozeScreenState(mDisplayStateSupported ? Display.STATE_DOZE : Display.STATE_ON);
291     }
292 
finishToSavePower()293     private void finishToSavePower() {
294         Log.w(mTag, "Exiting ambient mode due to low power battery saver");
295         finish();
296     }
297 
finishForCarMode()298     private void finishForCarMode() {
299         Log.w(mTag, "Exiting ambient mode, not allowed in car mode");
300         finish();
301     }
302 
listenForPulseSignals(boolean listen)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 
listenForBroadcasts(boolean listen)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 
listenForNotifications(boolean listen)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 
resetNotificationResets()341     private void resetNotificationResets() {
342         if (DEBUG) Log.d(mTag, "resetNotificationResets");
343         mScheduleResetsRemaining = mDozeParameters.getPulseScheduleResets();
344     }
345 
updateNotificationPulseDueToLight()346     private void updateNotificationPulseDueToLight() {
347         long timeMs = System.currentTimeMillis();
348         timeMs = Math.max(timeMs, mEarliestPulseDueToLight);
349         updateNotificationPulse(timeMs);
350     }
351 
updateNotificationPulse(long notificationTimeMs)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 
notificationPulseIntent(long instance)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 
rescheduleNotificationPulse(boolean predicate)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 
triggerEventToString(TriggerEvent event)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 
TriggerSensor(int type, boolean configured, boolean debugVibrate, int pulseReason)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 
setListening(boolean listen)502         public void setListening(boolean listen) {
503             if (mRequested == listen) return;
504             mRequested = listen;
505             updateListener();
506         }
507 
setDisabled(boolean disabled)508         public void setDisabled(boolean disabled) {
509             if (mDisabled == disabled) return;
510             mDisabled = disabled;
511             updateListener();
512         }
513 
updateListener()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
toString()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
onTrigger(TriggerEvent event)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