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 
ProximitySensor( @onNull Context context, @NonNull AudioModeProvider audioModeProvider, @NonNull AccelerometerListener accelerometerListener)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 
tearDown()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
orientationChanged(int orientation)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
onStateChange(InCallState oldState, InCallState newState, CallList callList)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
onAudioStateChanged(CallAudioState audioState)122   public void onAudioStateChanged(CallAudioState audioState) {
123     updateProximitySensorMode();
124   }
125 
onDialpadVisible(boolean visible)126   public void onDialpadVisible(boolean visible) {
127     mDialpadVisible = visible;
128     updateProximitySensorMode();
129   }
130 
setIsAttemptingVideoCall(boolean isAttemptingVideoCall)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. */
onInCallShowing(boolean showing)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 
onDisplayStateChanged(boolean isDisplayOn)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    */
isScreenReallyOff()163   public boolean isScreenReallyOff() {
164     return !mPowerManager.isScreenOn();
165   }
166 
turnOnProximitySensor()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 
turnOffProximitySensor(boolean screenOnImmediately)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    */
updateProximitySensorMode()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 
ProximityDisplayListener(DisplayManager displayManager)261     ProximityDisplayListener(DisplayManager displayManager) {
262       mDisplayManager = displayManager;
263     }
264 
register()265     void register() {
266       mDisplayManager.registerDisplayListener(this, null);
267     }
268 
unregister()269     void unregister() {
270       mDisplayManager.unregisterDisplayListener(this);
271     }
272 
273     @Override
onDisplayRemoved(int displayId)274     public void onDisplayRemoved(int displayId) {}
275 
276     @Override
onDisplayChanged(int displayId)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
onDisplayAdded(int displayId)292     public void onDisplayAdded(int displayId) {}
293   }
294 }
295