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