blob: c02ebf75ef827e4a51e25e2fb0fc5f9954904df4 [file] [log] [blame]
/*
* 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) {}
}
}