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