From f069d35b14a1fce9c110ac1e2edb4dfd89766232 Mon Sep 17 00:00:00 2001 From: jubb Date: Fri, 5 Nov 2021 14:36:25 +1100 Subject: [PATCH] feat: more commands handled, adding lock manager and bluetooth permissions --- app/src/main/AndroidManifest.xml | 1 + .../securesms/service/WebRtcCallService.kt | 68 +++++++- .../securesms/webrtc/AudioManagerCommand.kt | 3 +- .../securesms/webrtc/CallManager.kt | 140 ++++++++++----- .../securesms/webrtc/PeerConnectionWrapper.kt | 9 +- .../webrtc/audio/SignalAudioManager.kt | 21 +-- .../webrtc/locks/AccelerometerListener.java | 162 ++++++++++++++++++ .../securesms/webrtc/locks/LockManager.java | 148 ++++++++++++++++ .../securesms/webrtc/locks/ProximityLock.java | 50 ++++++ 9 files changed, 545 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/AccelerometerListener.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/LockManager.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/ProximityLock.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5dee3345a0..2a3a1b8616 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,6 +31,7 @@ android:required="false" /> + diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt index 858ebdcfec..a6ab506f79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt @@ -19,7 +19,6 @@ import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.calls.WebRtcCallActivity -import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.CallNotificationBuilder import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_ESTABLISHED import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_CONNECTING @@ -28,6 +27,8 @@ import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OU import org.thoughtcrime.securesms.webrtc.* import org.thoughtcrime.securesms.webrtc.CallManager.CallState.* import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger +import org.thoughtcrime.securesms.webrtc.locks.LockManager +import org.webrtc.SessionDescription import java.lang.AssertionError import java.util.* import java.util.concurrent.ExecutionException @@ -52,6 +53,7 @@ class WebRtcCallService: Service() { const val ACTION_SET_MUTE_AUDIO = "SET_MUTE_AUDIO" const val ACTION_SET_MUTE_VIDEO = "SET_MUTE_VIDEO" const val ACTION_FLIP_CAMERA = "FLIP_CAMERA" + const val ACTION_BLUETOOTH_CHANGE = "BLUETOOTH_CHANGE" const val ACTION_UPDATE_AUDIO = "UPDATE_AUDIO" const val ACTION_WIRED_HEADSET_CHANGE = "WIRED_HEADSET_CHANGE" const val ACTION_SCREEN_OFF = "SCREEN_OFF" @@ -107,6 +109,7 @@ class WebRtcCallService: Service() { private var lastNotificationId: Int = INVALID_NOTIFICATION_ID private var lastNotification: Notification? = null + private val lockManager by lazy { LockManager(this) } private val serviceExecutor = Executors.newSingleThreadExecutor() private val timeoutExecutor = Executors.newScheduledThreadPool(1) private val hangupOnCallAnswered = HangUpRtcOnPstnCallAnsweredListener { @@ -145,9 +148,9 @@ class WebRtcCallService: Service() { action == ACTION_REMOTE_HANGUP -> handleRemoteHangup(intent) action == ACTION_SET_MUTE_AUDIO -> handleSetMuteAudio(intent) action == ACTION_SET_MUTE_VIDEO -> handleSetMuteVideo(intent) - action == ACTION_FLIP_CAMERA -> handlesetCameraFlip(intent) -// action == ACTION_BLUETOOTH_CHANGE -> handleBluetoothChange(intent) -// action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChange(intent) + action == ACTION_FLIP_CAMERA -> handleSetCameraFlip(intent) + action == ACTION_BLUETOOTH_CHANGE -> handleBluetoothChange(intent) + action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChanged(intent) action == ACTION_SCREEN_OFF -> handleScreenOffChange(intent) action == ACTION_REMOTE_VIDEO_MUTE -> handleRemoteVideoMute(intent) action == ACTION_RESPONSE_MESSAGE -> handleResponseMessage(intent) @@ -369,9 +372,64 @@ class WebRtcCallService: Service() { private fun handleSetMuteVideo(intent: Intent) { val muted = intent.getBooleanExtra(EXTRA_MUTE, false) - callManager.handleSetMuteVideo(muted) + callManager.handleSetMuteVideo(muted, lockManager) } + private fun handleSetCameraFlip(intent: Intent) { + callManager.handleSetCameraFlip() + } + + private fun handleBluetoothChange(intent: Intent) { + val bluetoothAvailable = intent.getBooleanExtra(EXTRA_AVAILABLE, false) + callManager.postBluetoothAvailable(bluetoothAvailable) + } + + private fun handleWiredHeadsetChanged(intent: Intent) { + callManager.handleWiredHeadsetChanged(intent.getBooleanExtra(EXTRA_AVAILABLE, false)) + } + + private fun handleScreenOffChange(intent: Intent) { + callManager.handleScreenOffChange() + } + + private fun handleRemoteVideoMute(intent: Intent) { + val muted = intent.getBooleanExtra(EXTRA_MUTE, false) + val callId = intent.getSerializableExtra(EXTRA_CALL_ID) as UUID + + callManager.handleRemoteVideoMute(muted, callId) + } + + + private fun handleResponseMessage(intent: Intent) { + try { + val recipient = getRemoteRecipient(intent) + val callId = getCallId(intent) + val description = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) + callManager.handleResponseMessage(recipient, callId, SessionDescription(SessionDescription.Type.ANSWER, description)) + } catch (e: PeerConnectionException) { + terminate() + } + } + + private fun handleRemoteIceCandidate(intent: Intent) { + + } + + private fun handleLocalIceCandidate(intent: Intent) { + + } + + private fun handleCallConnected(intent: Intent) { + + } + + private fun handleIsInCallQuery(intent: Intent) { + + } + + + + private fun handleCheckTimeout(intent: Intent) { val callId = callManager.callId ?: return val callState = callManager.currentConnectionState diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioManagerCommand.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioManagerCommand.kt index 7bc0d0bd5e..fe6021ed9b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioManagerCommand.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioManagerCommand.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.webrtc import android.os.Parcelable import kotlinx.android.parcel.Parcelize +import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager @Parcelize @@ -10,7 +11,7 @@ open class AudioManagerCommand: Parcelable { object Initialize: AudioManagerCommand() @Parcelize - object StartOutgoingRinger: AudioManagerCommand() + data class StartOutgoingRinger(val type: OutgoingRinger.Type): AudioManagerCommand() @Parcelize object SilenceIncomingRinger: AudioManagerCommand() diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index 0b45b90dc7..e49dad9bce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.telephony.TelephonyManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import nl.komponents.kovenant.Promise import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.sending_receiving.MessageSender @@ -15,10 +17,13 @@ import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager +import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice +import org.thoughtcrime.securesms.webrtc.locks.LockManager import org.thoughtcrime.securesms.webrtc.video.CameraEventListener import org.thoughtcrime.securesms.webrtc.video.CameraState import org.webrtc.* import java.lang.NullPointerException +import java.nio.ByteBuffer import java.util.* import java.util.concurrent.Executors @@ -37,6 +42,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne } companion object { + + val VIDEO_DISABLED_JSON by lazy { buildJsonObject { put("video", false) } } + val VIDEO_ENABLED_JSON by lazy { buildJsonObject { put("video", true) } } + private val TAG = Log.tag(CallManager::class.java) val CONNECTED_STATES = arrayOf(CallState.STATE_CONNECTED) val PENDING_CONNECTION_STATES = arrayOf( @@ -50,11 +59,9 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne CallState.STATE_REMOTE_RINGING, CallState.STATE_CONNECTED ) - val DISCONNECTED_STATES = arrayOf(CallState.STATE_IDLE) private const val DATA_CHANNEL_NAME = "signaling" } - private val signalAudioManager: SignalAudioManager = SignalAudioManager(context, this, audioManager) private val _audioEvents = MutableStateFlow(StateEvent.AudioEnabled(false)) @@ -97,11 +104,11 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne } fun initializeAudioForCall() { - signalAudioManager.initializeAudioForCall() + signalAudioManager.handleCommand(AudioManagerCommand.Initialize) } fun startOutgoingRinger(ringerType: OutgoingRinger.Type) { - signalAudioManager.startOutgoingRinger(ringerType) + signalAudioManager.handleCommand(AudioManagerCommand.StartOutgoingRinger(ringerType)) } fun postConnectionEvent(newState: CallState) { @@ -112,36 +119,6 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne _callStateEvents.value = newState } - private fun createCameraCapturer(enumerator: CameraEnumerator): CameraVideoCapturer? { - val deviceNames = enumerator.deviceNames - - // First, try to find front facing camera - Log.d("Loki-RTC-vid", "Looking for front facing cameras.") - for (deviceName in deviceNames) { - if (enumerator.isFrontFacing(deviceName)) { - Log.d("Loki-RTC-vid", "Creating front facing camera capturer.") - val videoCapturer = enumerator.createCapturer(deviceName, null) - if (videoCapturer != null) { - return videoCapturer - } - } - } - - // Front facing camera not found, try something else - Log.d("Loki-RTC-vid", "Looking for other cameras.") - for (deviceName in deviceNames) { - if (!enumerator.isFrontFacing(deviceName)) { - Log.d("Loki-RTC-vid", "Creating other camera capturer.") - val videoCapturer = enumerator.createCapturer(deviceName, null) - if (videoCapturer != null) { - return videoCapturer - } - } - } - - return null - } - override fun newCallMessage(callMessage: SignalServiceProtos.CallMessage) { } @@ -262,7 +239,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne TODO("interpret the data channel buffer and check for signals") } - override fun onAudioDeviceChanged(activeDevice: SignalAudioManager.AudioDevice, devices: Set) { + override fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set) { signalAudioManager.handleCommand(AudioManagerCommand()) } @@ -279,7 +256,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne } fun stop() { - signalAudioManager.stop(currentConnectionState in OUTGOING_STATES) + signalAudioManager.handleCommand(AudioManagerCommand.Stop(currentConnectionState in OUTGOING_STATES)) peerConnection?.dispose() peerConnection = null @@ -295,8 +272,8 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne localCameraState = CameraState.UNKNOWN recipient = null callId = null - microphoneEnabled = true - remoteVideoEnabled = false + _audioEvents.value = StateEvent.AudioEnabled(false) + _videoEvents.value = StateEvent.VideoEnabled(false) pendingOutgoingIceUpdates.clear() pendingIncomingIceUpdates.clear() } @@ -410,10 +387,93 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne peerConnection?.setAudioEnabled(_audioEvents.value.isEnabled) } - fun handleSetMuteVideo(muted: Boolean) { + fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) { _videoEvents.value = StateEvent.VideoEnabled(!muted) peerConnection?.setVideoEnabled(_videoEvents.value.isEnabled) - TODO() + dataChannel?.let { channel -> + val toSend = if (muted) VIDEO_DISABLED_JSON else VIDEO_ENABLED_JSON + val buffer = DataChannel.Buffer(ByteBuffer.wrap(toSend.toString().encodeToByteArray()), false) + channel.send(buffer) + } + + if (currentConnectionState == CallState.STATE_CONNECTED) { + if (localCameraState.enabled) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO) + else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL) + } + + if (localCameraState.enabled + && !signalAudioManager.isSpeakerphoneOn() + && !signalAudioManager.isBluetoothScoOn() + && !signalAudioManager.isWiredHeadsetOn() + ) { + signalAudioManager.handleCommand(AudioManagerCommand.SetUserDevice(AudioDevice.SPEAKER_PHONE)) + } + } + + fun handleSetCameraFlip() { + if (!localCameraState.enabled) return + peerConnection?.let { connection -> + connection.flipCamera() + localCameraState = connection.getCameraState() + } + } + + fun postBluetoothAvailable(available: Boolean) { + // TODO: _bluetoothEnabled.value = available + } + + fun handleWiredHeadsetChanged(present: Boolean) { + if (currentConnectionState in arrayOf(CallState.STATE_CONNECTED, + CallState.STATE_DIALING, + CallState.STATE_REMOTE_RINGING)) { + if (present && signalAudioManager.isSpeakerphoneOn()) { + signalAudioManager.handleCommand(AudioManagerCommand.SetUserDevice(AudioDevice.WIRED_HEADSET)) + } else if (!present && !signalAudioManager.isSpeakerphoneOn() && !signalAudioManager.isBluetoothScoOn() && localCameraState.enabled) { + signalAudioManager.handleCommand(AudioManagerCommand.SetUserDevice(AudioDevice.SPEAKER_PHONE)) + } + } + } + + fun handleScreenOffChange() { + if (currentConnectionState in arrayOf(CallState.STATE_ANSWERING, CallState.STATE_LOCAL_RINGING)) { + signalAudioManager.handleCommand(AudioManagerCommand.SilenceIncomingRinger) + } + } + + fun handleRemoteVideoMute(muted: Boolean, intentCallId: UUID) { + val recipient = recipient ?: return + val callId = callId ?: return + if (currentConnectionState != CallState.STATE_CONNECTED || callId != intentCallId) { + Log.w(TAG,"Got video toggle for inactive call, ignoring..") + return + } + + _remoteVideoEvents.value = StateEvent.VideoEnabled(!muted) + } + + fun handleResponseMessage(recipient: Recipient, callId: UUID, answer: SessionDescription) { + if (currentConnectionState != CallState.STATE_DIALING || recipient != this.recipient || callId != this.callId) { + Log.w(TAG,"Got answer for recipient and call ID we're not currently dialing") + return + } + + val connection = peerConnection ?: throw AssertionError("assert") + + connection.setRemoteDescription(answer) + } + + fun handleRemoteIceCandidate(iceCandidates: List, callId: UUID) { + if (callId != this.callId) { + Log.w(TAG, "Got remote ice candidates for a call that isn't active") + } + + peerConnection?.let { connection -> + iceCandidates.forEach { candidate -> + connection.addIceCandidate(candidate) + } + } ?: run { + pendingIncomingIceUpdates.addAll(iceCandidates) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt index e29ba9cbd6..78d3fa6b70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt @@ -81,7 +81,10 @@ class PeerConnectionWrapper(context: Context, } fun createDataChannel(channelName: String): DataChannel { - + val dataChannelConfiguration = DataChannel.Init().apply { + ordered = true + } + return peerConnection.createDataChannel(channelName, dataChannelConfiguration) } fun addIceCandidate(candidate: IceCandidate) { @@ -228,4 +231,8 @@ class PeerConnectionWrapper(context: Context, } } + fun flipCamera() { + camera.flip() + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt index c7216e3fcf..342725c552 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt @@ -75,7 +75,7 @@ class SignalAudioManager(private val context: Context, is AudioManagerCommand.SetUserDevice -> selectAudioDevice(command.device) is AudioManagerCommand.StartIncomingRinger -> startIncomingRinger(command.vibrate) is AudioManagerCommand.SilenceIncomingRinger -> silenceIncomingRinger() - is AudioManagerCommand.StartOutgoingRinger -> startOutgoingRinger() + is AudioManagerCommand.StartOutgoingRinger -> startOutgoingRinger(command.type) } } } @@ -128,7 +128,7 @@ class SignalAudioManager(private val context: Context, Log.d(TAG, "Started") } - fun stop(playDisconnect: Boolean) { + private fun stop(playDisconnect: Boolean) { Log.d(TAG, "Stopping. state: $state") if (state == State.UNINITIALIZED) { Log.i(TAG, "Trying to stop AudioManager in incorrect state: $state") @@ -166,7 +166,7 @@ class SignalAudioManager(private val context: Context, Log.d(TAG, "Stopped") } - fun shutdown() { + private fun shutdown() { handler.post { stop(false) if (commandAndControlThread != null) { @@ -177,7 +177,7 @@ class SignalAudioManager(private val context: Context, } } - fun updateAudioDeviceState() { + private fun updateAudioDeviceState() { handler.assertHandlerThread() Log.i( @@ -342,13 +342,13 @@ class SignalAudioManager(private val context: Context, incomingRinger.stop() } - fun startOutgoingRinger(type: OutgoingRinger.Type) { + private fun startOutgoingRinger(type: OutgoingRinger.Type) { Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice") androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION setMicrophoneMute(false) - outgoingRinger.start(OutgoingRinger.Type.RINGING) + outgoingRinger.start(type) } private fun onWiredHeadsetChange(pluggedIn: Boolean, hasMic: Boolean) { @@ -357,10 +357,11 @@ class SignalAudioManager(private val context: Context, updateAudioDeviceState() } - fun initializeAudioForCall() { - val audioManager: AudioManager = ServiceUtil.getAudioManager(context) - audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) - } + fun isSpeakerphoneOn(): Boolean = androidAudioManager.isSpeakerphoneOn + + fun isBluetoothScoOn(): Boolean = androidAudioManager.isBluetoothScoOn + + fun isWiredHeadsetOn(): Boolean = androidAudioManager.isWiredHeadsetOn private inner class WiredHeadsetReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/AccelerometerListener.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/AccelerometerListener.java new file mode 100644 index 0000000000..39afab0cb2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/AccelerometerListener.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2009 The Android Open Source 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 org.thoughtcrime.securesms.webrtc.locks; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Handler; +import android.os.Message; + +import org.session.libsignal.utilities.Log; + +/** + * This class is used to listen to the accelerometer to monitor the + * orientation of the phone. The client of this class is notified when + * the orientation changes between horizontal and vertical. + */ +public final class AccelerometerListener { + private static final String TAG = "AccelerometerListener"; + private static final boolean DEBUG = true; + private static final boolean VDEBUG = false; + + private SensorManager mSensorManager; + private Sensor mSensor; + + // mOrientation is the orientation value most recently reported to the client. + private int mOrientation; + + // mPendingOrientation is the latest orientation computed based on the sensor value. + // This is sent to the client after a rebounce delay, at which point it is copied to + // mOrientation. + private int mPendingOrientation; + + private OrientationListener mListener; + + // Device orientation + public static final int ORIENTATION_UNKNOWN = 0; + public static final int ORIENTATION_VERTICAL = 1; + public static final int ORIENTATION_HORIZONTAL = 2; + + private static final int ORIENTATION_CHANGED = 1234; + + private static final int VERTICAL_DEBOUNCE = 100; + private static final int HORIZONTAL_DEBOUNCE = 500; + private static final double VERTICAL_ANGLE = 50.0; + + public interface OrientationListener { + public void orientationChanged(int orientation); + } + + public AccelerometerListener(Context context, OrientationListener listener) { + mListener = listener; + mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + + public void enable(boolean enable) { + if (DEBUG) Log.d(TAG, "enable(" + enable + ")"); + synchronized (this) { + if (enable) { + mOrientation = ORIENTATION_UNKNOWN; + mPendingOrientation = ORIENTATION_UNKNOWN; + mSensorManager.registerListener(mSensorListener, mSensor, + SensorManager.SENSOR_DELAY_NORMAL); + } else { + mSensorManager.unregisterListener(mSensorListener); + mHandler.removeMessages(ORIENTATION_CHANGED); + } + } + } + + private void setOrientation(int orientation) { + synchronized (this) { + if (mPendingOrientation == orientation) { + // Pending orientation has not changed, so do nothing. + return; + } + + // Cancel any pending messages. + // We will either start a new timer or cancel alltogether + // if the orientation has not changed. + mHandler.removeMessages(ORIENTATION_CHANGED); + + if (mOrientation != orientation) { + // Set timer to send an event if the orientation has changed since its + // previously reported value. + mPendingOrientation = orientation; + Message m = mHandler.obtainMessage(ORIENTATION_CHANGED); + // set delay to our debounce timeout + int delay = (orientation == ORIENTATION_VERTICAL ? VERTICAL_DEBOUNCE + : HORIZONTAL_DEBOUNCE); + mHandler.sendMessageDelayed(m, delay); + } else { + // no message is pending + mPendingOrientation = ORIENTATION_UNKNOWN; + } + } + } + + private void onSensorEvent(double x, double y, double z) { + if (VDEBUG) Log.d(TAG, "onSensorEvent(" + x + ", " + y + ", " + z + ")"); + + // If some values are exactly zero, then likely the sensor is not powered up yet. + // ignore these events to avoid false horizontal positives. + if (x == 0.0 || y == 0.0 || z == 0.0) return; + + // magnitude of the acceleration vector projected onto XY plane + double xy = Math.sqrt(x * x + y * y); + // compute the vertical angle + double angle = Math.atan2(xy, z); + // convert to degrees + angle = angle * 180.0 / Math.PI; + int orientation = (angle > VERTICAL_ANGLE ? ORIENTATION_VERTICAL : ORIENTATION_HORIZONTAL); + if (VDEBUG) Log.d(TAG, "angle: " + angle + " orientation: " + orientation); + setOrientation(orientation); + } + + SensorEventListener mSensorListener = new SensorEventListener() { + public void onSensorChanged(SensorEvent event) { + onSensorEvent(event.values[0], event.values[1], event.values[2]); + } + + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // ignore + } + }; + + Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case ORIENTATION_CHANGED: + synchronized (this) { + mOrientation = mPendingOrientation; + if (DEBUG) { + Log.d(TAG, "orientation: " + + (mOrientation == ORIENTATION_HORIZONTAL ? "horizontal" + : (mOrientation == ORIENTATION_VERTICAL ? "vertical" + : "unknown"))); + } + mListener.orientationChanged(mOrientation); + } + break; + } + } + }; +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/LockManager.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/LockManager.java new file mode 100644 index 0000000000..a7fac62bbc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/LockManager.java @@ -0,0 +1,148 @@ +package org.thoughtcrime.securesms.webrtc.locks; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.os.PowerManager; +import android.provider.Settings; + +import org.session.libsignal.utilities.Log; + +/** + * Maintains wake lock state. + * + * @author Stuart O. Anderson + */ +public class LockManager { + + private static final String TAG = LockManager.class.getSimpleName(); + + private final PowerManager.WakeLock fullLock; + private final PowerManager.WakeLock partialLock; + private final WifiManager.WifiLock wifiLock; + private final ProximityLock proximityLock; + + private final AccelerometerListener accelerometerListener; + private final boolean wifiLockEnforced; + + + private int orientation = AccelerometerListener.ORIENTATION_UNKNOWN; + private boolean proximityDisabled = false; + + public enum PhoneState { + IDLE, + PROCESSING, //used when the phone is active but before the user should be alerted. + INTERACTIVE, + IN_CALL, + IN_VIDEO + } + + private enum LockState { + FULL, + PARTIAL, + SLEEP, + PROXIMITY + } + + public LockManager(Context context) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + fullLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "signal:full"); + partialLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "signal:partial"); + proximityLock = new ProximityLock(pm); + + WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "signal:wifi"); + + fullLock.setReferenceCounted(false); + partialLock.setReferenceCounted(false); + wifiLock.setReferenceCounted(false); + + accelerometerListener = new AccelerometerListener(context, new AccelerometerListener.OrientationListener() { + @Override + public void orientationChanged(int newOrientation) { + orientation = newOrientation; + Log.d(TAG, "Orentation Update: " + newOrientation); + updateInCallLockState(); + } + }); + + wifiLockEnforced = isWifiPowerActiveModeEnabled(context); + } + + private boolean isWifiPowerActiveModeEnabled(Context context) { + int wifi_pwr_active_mode = Settings.Secure.getInt(context.getContentResolver(), "wifi_pwr_active_mode", -1); + Log.d(TAG, "Wifi Activity Policy: " + wifi_pwr_active_mode); + + if (wifi_pwr_active_mode == 0) { + return false; + } + + return true; + } + + private void updateInCallLockState() { + if (orientation != AccelerometerListener.ORIENTATION_HORIZONTAL && wifiLockEnforced && !proximityDisabled) { + setLockState(LockState.PROXIMITY); + } else { + setLockState(LockState.FULL); + } + } + + public void updatePhoneState(PhoneState state) { + switch(state) { + case IDLE: + setLockState(LockState.SLEEP); + accelerometerListener.enable(false); + break; + case PROCESSING: + setLockState(LockState.PARTIAL); + accelerometerListener.enable(false); + break; + case INTERACTIVE: + setLockState(LockState.FULL); + accelerometerListener.enable(false); + break; + case IN_VIDEO: + proximityDisabled = true; + accelerometerListener.enable(false); + updateInCallLockState(); + break; + case IN_CALL: + proximityDisabled = false; + accelerometerListener.enable(true); + updateInCallLockState(); + break; + } + } + + private synchronized void setLockState(LockState newState) { + switch(newState) { + case FULL: + fullLock.acquire(); + partialLock.acquire(); + wifiLock.acquire(); + proximityLock.release(); + break; + case PARTIAL: + partialLock.acquire(); + wifiLock.acquire(); + fullLock.release(); + proximityLock.release(); + break; + case SLEEP: + fullLock.release(); + partialLock.release(); + wifiLock.release(); + proximityLock.release(); + break; + case PROXIMITY: + partialLock.acquire(); + proximityLock.acquire(); + wifiLock.acquire(); + fullLock.release(); + break; + default: + throw new IllegalArgumentException("Unhandled Mode: " + newState); + } + Log.d(TAG, "Entered Lock State: " + newState); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/ProximityLock.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/ProximityLock.java new file mode 100644 index 0000000000..ab91437c7d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/ProximityLock.java @@ -0,0 +1,50 @@ +package org.thoughtcrime.securesms.webrtc.locks; + +import android.os.PowerManager; + +import org.session.libsignal.utilities.Log; +import org.session.libsignal.utilities.guava.Optional; + +/** + * Controls access to the proximity lock. + * The proximity lock is not part of the public API. + * + * @author Stuart O. Anderson + */ +class ProximityLock { + + private static final String TAG = ProximityLock.class.getSimpleName(); + + private final Optional proximityLock; + + ProximityLock(PowerManager pm) { + proximityLock = getProximityLock(pm); + } + + private Optional getProximityLock(PowerManager pm) { + if (pm.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { + return Optional.fromNullable(pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "signal:proximity")); + } else { + return Optional.absent(); + } + } + + public void acquire() { + if (!proximityLock.isPresent() || proximityLock.get().isHeld()) { + return; + } + + proximityLock.get().acquire(); + } + + public void release() { + if (!proximityLock.isPresent() || !proximityLock.get().isHeld()) { + return; + } + + proximityLock.get().release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); + + Log.d(TAG, "Released proximity lock:" + proximityLock.get().isHeld()); + } + +}