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());
+ }
+
+}