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
17package com.android.incallui;
18
19import com.google.common.base.Objects;
20
21import android.content.Context;
22import android.content.res.Configuration;
23import android.hardware.display.DisplayManager;
24import android.hardware.display.DisplayManager.DisplayListener;
25import android.os.PowerManager;
26import android.telecom.CallAudioState;
27import android.view.Display;
28
29import com.android.incallui.AudioModeProvider.AudioModeListener;
30import com.android.incallui.InCallPresenter.InCallState;
31import 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 */
42public 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
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
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
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
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
121    public void onSupportedAudioMode(int modeMask) {
122    }
123
124    @Override
125    public void onMute(boolean muted) {
126    }
127
128    /**
129     * Called when the audio mode changes during a call.
130     */
131    @Override
132    public void onAudioMode(int mode) {
133        updateProximitySensorMode();
134    }
135
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     */
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     */
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
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     */
177    public boolean isScreenReallyOff() {
178        return !mPowerManager.isScreenOn();
179    }
180
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
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     */
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
282        ProximityDisplayListener(DisplayManager displayManager) {
283            mDisplayManager = displayManager;
284        }
285
286        void register() {
287            mDisplayManager.registerDisplayListener(this, null);
288        }
289
290        void unregister() {
291            mDisplayManager.unregisterDisplayListener(this);
292        }
293
294        @Override
295        public void onDisplayRemoved(int displayId) {
296        }
297
298        @Override
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
314        public void onDisplayAdded(int displayId) {
315        }
316    }
317}
318