Home | History | Annotate | Download | only in doze
      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 
     94     public DozeService() {
     95         if (DEBUG) Log.d(mTag, "new DozeService()");
     96         setDebug(DEBUG);
     97     }
     98 
     99     @Override
    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
    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
    148     public void onAttachedToWindow() {
    149         if (DEBUG) Log.d(mTag, "onAttachedToWindow");
    150         super.onAttachedToWindow();
    151     }
    152 
    153     @Override
    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
    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 
    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 
    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 
    277     private void turnDisplayOff() {
    278         if (DEBUG) Log.d(mTag, "Display off");
    279         setDozeScreenState(Display.STATE_OFF);
    280     }
    281 
    282     private void turnDisplayOn() {
    283         if (DEBUG) Log.d(mTag, "Display on");
    284         setDozeScreenState(mDisplayStateSupported ? Display.STATE_DOZE : Display.STATE_ON);
    285     }
    286 
    287     private void finishToSavePower() {
    288         Log.w(mTag, "Exiting ambient mode due to low power battery saver");
    289         finish();
    290     }
    291 
    292     private void finishForCarMode() {
    293         Log.w(mTag, "Exiting ambient mode, not allowed in car mode");
    294         finish();
    295     }
    296 
    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 
    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 
    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 
    335     private void resetNotificationResets() {
    336         if (DEBUG) Log.d(mTag, "resetNotificationResets");
    337         mScheduleResetsRemaining = mDozeParameters.getPulseScheduleResets();
    338     }
    339 
    340     private void updateNotificationPulseDueToLight() {
    341         long timeMs = System.currentTimeMillis();
    342         timeMs = Math.max(timeMs, mEarliestPulseDueToLight);
    343         updateNotificationPulse(timeMs);
    344     }
    345 
    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 
    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 
    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 
    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 
    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 
    488         public void setListening(boolean listen) {
    489             if (mRequested == listen) return;
    490             mRequested = listen;
    491             updateListener();
    492         }
    493 
    494         public void setDisabled(boolean disabled) {
    495             if (mDisabled == disabled) return;
    496             mDisabled = disabled;
    497             updateListener();
    498         }
    499 
    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
    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
    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