Home | History | Annotate | Download | only in incallui
      1 /*
      2  * Copyright (C) 2013 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.incallui;
     18 
     19 import android.content.Context;
     20 import android.hardware.display.DisplayManager;
     21 import android.hardware.display.DisplayManager.DisplayListener;
     22 import android.os.PowerManager;
     23 import android.support.annotation.NonNull;
     24 import android.telecom.CallAudioState;
     25 import android.view.Display;
     26 import com.android.dialer.common.LogUtil;
     27 import com.android.incallui.InCallPresenter.InCallState;
     28 import com.android.incallui.InCallPresenter.InCallStateListener;
     29 import com.android.incallui.audiomode.AudioModeProvider;
     30 import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener;
     31 import com.android.incallui.call.CallList;
     32 import com.android.incallui.call.DialerCall;
     33 
     34 /**
     35  * Class manages the proximity sensor for the in-call UI. We enable the proximity sensor while the
     36  * user in a phone call. The Proximity sensor turns off the touchscreen and display when the user is
     37  * close to the screen to prevent user's cheek from causing touch events. The class requires special
     38  * knowledge of the activity and device state to know when the proximity sensor should be enabled
     39  * and disabled. Most of that state is fed into this class through public methods.
     40  */
     41 public class ProximitySensor
     42     implements AccelerometerListener.OrientationListener, InCallStateListener, AudioModeListener {
     43 
     44   private static final String TAG = ProximitySensor.class.getSimpleName();
     45 
     46   private final PowerManager mPowerManager;
     47   private final PowerManager.WakeLock mProximityWakeLock;
     48   private final AudioModeProvider mAudioModeProvider;
     49   private final AccelerometerListener mAccelerometerListener;
     50   private final ProximityDisplayListener mDisplayListener;
     51   private int mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN;
     52   private boolean mUiShowing = false;
     53   private boolean mIsPhoneOffhook = false;
     54   private boolean mDialpadVisible;
     55   private boolean mIsAttemptingVideoCall;
     56   private boolean mIsVideoCall;
     57 
     58   public ProximitySensor(
     59       @NonNull Context context,
     60       @NonNull AudioModeProvider audioModeProvider,
     61       @NonNull AccelerometerListener accelerometerListener) {
     62     mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
     63     if (mPowerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
     64       mProximityWakeLock =
     65           mPowerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
     66     } else {
     67       LogUtil.i("ProximitySensor.constructor", "Device does not support proximity wake lock.");
     68       mProximityWakeLock = null;
     69     }
     70     mAccelerometerListener = accelerometerListener;
     71     mAccelerometerListener.setListener(this);
     72 
     73     mDisplayListener =
     74         new ProximityDisplayListener(
     75             (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE));
     76     mDisplayListener.register();
     77 
     78     mAudioModeProvider = audioModeProvider;
     79     mAudioModeProvider.addListener(this);
     80   }
     81 
     82   public void tearDown() {
     83     mAudioModeProvider.removeListener(this);
     84 
     85     mAccelerometerListener.enable(false);
     86     mDisplayListener.unregister();
     87 
     88     turnOffProximitySensor(true);
     89   }
     90 
     91   /** Called to identify when the device is laid down flat. */
     92   @Override
     93   public void orientationChanged(int orientation) {
     94     mOrientation = orientation;
     95     updateProximitySensorMode();
     96   }
     97 
     98   /** Called to keep track of the overall UI state. */
     99   @Override
    100   public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
    101     // We ignore incoming state because we do not want to enable proximity
    102     // sensor during incoming call screen. We check hasLiveCall() because a disconnected call
    103     // can also put the in-call screen in the INCALL state.
    104     boolean hasOngoingCall = InCallState.INCALL == newState && callList.hasLiveCall();
    105     boolean isOffhook = (InCallState.OUTGOING == newState) || hasOngoingCall;
    106 
    107     DialerCall activeCall = callList.getActiveCall();
    108     boolean isVideoCall = activeCall != null && activeCall.isVideoCall();
    109 
    110     if (isOffhook != mIsPhoneOffhook || mIsVideoCall != isVideoCall) {
    111       mIsPhoneOffhook = isOffhook;
    112       mIsVideoCall = isVideoCall;
    113 
    114       mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN;
    115       mAccelerometerListener.enable(mIsPhoneOffhook);
    116 
    117       updateProximitySensorMode();
    118     }
    119   }
    120 
    121   @Override
    122   public void onAudioStateChanged(CallAudioState audioState) {
    123     updateProximitySensorMode();
    124   }
    125 
    126   public void onDialpadVisible(boolean visible) {
    127     mDialpadVisible = visible;
    128     updateProximitySensorMode();
    129   }
    130 
    131   public void setIsAttemptingVideoCall(boolean isAttemptingVideoCall) {
    132     LogUtil.i(
    133         "ProximitySensor.setIsAttemptingVideoCall",
    134         "isAttemptingVideoCall: %b",
    135         isAttemptingVideoCall);
    136     mIsAttemptingVideoCall = isAttemptingVideoCall;
    137     updateProximitySensorMode();
    138   }
    139   /** Used to save when the UI goes in and out of the foreground. */
    140   public void onInCallShowing(boolean showing) {
    141     if (showing) {
    142       mUiShowing = true;
    143 
    144       // We only consider the UI not showing for instances where another app took the foreground.
    145       // If we stopped showing because the screen is off, we still consider that showing.
    146     } else if (mPowerManager.isScreenOn()) {
    147       mUiShowing = false;
    148     }
    149     updateProximitySensorMode();
    150   }
    151 
    152   void onDisplayStateChanged(boolean isDisplayOn) {
    153     LogUtil.i("ProximitySensor.onDisplayStateChanged", "isDisplayOn: %b", isDisplayOn);
    154     mAccelerometerListener.enable(isDisplayOn);
    155   }
    156 
    157   /**
    158    * TODO: There is no way to determine if a screen is off due to proximity or if it is legitimately
    159    * off, but if ever we can do that in the future, it would be useful here. Until then, this
    160    * function will simply return true of the screen is off. TODO: Investigate whether this can be
    161    * replaced with the ProximityDisplayListener.
    162    */
    163   public boolean isScreenReallyOff() {
    164     return !mPowerManager.isScreenOn();
    165   }
    166 
    167   private void turnOnProximitySensor() {
    168     if (mProximityWakeLock != null) {
    169       if (!mProximityWakeLock.isHeld()) {
    170         LogUtil.i("ProximitySensor.turnOnProximitySensor", "acquiring wake lock");
    171         mProximityWakeLock.acquire();
    172       } else {
    173         LogUtil.i("ProximitySensor.turnOnProximitySensor", "wake lock already acquired");
    174       }
    175     }
    176   }
    177 
    178   private void turnOffProximitySensor(boolean screenOnImmediately) {
    179     if (mProximityWakeLock != null) {
    180       if (mProximityWakeLock.isHeld()) {
    181         LogUtil.i("ProximitySensor.turnOffProximitySensor", "releasing wake lock");
    182         int flags = (screenOnImmediately ? 0 : PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
    183         mProximityWakeLock.release(flags);
    184       } else {
    185         LogUtil.i("ProximitySensor.turnOffProximitySensor", "wake lock already released");
    186       }
    187     }
    188   }
    189 
    190   /**
    191    * Updates the wake lock used to control proximity sensor behavior, based on the current state of
    192    * the phone.
    193    *
    194    * <p>On devices that have a proximity sensor, to avoid false touches during a call, we hold a
    195    * PROXIMITY_SCREEN_OFF_WAKE_LOCK wake lock whenever the phone is off hook. (When held, that wake
    196    * lock causes the screen to turn off automatically when the sensor detects an object close to the
    197    * screen.)
    198    *
    199    * <p>This method is a no-op for devices that don't have a proximity sensor.
    200    *
    201    * <p>Proximity wake lock will be released if any of the following conditions are true: the audio
    202    * is routed through bluetooth, a wired headset, or the speaker; the user requested, received a
    203    * request for, or is in a video call; or the phone is horizontal while in a call.
    204    */
    205   private synchronized void updateProximitySensorMode() {
    206     final int audioRoute = mAudioModeProvider.getAudioState().getRoute();
    207 
    208     boolean screenOnImmediately =
    209         (CallAudioState.ROUTE_WIRED_HEADSET == audioRoute
    210             || CallAudioState.ROUTE_SPEAKER == audioRoute
    211             || CallAudioState.ROUTE_BLUETOOTH == audioRoute
    212             || mIsAttemptingVideoCall
    213             || mIsVideoCall);
    214 
    215     // We do not keep the screen off when the user is outside in-call screen and we are
    216     // horizontal, but we do not force it on when we become horizontal until the
    217     // proximity sensor goes negative.
    218     final boolean horizontal = (mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL);
    219     screenOnImmediately |= !mUiShowing && horizontal;
    220 
    221     // We do not keep the screen off when dialpad is visible, we are horizontal, and
    222     // the in-call screen is being shown.
    223     // At that moment we're pretty sure users want to use it, instead of letting the
    224     // proximity sensor turn off the screen by their hands.
    225     screenOnImmediately |= mDialpadVisible && horizontal;
    226 
    227     LogUtil.i(
    228         "ProximitySensor.updateProximitySensorMode",
    229         "screenOnImmediately: %b, dialPadVisible: %b, "
    230             + "offHook: %b, horizontal: %b, uiShowing: %b, audioRoute: %s",
    231         screenOnImmediately,
    232         mDialpadVisible,
    233         mIsPhoneOffhook,
    234         mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL,
    235         mUiShowing,
    236         CallAudioState.audioRouteToString(audioRoute));
    237 
    238     if (mIsPhoneOffhook && !screenOnImmediately) {
    239       LogUtil.v("ProximitySensor.updateProximitySensorMode", "turning on proximity sensor");
    240       // Phone is in use!  Arrange for the screen to turn off
    241       // automatically when the sensor detects a close object.
    242       turnOnProximitySensor();
    243     } else {
    244       LogUtil.v("ProximitySensor.updateProximitySensorMode", "turning off proximity sensor");
    245       // Phone is either idle, or ringing.  We don't want any special proximity sensor
    246       // behavior in either case.
    247       turnOffProximitySensor(screenOnImmediately);
    248     }
    249   }
    250 
    251   /**
    252    * Implementation of a {@link DisplayListener} that maintains a binary state: Screen on vs screen
    253    * off. Used by the proximity sensor manager to decide whether or not it needs to listen to
    254    * accelerometer events.
    255    */
    256   public class ProximityDisplayListener implements DisplayListener {
    257 
    258     private DisplayManager mDisplayManager;
    259     private boolean mIsDisplayOn = true;
    260 
    261     ProximityDisplayListener(DisplayManager displayManager) {
    262       mDisplayManager = displayManager;
    263     }
    264 
    265     void register() {
    266       mDisplayManager.registerDisplayListener(this, null);
    267     }
    268 
    269     void unregister() {
    270       mDisplayManager.unregisterDisplayListener(this);
    271     }
    272 
    273     @Override
    274     public void onDisplayRemoved(int displayId) {}
    275 
    276     @Override
    277     public void onDisplayChanged(int displayId) {
    278       if (displayId == Display.DEFAULT_DISPLAY) {
    279         final Display display = mDisplayManager.getDisplay(displayId);
    280 
    281         final boolean isDisplayOn = display.getState() != Display.STATE_OFF;
    282         // For call purposes, we assume that as long as the screen is not truly off, it is
    283         // considered on, even if it is in an unknown or low power idle state.
    284         if (isDisplayOn != mIsDisplayOn) {
    285           mIsDisplayOn = isDisplayOn;
    286           onDisplayStateChanged(mIsDisplayOn);
    287         }
    288       }
    289     }
    290 
    291     @Override
    292     public void onDisplayAdded(int displayId) {}
    293   }
    294 }
    295