| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * Copyright (C) 2023 The LineageOS Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.incallui; |
| |
| import android.content.Context; |
| import android.hardware.display.DisplayManager; |
| import android.hardware.display.DisplayManager.DisplayListener; |
| import android.os.PowerManager; |
| import android.os.Trace; |
| import android.telecom.CallAudioState; |
| import android.view.Display; |
| |
| import androidx.annotation.NonNull; |
| |
| import com.android.dialer.common.LogUtil; |
| import com.android.incallui.InCallPresenter.InCallState; |
| import com.android.incallui.InCallPresenter.InCallStateListener; |
| import com.android.incallui.audiomode.AudioModeProvider; |
| import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener; |
| import com.android.incallui.call.CallList; |
| import com.android.incallui.call.DialerCall; |
| |
| /** |
| * Class manages the proximity sensor for the in-call UI. We enable the proximity sensor while the |
| * user in a phone call. The Proximity sensor turns off the touchscreen and display when the user is |
| * close to the screen to prevent user's cheek from causing touch events. The class requires special |
| * knowledge of the activity and device state to know when the proximity sensor should be enabled |
| * and disabled. Most of that state is fed into this class through public methods. |
| */ |
| public class ProximitySensor |
| implements AccelerometerListener.OrientationListener, InCallStateListener, AudioModeListener { |
| |
| private static final String TAG = ProximitySensor.class.getSimpleName(); |
| |
| private final PowerManager powerManager; |
| private final PowerManager.WakeLock proximityWakeLock; |
| private final AudioModeProvider audioModeProvider; |
| private final AccelerometerListener accelerometerListener; |
| private final ProximityDisplayListener displayListener; |
| private int orientation = AccelerometerListener.ORIENTATION_UNKNOWN; |
| private boolean uiShowing = false; |
| private boolean isPhoneOffhook = false; |
| private boolean dialpadVisible; |
| private boolean isAttemptingVideoCall; |
| private boolean isVideoCall; |
| private boolean isRttCall; |
| |
| public ProximitySensor( |
| @NonNull Context context, |
| @NonNull AudioModeProvider audioModeProvider, |
| @NonNull AccelerometerListener accelerometerListener) { |
| Trace.beginSection("ProximitySensor.Constructor"); |
| powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); |
| if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { |
| proximityWakeLock = |
| powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); |
| } else { |
| LogUtil.i("ProximitySensor.constructor", "Device does not support proximity wake lock."); |
| proximityWakeLock = null; |
| } |
| this.accelerometerListener = accelerometerListener; |
| this.accelerometerListener.setListener(this); |
| |
| displayListener = |
| new ProximityDisplayListener( |
| (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE)); |
| displayListener.register(); |
| |
| this.audioModeProvider = audioModeProvider; |
| this.audioModeProvider.addListener(this); |
| Trace.endSection(); |
| } |
| |
| public void tearDown() { |
| audioModeProvider.removeListener(this); |
| |
| accelerometerListener.enable(false); |
| displayListener.unregister(); |
| |
| turnOffProximitySensor(true); |
| } |
| |
| /** Called to identify when the device is laid down flat. */ |
| @Override |
| public void orientationChanged(int orientation) { |
| this.orientation = orientation; |
| updateProximitySensorMode(); |
| } |
| |
| /** Called to keep track of the overall UI state. */ |
| @Override |
| public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { |
| // We ignore incoming state because we do not want to enable proximity |
| // sensor during incoming call screen. We check hasLiveCall() because a disconnected call |
| // can also put the in-call screen in the INCALL state. |
| boolean hasOngoingCall = InCallState.INCALL == newState && callList.hasLiveCall(); |
| boolean isOffhook = |
| InCallState.PENDING_OUTGOING == newState |
| || InCallState.OUTGOING == newState |
| || hasOngoingCall; |
| |
| DialerCall activeCall = callList.getActiveCall(); |
| boolean isVideoCall = activeCall != null && activeCall.isVideoCall(); |
| boolean isRttCall = activeCall != null && activeCall.isActiveRttCall(); |
| |
| if (isOffhook != isPhoneOffhook |
| || this.isVideoCall != isVideoCall |
| || this.isRttCall != isRttCall) { |
| isPhoneOffhook = isOffhook; |
| this.isVideoCall = isVideoCall; |
| this.isRttCall = isRttCall; |
| |
| orientation = AccelerometerListener.ORIENTATION_UNKNOWN; |
| accelerometerListener.enable(isPhoneOffhook); |
| |
| updateProximitySensorMode(); |
| } |
| } |
| |
| @Override |
| public void onAudioStateChanged(CallAudioState audioState) { |
| updateProximitySensorMode(); |
| } |
| |
| public void onDialpadVisible(boolean visible) { |
| dialpadVisible = visible; |
| updateProximitySensorMode(); |
| } |
| |
| public void setIsAttemptingVideoCall(boolean isAttemptingVideoCall) { |
| LogUtil.i( |
| "ProximitySensor.setIsAttemptingVideoCall", |
| "isAttemptingVideoCall: %b", |
| isAttemptingVideoCall); |
| this.isAttemptingVideoCall = isAttemptingVideoCall; |
| updateProximitySensorMode(); |
| } |
| /** Used to save when the UI goes in and out of the foreground. */ |
| public void onInCallShowing(boolean showing) { |
| if (showing) { |
| uiShowing = true; |
| |
| // We only consider the UI not showing for instances where another app took the foreground. |
| // If we stopped showing because the screen is off, we still consider that showing. |
| } else if (powerManager.isScreenOn()) { |
| uiShowing = false; |
| } |
| updateProximitySensorMode(); |
| } |
| |
| void onDisplayStateChanged(boolean isDisplayOn) { |
| LogUtil.i("ProximitySensor.onDisplayStateChanged", "isDisplayOn: %b", isDisplayOn); |
| accelerometerListener.enable(isDisplayOn); |
| } |
| |
| private void turnOnProximitySensor() { |
| if (proximityWakeLock != null) { |
| if (!proximityWakeLock.isHeld()) { |
| LogUtil.i("ProximitySensor.turnOnProximitySensor", "acquiring wake lock"); |
| proximityWakeLock.acquire(); |
| } else { |
| LogUtil.i("ProximitySensor.turnOnProximitySensor", "wake lock already acquired"); |
| } |
| } |
| } |
| |
| private void turnOffProximitySensor(boolean screenOnImmediately) { |
| if (proximityWakeLock != null) { |
| if (proximityWakeLock.isHeld()) { |
| LogUtil.i("ProximitySensor.turnOffProximitySensor", "releasing wake lock"); |
| int flags = (screenOnImmediately ? 0 : PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); |
| proximityWakeLock.release(flags); |
| } else { |
| LogUtil.i("ProximitySensor.turnOffProximitySensor", "wake lock already released"); |
| } |
| } |
| } |
| |
| /** |
| * Updates the wake lock used to control proximity sensor behavior, based on the current state of |
| * the phone. |
| * |
| * <p>On devices that have a proximity sensor, to avoid false touches during a call, we hold a |
| * PROXIMITY_SCREEN_OFF_WAKE_LOCK wake lock whenever the phone is off hook. (When held, that wake |
| * lock causes the screen to turn off automatically when the sensor detects an object close to the |
| * screen.) |
| * |
| * <p>This method is a no-op for devices that don't have a proximity sensor. |
| * |
| * <p>Proximity wake lock will be released if any of the following conditions are true: the audio |
| * is routed through bluetooth, a wired headset, or the speaker; the user requested, received a |
| * request for, or is in a video call; or the phone is horizontal while in a call. |
| */ |
| private synchronized void updateProximitySensorMode() { |
| Trace.beginSection("ProximitySensor.updateProximitySensorMode"); |
| final int audioRoute = audioModeProvider.getAudioState().getRoute(); |
| |
| boolean screenOnImmediately = |
| (CallAudioState.ROUTE_WIRED_HEADSET == audioRoute |
| || CallAudioState.ROUTE_SPEAKER == audioRoute |
| || CallAudioState.ROUTE_BLUETOOTH == audioRoute |
| || isAttemptingVideoCall |
| || isVideoCall |
| || isRttCall); |
| |
| // We do not keep the screen off when the user is outside in-call screen and we are |
| // horizontal, but we do not force it on when we become horizontal until the |
| // proximity sensor goes negative. |
| final boolean horizontal = (orientation == AccelerometerListener.ORIENTATION_HORIZONTAL); |
| screenOnImmediately |= !uiShowing && horizontal; |
| |
| // We do not keep the screen off when dialpad is visible, we are horizontal, and |
| // the in-call screen is being shown. |
| // At that moment we're pretty sure users want to use it, instead of letting the |
| // proximity sensor turn off the screen by their hands. |
| screenOnImmediately |= dialpadVisible && horizontal; |
| |
| LogUtil.i( |
| "ProximitySensor.updateProximitySensorMode", |
| "screenOnImmediately: %b, dialPadVisible: %b, " |
| + "offHook: %b, horizontal: %b, uiShowing: %b, audioRoute: %s", |
| screenOnImmediately, |
| dialpadVisible, |
| isPhoneOffhook, |
| orientation == AccelerometerListener.ORIENTATION_HORIZONTAL, |
| uiShowing, |
| CallAudioState.audioRouteToString(audioRoute)); |
| |
| if (isPhoneOffhook && !screenOnImmediately) { |
| LogUtil.v("ProximitySensor.updateProximitySensorMode", "turning on proximity sensor"); |
| // Phone is in use! Arrange for the screen to turn off |
| // automatically when the sensor detects a close object. |
| turnOnProximitySensor(); |
| } else { |
| LogUtil.v("ProximitySensor.updateProximitySensorMode", "turning off proximity sensor"); |
| // Phone is either idle, or ringing. We don't want any special proximity sensor |
| // behavior in either case. |
| turnOffProximitySensor(screenOnImmediately); |
| } |
| Trace.endSection(); |
| } |
| |
| /** |
| * Implementation of a {@link DisplayListener} that maintains a binary state: Screen on vs screen |
| * off. Used by the proximity sensor manager to decide whether or not it needs to listen to |
| * accelerometer events. |
| */ |
| public class ProximityDisplayListener implements DisplayListener { |
| |
| private final DisplayManager displayManager; |
| private boolean isDisplayOn = true; |
| |
| ProximityDisplayListener(DisplayManager displayManager) { |
| this.displayManager = displayManager; |
| } |
| |
| void register() { |
| displayManager.registerDisplayListener(this, null); |
| } |
| |
| void unregister() { |
| displayManager.unregisterDisplayListener(this); |
| } |
| |
| @Override |
| public void onDisplayRemoved(int displayId) {} |
| |
| @Override |
| public void onDisplayChanged(int displayId) { |
| if (displayId == Display.DEFAULT_DISPLAY) { |
| final Display display = displayManager.getDisplay(displayId); |
| |
| final boolean isDisplayOn = display.getState() != Display.STATE_OFF; |
| // For call purposes, we assume that as long as the screen is not truly off, it is |
| // considered on, even if it is in an unknown or low power idle state. |
| if (isDisplayOn != this.isDisplayOn) { |
| this.isDisplayOn = isDisplayOn; |
| onDisplayStateChanged(this.isDisplayOn); |
| } |
| } |
| } |
| |
| @Override |
| public void onDisplayAdded(int displayId) {} |
| } |
| } |