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 mEarliestPulseDueToLight;
92     private int mScheduleResetsRemaining;
93 
DozeService()94     public DozeService() {
95         if (DEBUG) Log.d(mTag, "new DozeService()");
96         setDebug(DEBUG);
97     }
98 
99     @Override
dumpOnHandler(FileDescriptor fd, PrintWriter pw, String[] args)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
onCreate()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
onAttachedToWindow()148     public void onAttachedToWindow() {
149         if (DEBUG) Log.d(mTag, "onAttachedToWindow");
150         super.onAttachedToWindow();
151     }
152 
153     @Override
onDreamingStarted()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
onDreamingStopped()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 
requestPulse(final int reason)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 
continuePulsing(int reason)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 
turnDisplayOff()277     private void turnDisplayOff() {
278         if (DEBUG) Log.d(mTag, "Display off");
279         setDozeScreenState(Display.STATE_OFF);
280     }
281 
turnDisplayOn()282     private void turnDisplayOn() {
283         if (DEBUG) Log.d(mTag, "Display on");
284         setDozeScreenState(mDisplayStateSupported ? Display.STATE_DOZE : Display.STATE_ON);
285     }
286 
finishToSavePower()287     private void finishToSavePower() {
288         Log.w(mTag, "Exiting ambient mode due to low power battery saver");
289         finish();
290     }
291 
finishForCarMode()292     private void finishForCarMode() {
293         Log.w(mTag, "Exiting ambient mode, not allowed in car mode");
294         finish();
295     }
296 
listenForPulseSignals(boolean listen)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 
listenForBroadcasts(boolean listen)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 
listenForNotifications(boolean listen)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 
resetNotificationResets()335     private void resetNotificationResets() {
336         if (DEBUG) Log.d(mTag, "resetNotificationResets");
337         mScheduleResetsRemaining = mDozeParameters.getPulseScheduleResets();
338     }
339 
updateNotificationPulseDueToLight()340     private void updateNotificationPulseDueToLight() {
341         long timeMs = System.currentTimeMillis();
342         timeMs = Math.max(timeMs, mEarliestPulseDueToLight);
343         updateNotificationPulse(timeMs);
344     }
345 
updateNotificationPulse(long notificationTimeMs)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 
notificationPulseIntent(long instance)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 
rescheduleNotificationPulse(boolean predicate)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 
triggerEventToString(TriggerEvent event)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 
TriggerSensor(int type, boolean configured, boolean debugVibrate, int pulseReason)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 
setListening(boolean listen)488         public void setListening(boolean listen) {
489             if (mRequested == listen) return;
490             mRequested = listen;
491             updateListener();
492         }
493 
setDisabled(boolean disabled)494         public void setDisabled(boolean disabled) {
495             if (mDisabled == disabled) return;
496             mDisabled = disabled;
497             updateListener();
498         }
499 
updateListener()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
toString()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
onTrigger(TriggerEvent event)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