mobile: call settings, request camera on iOS on call start (#701)

* mobile: call settings, request camera on iOS on call start

* refactor preferences

* fix typo
This commit is contained in:
Evgeny Poberezkin
2022-05-27 16:36:33 +01:00
committed by GitHub
parent 79d9e90ab7
commit da13e6614b
28 changed files with 686 additions and 513 deletions
+5
View File
@@ -1,5 +1,8 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<ComposeCustomCodeStyleSettings>
<option name="USE_CUSTOM_FORMATTING_FOR_MODIFIERS" value="false" />
</ComposeCustomCodeStyleSettings>
<JetCodeStyleSettings>
<option name="SPACE_BEFORE_EXTEND_COLON" value="false" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="3" />
@@ -123,9 +126,11 @@
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="RIGHT_MARGIN" value="140" />
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="0" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="0" />
<option name="METHOD_PARAMETERS_WRAP" value="0" />
<option name="EXTENDS_LIST_WRAP" value="0" />
-1
View File
@@ -1,6 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>
+13 -5
View File
@@ -96,8 +96,7 @@ const processCommand = (function () {
const pc = new RTCPeerConnection(config.peerConnectionConfig);
const remoteStream = new MediaStream();
const localCamera = VideoCamera.User;
const constraints = callMediaConstraints(mediaType, localCamera);
const localStream = await navigator.mediaDevices.getUserMedia(constraints);
const localStream = await getLocalMediaStream(mediaType, localCamera);
const iceCandidates = getIceCandidates(pc, config);
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
await setupMediaStreams(call);
@@ -156,11 +155,17 @@ const processCommand = (function () {
try {
switch (command.type) {
case "capabilities":
console.log("starting outgoing call - capabilities");
if (activeCall)
endCall();
// This request for local media stream is made to prompt for camera/mic permissions on call start
if (command.media)
await getLocalMediaStream(command.media, VideoCamera.User);
const encryption = supportsInsertableStreams(command.useWorker);
resp = { type: "capabilities", capabilities: { encryption } };
break;
case "start": {
console.log("starting call");
console.log("starting incoming call - create webrtc session");
if (activeCall)
endCall();
const { media, useWorker, iceServers, relay } = command;
@@ -394,8 +399,7 @@ const processCommand = (function () {
for (const t of call.localStream.getTracks())
t.stop();
call.localCamera = camera;
const constraints = callMediaConstraints(call.localMedia, camera);
const localStream = await navigator.mediaDevices.getUserMedia(constraints);
const localStream = await getLocalMediaStream(call.localMedia, camera);
replaceTracks(pc, localStream.getVideoTracks());
replaceTracks(pc, localStream.getAudioTracks());
call.localStream = localStream;
@@ -430,6 +434,10 @@ const processCommand = (function () {
console.log(`no ${operation}`);
}
}
function getLocalMediaStream(mediaType, facingMode) {
const constraints = callMediaConstraints(mediaType, facingMode);
return navigator.mediaDevices.getUserMedia(constraints);
}
function callMediaConstraints(mediaType, facingMode) {
switch (mediaType) {
case CallMediaType.Audio:
@@ -72,7 +72,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
val m = vm.chatModel
val lastLAVal = lastLA.value
if (
m.controller.getPerformLA()
m.controller.prefPerformLA.get()
&& (lastLAVal == null || (System.nanoTime() - lastLAVal >= 30 * 1e+9))
) {
userAuthorized.value = false
@@ -91,7 +91,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
LAResult.Unavailable -> {
userAuthorized.value = true
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
laUnavailableTurningOffAlert()
}
}
@@ -106,13 +106,13 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
}
private fun schedulePeriodicServiceRestartWorker() {
val workerVersion = chatController.getAutoRestartWorkerVersion()
val workerVersion = chatController.prefAutoRestartWorkerVersion.get()
val workPolicy = if (workerVersion == SimplexService.SERVICE_START_WORKER_VERSION) {
Log.d(TAG, "ServiceStartWorker version matches: choosing KEEP as existing work policy")
ExistingPeriodicWorkPolicy.KEEP
} else {
Log.d(TAG, "ServiceStartWorker version DOES NOT MATCH: choosing REPLACE as existing work policy")
chatController.setAutoRestartWorkerVersion(SimplexService.SERVICE_START_WORKER_VERSION)
chatController.prefAutoRestartWorkerVersion.set(SimplexService.SERVICE_START_WORKER_VERSION)
ExistingPeriodicWorkPolicy.REPLACE
}
val work = PeriodicWorkRequestBuilder<SimplexService.ServiceStartWorker>(SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES)
@@ -126,7 +126,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
private fun setPerformLA(on: Boolean) {
val m = vm.chatModel
if (on) {
m.controller.setLANoticeShown(true)
m.controller.prefLANoticeShown.set(true)
authenticate(
generalGetString(R.string.auth_enable),
generalGetString(R.string.auth_confirm_credential),
@@ -135,24 +135,24 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
when (laResult) {
LAResult.Success -> {
m.performLA.value = true
m.controller.setPerformLA(true)
m.controller.prefPerformLA.set(true)
userAuthorized.value = true
lastLA.value = System.nanoTime()
laTurnedOnAlert()
}
is LAResult.Error -> {
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
laErrorToast(applicationContext, laResult.errString)
}
LAResult.Failed -> {
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
laFailedToast(applicationContext)
}
LAResult.Unavailable -> {
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
laUnavailableInstructionAlert()
}
}
@@ -167,21 +167,21 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
when (laResult) {
LAResult.Success -> {
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
}
is LAResult.Error -> {
m.performLA.value = true
m.controller.setPerformLA(true)
m.controller.prefPerformLA.set(true)
laErrorToast(applicationContext, laResult.errString)
}
LAResult.Failed -> {
m.performLA.value = true
m.controller.setPerformLA(true)
m.controller.prefPerformLA.set(true)
laFailedToast(applicationContext)
}
LAResult.Unavailable -> {
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
laUnavailableTurningOffAlert()
}
}
@@ -212,7 +212,7 @@ fun MainPage(
var showAdvertiseLAAlert by remember { mutableStateOf(false) }
LaunchedEffect(showAdvertiseLAAlert) {
if (
!chatModel.controller.getLANoticeShown()
!chatModel.controller.prefLANoticeShown.get()
&& showAdvertiseLAAlert
&& chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete
&& chatModel.chats.isNotEmpty()
@@ -61,7 +61,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
withApi {
when (event) {
Lifecycle.Event.ON_STOP ->
if (!chatController.getRunServiceInBackground()) SimplexService.stop(applicationContext)
if (!chatController.prefRunServiceInBackground.get()) SimplexService.stop(applicationContext)
Lifecycle.Event.ON_START ->
SimplexService.start(applicationContext)
Lifecycle.Event.ON_RESUME ->
@@ -50,9 +50,18 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
var chatModel = ChatModel(this)
private val sharedPreferences: SharedPreferences = appContext.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
val prefRunServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true)
val prefBackgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false)
private val prefBackgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false)
val prefAutoRestartWorkerVersion = mkIntPreference(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0)
val prefWebrtcPolicyRelay = mkBoolPreference(SHARED_PREFS_WEBRTC_POLICY_RELAY, true)
val prefAcceptCallsFromLockScreen = mkBoolPreference(SHARED_PREFS_WEBRTC_ACCEPT_CALLS_FROM_LOCK_SCREEN, false)
val prefPerformLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false)
val prefLANoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
init {
chatModel.runServiceInBackground.value = getRunServiceInBackground()
chatModel.performLA.value = getPerformLA()
chatModel.runServiceInBackground.value = prefRunServiceInBackground.get()
chatModel.performLA.value = prefPerformLA.get()
}
suspend fun startChat(user: User) {
@@ -529,7 +538,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
if (invitation != null) {
chatModel.callManager.reportCallRemoteEnded(invitation = invitation)
}
withCall(r, r.contact) { call ->
withCall(r, r.contact) { _ ->
chatModel.callCommand.value = WCallCommand.End
withApi {
chatModel.activeCall.value = null
@@ -583,7 +592,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
fun showBackgroundServiceNoticeIfNeeded() {
Log.d(TAG, "showBackgroundServiceNoticeIfNeeded")
if (!getBackgroundServiceNoticeShown()) {
if (!prefBackgroundServiceNoticeShown.get()) {
// the branch for the new users who has never seen service notice
if (isIgnoringBatteryOptimizations(appContext)) {
showBGServiceNotice()
@@ -591,20 +600,20 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
showBGServiceNoticeIgnoreOptimization()
}
// set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice
setBackgroundServiceNoticeShown(true)
setBackgroundServiceBatteryNoticeShown(true)
} else if (!isIgnoringBatteryOptimizations(appContext) && getRunServiceInBackground()) {
prefBackgroundServiceNoticeShown.set(true)
prefBackgroundServiceBatteryNoticeShown.set(true)
} else if (!isIgnoringBatteryOptimizations(appContext) && prefRunServiceInBackground.get()) {
// the branch for users who have app installed, and have seen the service notice,
// but the battery optimization for the app is on (Android 12) AND the service is running
if (getBackgroundServiceBatteryNoticeShown()) {
if (prefBackgroundServiceBatteryNoticeShown.get()) {
// users have been presented with battery notice before - they did not allow ignoring optimizitions -> disable service
showDisablingServiceNotice()
setRunServiceInBackground(false)
prefRunServiceInBackground.set(false)
chatModel.runServiceInBackground.value = false
} else {
// show battery optimization notice
showBGServiceNoticeIgnoreOptimization()
setBackgroundServiceBatteryNoticeShown(true)
prefBackgroundServiceBatteryNoticeShown.set(true)
}
}
}
@@ -695,8 +704,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
fun showLANotice(activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
if (!getLANoticeShown()) {
setLANoticeShown(true)
if (!prefLANoticeShown.get()) {
prefLANoticeShown.set(true)
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.la_notice_title),
text = generalGetString(R.string.la_notice_text),
@@ -710,22 +719,22 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
when (laResult) {
LAResult.Success -> {
chatModel.performLA.value = true
setPerformLA(true)
prefPerformLA.set(true)
laTurnedOnAlert()
}
is LAResult.Error -> {
chatModel.performLA.value = false
setPerformLA(false)
prefPerformLA.set(false)
laErrorToast(appContext, laResult.errString)
}
LAResult.Failed -> {
chatModel.performLA.value = false
setPerformLA(false)
prefPerformLA.set(false)
laFailedToast(appContext)
}
LAResult.Unavailable -> {
chatModel.performLA.value = false
setPerformLA(false)
prefPerformLA.set(false)
chatModel.showAdvertiseLAUnavailableAlert.value = true
}
}
@@ -736,34 +745,6 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
}
}
fun getAutoRestartWorkerVersion(): Int = sharedPreferences.getInt(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0)
fun setAutoRestartWorkerVersion(version: Int) =
sharedPreferences.edit()
.putInt(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, version)
.apply()
fun getRunServiceInBackground(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true)
fun setRunServiceInBackground(runService: Boolean) =
sharedPreferences.edit()
.putBoolean(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, runService)
.apply()
private fun getBackgroundServiceNoticeShown(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false)
fun setBackgroundServiceNoticeShown(shown: Boolean) =
sharedPreferences.edit()
.putBoolean(SHARED_PREFS_SERVICE_NOTICE_SHOWN, shown)
.apply()
private fun getBackgroundServiceBatteryNoticeShown(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false)
fun setBackgroundServiceBatteryNoticeShown(shown: Boolean) =
sharedPreferences.edit()
.putBoolean(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, shown)
.apply()
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true
val powerManager = context.getSystemService(Application.POWER_SERVICE) as PowerManager
@@ -783,19 +764,17 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
}
}
fun getPerformLA(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_PERFORM_LA, false)
private fun mkIntPreference(prefName: String, default: Int) =
Preference(
get = fun() = sharedPreferences.getInt(prefName, default),
set = fun(value) = sharedPreferences.edit().putInt(prefName, value).apply()
)
fun setPerformLA(performLA: Boolean) =
sharedPreferences.edit()
.putBoolean(SHARED_PREFS_PERFORM_LA, performLA)
.apply()
fun getLANoticeShown(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_LA_NOTICE_SHOWN, false)
fun setLANoticeShown(shown: Boolean) =
sharedPreferences.edit()
.putBoolean(SHARED_PREFS_LA_NOTICE_SHOWN, shown)
.apply()
private fun mkBoolPreference(prefName: String, default: Boolean) =
Preference(
get = fun() = sharedPreferences.getBoolean(prefName, default),
set = fun(value) = sharedPreferences.edit().putBoolean(prefName, value).apply()
)
companion object {
private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS"
@@ -803,11 +782,15 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground"
private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown"
private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown"
private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay"
private const val SHARED_PREFS_WEBRTC_ACCEPT_CALLS_FROM_LOCK_SCREEN = "AcceptCallsFromLockScreen"
private const val SHARED_PREFS_PERFORM_LA = "PerformLA"
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
}
}
class Preference<T>(val get: () -> T, val set: (T) -> Unit)
// ChatCommand
sealed class CC {
class Console(val cmd: String): CC()
@@ -156,7 +156,7 @@ private fun ActiveCallOverlayLayout(
ControlButton(call, Icons.Filled.FlipCameraAndroid, R.string.icon_descr_flip_camera, flipCamera)
ControlButton(call, Icons.Filled.Videocam, R.string.icon_descr_video_off, toggleVideo)
} else {
Spacer(Modifier.size(40.dp))
Spacer(Modifier.size(48.dp))
ControlButton(call, Icons.Outlined.VideocamOff, R.string.icon_descr_video_on, toggleVideo)
}
}
@@ -11,7 +11,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@@ -25,14 +24,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Contact
import chat.simplex.app.model.NtfManager.Companion.OpenChatAction
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.onboarding.SimpleXLogo
@@ -93,7 +93,7 @@ fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) {
activity.finish()
}
}
SimpleXTheme(darkTheme = true) {
SimpleXTheme {
Surface(
Modifier
.background(MaterialTheme.colors.background)
@@ -104,33 +104,48 @@ fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) {
if (invitation != null) IncomingCallAlertView(invitation, m)
}
} else if (invitation != null) {
IncomingCallLockScreenAlert(invitation, m)
IncomingCallLockScreenAlert(invitation, m, activity)
}
}
}
}
@Composable
fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel) {
fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel, activity: IncomingCallActivity) {
val cm = chatModel.callManager
val cxt = LocalContext.current
val scope = rememberCoroutineScope()
var acceptCallsFromLockScreen by remember { mutableStateOf(chatModel.controller.prefAcceptCallsFromLockScreen.get()) }
LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = true) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
IncomingCallLockScreenAlertLayout(
invitation,
acceptCallsFromLockScreen,
rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = { chatModel.activeCallInvitation.value = null },
acceptCall = { cm.acceptIncomingCall(invitation = invitation) }
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
openApp = {
SoundPlayer.shared.stop()
var intent = Intent(activity, MainActivity::class.java)
.setAction(OpenChatAction)
.putExtra("chatId", invitation.contact.id)
activity.startActivity(intent)
activity.finish()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getKeyguardManager(activity).requestDismissKeyguard(activity, null)
}
}
)
}
@Composable
fun IncomingCallLockScreenAlertLayout(
invitation: CallInvitation,
acceptCallsFromLockScreen: Boolean,
rejectCall: () -> Unit,
ignoreCall: () -> Unit,
acceptCall: () -> Unit
acceptCall: () -> Unit,
openApp: () -> Unit
) {
Column(
Modifier
@@ -139,22 +154,24 @@ fun IncomingCallLockScreenAlertLayout(
horizontalAlignment = Alignment.CenterHorizontally
) {
IncomingCallInfo(invitation)
Spacer(
Modifier
.fillMaxHeight()
.weight(1f))
ProfileImage(size = 192.dp, image = invitation.contact.profile.image)
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
Spacer(
Modifier
.fillMaxHeight()
.weight(1f))
Row {
LockScreenCallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall)
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall)
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall)
Spacer(Modifier.fillMaxHeight().weight(1f))
if (acceptCallsFromLockScreen) {
ProfileImage(size = 192.dp, image = invitation.contact.profile.image)
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
Spacer(Modifier.fillMaxHeight().weight(1f))
Row {
LockScreenCallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall)
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall)
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall)
}
} else {
SimpleXLogo()
Text(stringResource(R.string.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp)
Text(stringResource(R.string.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp)
Spacer(Modifier.fillMaxHeight().weight(1f))
SimpleButton(text = stringResource(R.string.open_verb), icon = Icons.Filled.Check, click = openApp)
}
}
}
@@ -166,7 +183,9 @@ private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color,
color = Color.Transparent
) {
Column(
Modifier.defaultMinSize(minWidth = 50.dp).padding(4.dp),
Modifier
.defaultMinSize(minWidth = 50.dp)
.padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
IconButton(action) {
@@ -185,16 +204,21 @@ private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color,
@Composable
fun PreviewIncomingCallLockScreenAlert() {
SimpleXTheme(true) {
Surface(Modifier.background(MaterialTheme.colors.background).fillMaxSize()) {
Surface(
Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize()) {
IncomingCallLockScreenAlertLayout(
invitation = CallInvitation(
contact = Contact.sampleData,
peerMedia = CallMediaType.Audio,
sharedKey = null
),
acceptCallsFromLockScreen = false,
rejectCall = {},
ignoreCall = {},
acceptCall = {}
acceptCall = {},
openApp = {},
)
}
}
@@ -0,0 +1,63 @@
package chat.simplex.app.views.usersettings
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
@Composable
fun CallSettingsView(m: ChatModel) {
CallSettingsLayout(
webrtcPolicyRelay = m.controller.prefWebrtcPolicyRelay,
acceptCallsFromLockScreen = m.controller.prefAcceptCallsFromLockScreen
)
}
@Composable
fun CallSettingsLayout(
webrtcPolicyRelay: Preference<Boolean>,
acceptCallsFromLockScreen: Preference<Boolean>,
) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
stringResource(R.string.call_settings),
Modifier.padding(bottom = 24.dp),
style = MaterialTheme.typography.h1
)
SharedPreferenceToggle(stringResource(R.string.connect_calls_via_relay), webrtcPolicyRelay)
SharedPreferenceToggle(stringResource(R.string.accept_calls_from_lock_screen), acceptCallsFromLockScreen)
}
}
@Composable
fun SharedPreferenceToggle(
text: String,
preference: Preference<Boolean>
) {
var preferenceState by remember { mutableStateOf(preference.get()) }
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(text, Modifier.padding(end = 24.dp))
Spacer(Modifier.fillMaxWidth().weight(1f))
Switch(
checked = preferenceState,
onCheckedChange = {
preference.set(it)
preferenceState = it
},
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
)
}
}
@@ -1,8 +1,9 @@
package chat.simplex.app.views.usersettings
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@@ -12,6 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -32,9 +34,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
val user = chatModel.currentUser.value
fun setRunServiceInBackground(on: Boolean) {
chatModel.controller.setRunServiceInBackground(on)
chatModel.controller.prefRunServiceInBackground.set(on)
if (on && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
chatModel.controller.setBackgroundServiceNoticeShown(false)
chatModel.controller.prefBackgroundServiceNoticeShown.set(false)
}
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
chatModel.runServiceInBackground.value = on
@@ -75,6 +77,7 @@ fun SettingsLayout(
Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Column(
Modifier
@@ -83,6 +86,7 @@ fun SettingsLayout(
.padding(8.dp)
.padding(top = 16.dp)
) {
@Composable fun divider() = Divider(Modifier.padding(horizontal = 8.dp))
Text(
stringResource(R.string.your_settings),
style = MaterialTheme.typography.h1,
@@ -93,150 +97,219 @@ fun SettingsLayout(
SettingsSectionView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp) {
ProfilePreview(profile)
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showModal { UserAddressView(it) }) {
Icon(
Icons.Outlined.QrCode,
contentDescription = stringResource(R.string.icon_descr_address),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.your_simplex_contact_address))
}
divider()
UserAddressSection(showModal)
Spacer(Modifier.height(24.dp))
SettingsSectionView(showModal { HelpView(it) }) {
Icon(
Icons.Outlined.HelpOutline,
contentDescription = stringResource(R.string.icon_descr_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.how_to_use_simplex_chat))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showModal { SimpleXInfo(it, onboarding = false) }) {
Icon(
Icons.Outlined.Info,
contentDescription = stringResource(R.string.icon_descr_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.about_simplex_chat))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showModal { MarkdownHelpView() }) {
Icon(
Icons.Outlined.TextFormat,
contentDescription = stringResource(R.string.markdown_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.markdown_in_messages))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView({ uriHandler.openUri(simplexTeamUri) }) {
Icon(
Icons.Outlined.Tag,
contentDescription = stringResource(R.string.icon_descr_simplex_team),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.chat_with_the_founder),
color = MaterialTheme.colors.primary
)
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView({ uriHandler.openUri("mailto:chat@simplex.chat") }) {
Icon(
Icons.Outlined.Email,
contentDescription = stringResource(R.string.icon_descr_email),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.send_us_an_email),
color = MaterialTheme.colors.primary
)
}
CallSettingsSection(showModal)
divider()
ChatLockSection(performLA, setPerformLA)
divider()
PrivateNotificationsSection(runServiceInBackground, setRunServiceInBackground)
divider()
SMPServersSection(showModal)
Spacer(Modifier.height(24.dp))
SettingsSectionView(showModal { SMPServersView(it) }) {
Icon(
Icons.Outlined.Dns,
contentDescription = stringResource(R.string.smp_servers),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.smp_servers))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView() {
Icon(
Icons.Outlined.Bolt,
contentDescription = stringResource(R.string.private_notifications),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.private_notifications), Modifier
.padding(end = 24.dp)
.fillMaxWidth()
.weight(1F)
)
Switch(
checked = runServiceInBackground.value,
onCheckedChange = { setRunServiceInBackground(it) },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 8.dp)
)
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView() {
Icon(
Icons.Outlined.Lock,
contentDescription = stringResource(R.string.chat_lock),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.chat_lock), Modifier
.padding(end = 24.dp)
.fillMaxWidth()
.weight(1F)
)
Switch(
checked = performLA.value,
onCheckedChange = { setPerformLA(it) },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 8.dp)
)
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showTerminal) {
Icon(
painter = painterResource(id = R.drawable.ic_outline_terminal),
contentDescription = stringResource(R.string.chat_console),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.chat_console))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(id = R.drawable.ic_github),
contentDescription = "GitHub",
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(annotatedStringResource(R.string.install_simplex_chat_for_terminal))
}
Divider(Modifier.padding(horizontal = 8.dp))
// SettingsSectionView(showVideoChatPrototype) {
SettingsSectionView() {
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
HelpViewSection(showModal)
divider()
SimpleXInfoSection(showModal)
divider()
MarkdownHelpSection(showModal)
divider()
ConnectToDevelopersSection(uriHandler)
divider()
SendEmailSection(uriHandler)
Spacer(Modifier.height(24.dp))
ChatConsoleSection(showTerminal)
divider()
InstallTerminalAppSection(uriHandler)
divider()
AppVersionSection()
}
}
}
@Composable private fun UserAddressSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { UserAddressView(it) }) {
Icon(
Icons.Outlined.QrCode,
contentDescription = stringResource(R.string.icon_descr_address),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.your_simplex_contact_address))
}
}
@Composable private fun CallSettingsSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { CallSettingsView(it) }) {
Icon(
Icons.Outlined.Videocam,
contentDescription = stringResource(R.string.call_settings),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.call_settings))
}
}
@Composable private fun HelpViewSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { HelpView(it) }) {
Icon(
Icons.Outlined.HelpOutline,
contentDescription = stringResource(R.string.icon_descr_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.how_to_use_simplex_chat))
}
}
@Composable private fun SimpleXInfoSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { SimpleXInfo(it, onboarding = false) }) {
Icon(
Icons.Outlined.Info,
contentDescription = stringResource(R.string.icon_descr_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.about_simplex_chat))
}
}
@Composable private fun MarkdownHelpSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { MarkdownHelpView() }) {
Icon(
Icons.Outlined.TextFormat,
contentDescription = stringResource(R.string.markdown_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.markdown_in_messages))
}
}
@Composable private fun ConnectToDevelopersSection(uriHandler: UriHandler) {
SettingsSectionView({ uriHandler.openUri(simplexTeamUri) }) {
Icon(
Icons.Outlined.Tag,
contentDescription = stringResource(R.string.icon_descr_simplex_team),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.chat_with_the_founder),
color = MaterialTheme.colors.primary
)
}
}
@Composable private fun SendEmailSection(uriHandler: UriHandler) {
SettingsSectionView({ uriHandler.openUri("mailto:chat@simplex.chat") }) {
Icon(
Icons.Outlined.Email,
contentDescription = stringResource(R.string.icon_descr_email),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.send_us_an_email),
color = MaterialTheme.colors.primary
)
}
}
@Composable private fun SMPServersSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { SMPServersView(it) }) {
Icon(
Icons.Outlined.Dns,
contentDescription = stringResource(R.string.smp_servers),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.smp_servers))
}
}
@Composable private fun PrivateNotificationsSection(
runServiceInBackground: MutableState<Boolean>,
setRunServiceInBackground: (Boolean) -> Unit
) {
SettingsSectionView() {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Outlined.Bolt,
contentDescription = stringResource(R.string.private_notifications),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.private_notifications),
Modifier
.padding(end = 24.dp)
.fillMaxWidth()
.weight(1f)
)
Switch(
checked = runServiceInBackground.value,
onCheckedChange = { setRunServiceInBackground(it) },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 8.dp)
)
}
}
}
@Composable private fun ChatLockSection(performLA: MutableState<Boolean>, setPerformLA: (Boolean) -> Unit) {
SettingsSectionView() {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Outlined.Lock,
contentDescription = stringResource(R.string.chat_lock),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.chat_lock), Modifier
.padding(end = 24.dp)
.fillMaxWidth()
.weight(1F)
)
Switch(
checked = performLA.value,
onCheckedChange = { setPerformLA(it) },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 8.dp)
)
}
}
}
@Composable private fun ChatConsoleSection(showTerminal: () -> Unit) {
SettingsSectionView(showTerminal) {
Icon(
painter = painterResource(id = R.drawable.ic_outline_terminal),
contentDescription = stringResource(R.string.chat_console),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.chat_console))
}
}
@Composable private fun InstallTerminalAppSection(uriHandler: UriHandler) {
SettingsSectionView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(id = R.drawable.ic_github),
contentDescription = "GitHub",
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(annotatedStringResource(R.string.install_simplex_chat_for_terminal))
}
}
@Composable private fun AppVersionSection() {
SettingsSectionView() {
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
}
@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary) {
ProfileImage(size = size, image = profileOf.image, color = color)
Spacer(Modifier.padding(horizontal = 4.dp))
@@ -377,6 +377,16 @@
<string name="icon_descr_video_call">видеозвонок</string>
<string name="icon_descr_audio_call">аудиозвонок</string>
<!-- Call settings -->
<string name="call_settings">Настройки звонков</string>
<string name="connect_calls_via_relay">Соединяться через сервер (relay)</string>
<string name="accept_calls_from_lock_screen">Принимать с экрана блокировки</string>
<!-- Call Lock Screen -->
<string name="open_simplex_chat_to_accept_call">Open <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\nto accept call</string>
<string name="allow_accepting_calls_from_lock_screen">You can allow accepting calls from lock screen via Settings.</string>
<string name="open_verb">Open</string>
<!-- Call overlay -->
<string name="status_e2e_encrypted">e2e зашифровано</string>
<string name="status_no_e2e_encryption">нет e2e шифрования</string>
@@ -379,6 +379,16 @@
<string name="icon_descr_video_call">video call</string>
<string name="icon_descr_audio_call">audio call</string>
<!-- Call settings -->
<string name="call_settings">Call settings</string>
<string name="connect_calls_via_relay">Connect via relay</string>
<string name="accept_calls_from_lock_screen">Accept calls from lock screen</string>
<!-- Call Lock Screen -->
<string name="open_simplex_chat_to_accept_call">Open <xliff:g id="appNameFull">SimpleX Chat</xliff:g> to accept call</string>
<string name="allow_accepting_calls_from_lock_screen">Enable calls from lock screen via Settings.</string>
<string name="open_verb">Open</string>
<!-- Call overlay -->
<string name="status_e2e_encrypted">e2e encrypted</string>
<string name="status_no_e2e_encryption">no e2e encryption</string>