From d48d4ed8f90a2623884f377390cbe6470b19a54c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Feb 2022 22:22:07 +0000 Subject: [PATCH] android app: connect via QR code (#338) * connecting via QR code works * add contact/scan qr code pages * new chat sheet layout * remove unused imports and some warnings --- apps/android/app/build.gradle | 17 ++- apps/android/app/src/main/AndroidManifest.xml | 2 + .../java/chat/simplex/app/MainActivity.kt | 9 +- .../main/java/chat/simplex/app/MainPage.kt | 2 + .../java/chat/simplex/app/ui/theme/Type.kt | 9 +- .../chat/simplex/app/views/TerminalView.kt | 12 +- .../chat/simplex/app/views/WelcomeView.kt | 23 ++-- .../chat/simplex/app/views/chat/ChatView.kt | 2 + .../app/views/chat/item/TextItemView.kt | 4 +- .../app/views/chatlist/ChatListView.kt | 72 +++++++----- .../app/views/helpers/CloseSheetBar.kt | 11 +- .../simplex/app/views/helpers/SimpleButton.kt | 2 +- .../chat/simplex/app/views/helpers/Util.kt | 5 +- .../app/views/newchat/AddContactView.kt | 22 ++-- .../app/views/newchat/ConnectContactView.kt | 90 ++++++++++++++ .../simplex/app/views/newchat/NewChatSheet.kt | 93 ++++++++++++--- .../chat/simplex/app/views/newchat/QRCode.kt | 6 - .../app/views/newchat/QRCodeScanner.kt | 110 ++++++++++++++++++ 18 files changed, 392 insertions(+), 99 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 368a46794a..b5dc59627c 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -65,16 +65,27 @@ dependencies { implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation 'androidx.activity:activity-compose:1.3.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' + implementation 'androidx.activity:activity-compose:1.4.0' implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' implementation "androidx.compose.material:material-icons-extended:$compose_version" + implementation "androidx.navigation:navigation-compose:2.4.1" + + def camerax_version = "1.1.0-beta01" + implementation "androidx.camera:camera-core:${camerax_version}" + implementation "androidx.camera:camera-camera2:${camerax_version}" + implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation "androidx.camera:camera-view:${camerax_version}" + //Barcode implementation 'com.google.zxing:core:3.4.0' + implementation 'com.google.mlkit:barcode-scanning:17.0.2' + //Camera Permission + implementation "com.google.accompanist:accompanist-permissions:0.19.0" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" - implementation "androidx.navigation:navigation-compose:2.4.1" } diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index b0fdea62da..7a0ce24bb8 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 15bfceb682..76468bd013 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -19,8 +19,10 @@ import chat.simplex.app.views.* import chat.simplex.app.views.chat.ChatView import chat.simplex.app.views.chatlist.* import chat.simplex.app.views.newchat.AddContactView +import chat.simplex.app.views.newchat.ConnectContactView +import com.google.accompanist.permissions.ExperimentalPermissionsApi - +@ExperimentalPermissionsApi @ExperimentalMaterialApi class MainActivity: ComponentActivity() { private val viewModel by viewModels() @@ -39,6 +41,7 @@ class SimplexViewModel(application: Application) : AndroidViewModel(application) val chatModel = getApplication().chatModel } +@ExperimentalPermissionsApi @ExperimentalMaterialApi @Composable fun Navigation(chatModel: ChatModel) { @@ -64,6 +67,9 @@ fun Navigation(chatModel: ChatModel) { composable(route = Pages.AddContact.route) { AddContactView(chatModel, nav) } + composable(route = Pages.Connect.route) { + ConnectContactView(chatModel, nav) + } composable(route = Pages.Terminal.route) { TerminalView(chatModel, navController = nav) } @@ -86,4 +92,5 @@ sealed class Pages(val route: String) { object ChatList: Pages("chats") object Chat: Pages("chat") object AddContact: Pages("add_contact") + object Connect: Pages("connect") } diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt index 47ffe9dd91..f42208b2a0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt @@ -7,7 +7,9 @@ import chat.simplex.app.model.ChatModel import chat.simplex.app.views.TerminalView import chat.simplex.app.views.WelcomeView import chat.simplex.app.views.chatlist.ChatListView +import com.google.accompanist.permissions.ExperimentalPermissionsApi +@ExperimentalPermissionsApi @ExperimentalMaterialApi @Composable fun MainPage(chatModel: ChatModel, nav: NavController) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt index f03afbd0ff..10408878a6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt @@ -16,13 +16,18 @@ val Typography = Typography( h2 = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, - fontSize = 20.sp + fontSize = 24.sp ), body1 = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp ), + body2 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp + ), button = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, @@ -31,6 +36,6 @@ val Typography = Typography( caption = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, - fontSize = 14.sp + fontSize = 20.sp ) ) \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 5a73363cf3..d506754675 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -13,22 +13,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.navigation.* -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController import chat.simplex.app.model.* -import androidx.navigation.navArgument -import chat.simplex.app.Pages import chat.simplex.app.model.ChatModel import chat.simplex.app.model.TerminalItem -import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.SendMsgView import chat.simplex.app.views.helpers.withApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.DelicateCoroutinesApi +@DelicateCoroutinesApi @Composable fun TerminalView(chatModel: ChatModel, navController: NavController) { Column { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index d2d92f27e8..3ac36a12b3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -13,14 +13,12 @@ import androidx.compose.material.TextField import androidx.compose.material.Button import androidx.compose.ui.unit.dp import androidx.compose.ui.Modifier -import chat.simplex.app.SimplexViewModel import chat.simplex.app.model.ChatModel import chat.simplex.app.model.Profile -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import chat.simplex.app.views.helpers.withApi +import kotlinx.coroutines.DelicateCoroutinesApi +@DelicateCoroutinesApi @Composable fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) { Column( @@ -38,6 +36,7 @@ fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) { } } +@DelicateCoroutinesApi @Composable fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) { var displayName by remember { mutableStateOf("") } @@ -51,14 +50,12 @@ fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) { Text("Full Name (Optional)") TextField(value = fullName, onValueChange = { fullName = it }) Button(onClick={ - GlobalScope.launch { - withContext(Dispatchers.Main) { - val user = chatModel.controller.apiCreateActiveUser( - Profile(displayName, fullName) - ) - chatModel.controller.startChat(user) - routeHome() - } + withApi { + val user = chatModel.controller.apiCreateActiveUser( + Profile(displayName, fullName) + ) + chatModel.controller.startChat(user) + routeHome() } }, enabled = displayName.isNotEmpty() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 72111b9d5b..35a537bd31 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -19,8 +19,10 @@ import chat.simplex.app.model.* import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.item.ChatItemView import chat.simplex.app.views.helpers.withApi +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.datetime.Clock +@DelicateCoroutinesApi @Composable fun ChatView(chatModel: ChatModel, nav: NavController) { if (chatModel.chatId.value != null && chatModel.chats.count() > 0) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt index 6faafba256..6974c34b14 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -20,11 +20,9 @@ val ReceivedColorLight = Color(0x1EF1F0F5) @Composable fun TextItemView(chatItem: ChatItem) { val sent = chatItem.chatDir.sent - val color = if (sent) SentColorLight else ReceivedColorLight - Surface( shape = RoundedCornerShape(18.dp), - color = color + color = if (sent) SentColorLight else ReceivedColorLight ) { Box( modifier = Modifier.padding(vertical = 6.dp, horizontal = 12.dp) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index bead9f11c6..cd6466c468 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -5,25 +5,25 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import chat.simplex.app.Pages import chat.simplex.app.model.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.newchat.NewChatSheet +import com.google.accompanist.permissions.ExperimentalPermissionsApi import kotlinx.coroutines.* @@ -46,6 +46,8 @@ fun scaffoldController(): ScaffoldController { ) } +@DelicateCoroutinesApi +@ExperimentalPermissionsApi @ExperimentalMaterialApi @Composable fun ChatListView(chatModel: ChatModel, nav: NavController) { @@ -53,20 +55,31 @@ fun ChatListView(chatModel: ChatModel, nav: NavController) { BottomSheetScaffold( scaffoldState = newChatCtrl.state, sheetPeekHeight = 0.dp, - topBar = { ChatListToolbar(newChatCtrl) }, - sheetContent = { NewChatSheet(chatModel, newChatCtrl, nav) } + sheetContent = { NewChatSheet(chatModel, newChatCtrl, nav) }, + sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp), ) { - Column(modifier = Modifier - .padding(vertical = 8.dp) - .fillMaxSize() - .background(MaterialTheme.colors.background) - ) { - ChatList(chatModel, nav) - Button( - onClick = { nav.navigate(Pages.Terminal.route) }, - modifier = Modifier.padding(14.dp) + Box { + Column( + modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxSize() + .background(MaterialTheme.colors.background) ) { - Text("Terminal") + ChatListToolbar(newChatCtrl) + ChatList(chatModel, nav) + Button( + onClick = { nav.navigate(Pages.Terminal.route) }, + modifier = Modifier.padding(14.dp) + ) { + Text("Terminal") + } + } + if (newChatCtrl.state.bottomSheetState.isExpanded) { + Surface(Modifier + .fillMaxSize() + .clickable { newChatCtrl.collapse() }, + color = Color.Black.copy(alpha = 0.12F) + ) {} } } } @@ -97,29 +110,30 @@ fun ChatListToolbar(newChatSheetCtrl: ScaffoldController) { Icons.Outlined.PersonAdd, "Add Contact", tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(horizontal = 10.dp) + modifier = Modifier + .padding(horizontal = 10.dp) .clickable { newChatSheetCtrl.toggle() } ) } } -fun goToChat(chat: Chat, chatModel: ChatModel, navController: NavController) { - GlobalScope.launch { - withContext(Dispatchers.Main) { - val cInfo = chat.chatInfo - val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) - if (chat != null ) { - chatModel.chatId = mutableStateOf(cInfo.id) - chatModel.chatItems = chat.chatItems.toMutableStateList() - navController.navigate(Pages.Chat.route) - } else { - // TODO show error? or will apiGetChat show it - } +@DelicateCoroutinesApi +fun goToChat(chatPreview: Chat, chatModel: ChatModel, navController: NavController) { + withApi { + val cInfo = chatPreview.chatInfo + val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) + if (chat != null ) { + chatModel.chatId = mutableStateOf(cInfo.id) + chatModel.chatItems = chat.chatItems.toMutableStateList() + navController.navigate(Pages.Chat.route) + } else { + // TODO show error? or will apiGetChat show it } } } +@DelicateCoroutinesApi @Composable fun ChatList(chatModel: ChatModel, navController: NavController) { LazyColumn( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt index 0c3afe9282..c323f4317b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt @@ -18,16 +18,19 @@ import chat.simplex.app.views.newchat.AddContactLayout @Composable fun CloseSheetBar(close: () -> Unit) { - Row (Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.End + Row ( + Modifier + .fillMaxWidth() + .height(40.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically ) { Icon( Icons.Outlined.Close, "Close button", tint = MaterialTheme.colors.primary, modifier = Modifier - .padding(horizontal = 10.dp) + .padding(vertical = 10.dp) .clickable { close() } ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt index 7f04d4401a..056dfe626e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt @@ -29,7 +29,7 @@ fun SimpleButton(text: String, icon: ImageVector, click: () -> Unit) { modifier = Modifier.padding(horizontal = 10.dp) ) Text(text, - style = MaterialTheme.typography.button, + style = MaterialTheme.typography.caption, color = MaterialTheme.colors.primary ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index ea38d0a509..a8f5ec9dd4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -4,7 +4,4 @@ import kotlinx.coroutines.* @DelicateCoroutinesApi fun withApi(action: suspend CoroutineScope.() -> Unit): Job = - GlobalScope.launch { - withContext(Dispatchers.Main) { action() } - } - + GlobalScope.launch { withContext(Dispatchers.Main, action) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt index 0714264bde..7066024bf2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt @@ -3,11 +3,8 @@ package chat.simplex.app.views.newchat import android.content.Context import android.content.Intent import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material.Button import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Share @@ -15,11 +12,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.* +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.navigation.NavController -import chat.simplex.app.Pages import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.SimpleButton import chat.simplex.app.ui.theme.SimpleXTheme @@ -56,14 +54,24 @@ fun AddContactLayout(connReq: String, close: () -> Unit, share: () -> Unit) { Text( "Show QR code to your contact\nto scan from the app", style = MaterialTheme.typography.h2, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 8.dp) ) QRCode(connReq) Text( - "If you can't show QR code, you can share the invitation link via any channel.\nIt does not have to be secure or private, you just have to know who you are sharing it with.", + buildAnnotatedString { + append("If you cannot meet in person, you can ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append("scan QR code in the video call") + } + append(", or you can share the invitation link via any other channel.") + }, textAlign = TextAlign.Center, style = MaterialTheme.typography.caption, - modifier = Modifier.padding(bottom = 16.dp).padding(horizontal = 16.dp) + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp) + .padding(bottom = 16.dp) ) SimpleButton("Share invitation link", icon = Icons.Outlined.Share, click = share) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt new file mode 100644 index 0000000000..be219e4c06 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt @@ -0,0 +1,90 @@ +package chat.simplex.app.views.newchat + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.* +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.navigation.NavController +import chat.simplex.app.model.ChatModel +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.CloseSheetBar +import chat.simplex.app.views.helpers.withApi +import kotlinx.coroutines.DelicateCoroutinesApi + +@DelicateCoroutinesApi +@Composable +fun ConnectContactView(chatModel: ChatModel, nav: NavController) { + ConnectContactLayout( + qrCodeScanner = { + QRCodeScanner { connReqUri -> + withApi { + val res = chatModel.controller.apiConnect(connReqUri) + // check if it is valid + nav.popBackStack() + } + } + }, + close = { nav.popBackStack() } + ) +} + +@Composable +fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Unit) { + Column( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxSize() + .background(MaterialTheme.colors.background), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CloseSheetBar(close) + Text( + "Scan QR code", + style = MaterialTheme.typography.h1, + modifier = Modifier.padding(bottom = 8.dp) + ) + Text( + "Your chat profile will be sent\nto your contact", + style = MaterialTheme.typography.h2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 16.dp) + ) + Box ( + Modifier + .fillMaxWidth() + .aspectRatio(ratio = 1F) + ) { qrCodeScanner() } + Text( + buildAnnotatedString { + append("If you cannot meet in person, you can ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append("scan QR code in the video call") + } + append(", or you can create the invitation link.") + }, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.caption, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 16.dp) + ) + } +} + +@Preview +@Composable +fun PreviewConnectContactLayout() { + SimpleXTheme { + ConnectContactLayout( + qrCodeScanner = { Surface {} }, + close = {}, + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt index 4d74ff895b..4a2f47f6f5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt @@ -1,37 +1,57 @@ package chat.simplex.app.views.newchat -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.ExperimentalMaterialApi +import android.Manifest +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.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.Composable -import androidx.compose.material.Text +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +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.navigation.NavController import chat.simplex.app.Pages import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chatlist.ScaffoldController import chat.simplex.app.views.helpers.withApi +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.coroutines.DelicateCoroutinesApi +@DelicateCoroutinesApi +@ExperimentalPermissionsApi @ExperimentalMaterialApi @Composable fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: NavController) { + val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) NewChatSheetLayout( addContact = { withApi { -// show spinner + // show spinner chatModel.connReqInvitation = chatModel.controller.apiAddContact() -// hide spinner + // hide spinner if (chatModel.connReqInvitation != null) { newChatCtrl.collapse() nav.navigate(Pages.AddContact.route) } } }, - scanCode = {}, + scanCode = { + newChatCtrl.collapse() + nav.navigate(Pages.Connect.route) + cameraPermissionState.launchPermissionRequest() + }, close = { newChatCtrl.collapse() } @@ -40,19 +60,60 @@ fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: Nav @Composable fun NewChatSheetLayout(addContact: () -> Unit, scanCode: () -> Unit, close: () -> Unit) { - Column ( Modifier.padding(all = 8.dp) ) { - Button(onClick = addContact) { - Text("Add contact") + Row(Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 48.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Box(Modifier.weight(1F).fillMaxWidth()) { + ActionButton( + "Add contact", "(create QR code\nor link)", + Icons.Outlined.PersonAdd, click = addContact + ) } - Button(onClick = scanCode) { - Text("Scan QR code") + Box(Modifier.weight(1F).fillMaxWidth()) { + ActionButton( + "Scan QR code", "(in person or in video call)", + Icons.Outlined.QrCode, click = scanCode + ) } - Button(onClick = close) { - Text("Cancel") + Box(Modifier.weight(1F).fillMaxWidth()) { + ActionButton( + "Create Group", "(coming soon!)", + Icons.Outlined.GroupAdd, disabled = true + ) } } } +@Composable +fun ActionButton(text: String, comment: String, icon: ImageVector, disabled: Boolean = false, + click: () -> Unit = {}) { + Column( + Modifier + .clickable(onClick = click) + .padding(horizontal = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val tint = if (disabled) DarkGray else MaterialTheme.colors.primary + Icon(icon, text, + tint = tint, + modifier = Modifier + .size(40.dp) + .padding(bottom = 8.dp)) + Text(text, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + color = tint, + modifier = Modifier.padding(bottom = 4.dp) + ) + Text(comment, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.body2 + ) + } +} + @Preview @Composable fun PreviewNewChatSheet() { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt index 0a0fc2a2e8..c3d1cd0814 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt @@ -3,15 +3,9 @@ package chat.simplex.app.views.newchat import android.graphics.Bitmap import android.graphics.Color import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.toSize import chat.simplex.app.ui.theme.SimpleXTheme import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt new file mode 100644 index 0000000000..873d4e6ae6 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt @@ -0,0 +1,110 @@ +package chat.simplex.app.views.newchat + +import android.util.Log +import android.view.ViewGroup +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import com.google.common.util.concurrent.ListenableFuture +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import java.util.concurrent.TimeUnit + +// Bar code scanner adapted from https://github.com/MakeItEasyDev/Jetpack-Compose-BarCode-Scanner + +@Composable +fun QRCodeScanner(onBarcode: (String) -> Unit) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + var preview by remember { mutableStateOf(null) } + + AndroidView( + factory = { AndroidViewContext -> + PreviewView(AndroidViewContext).apply { + this.scaleType = PreviewView.ScaleType.FILL_CENTER + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + } + }, +// modifier = Modifier.fillMaxSize(), + update = { previewView -> + val cameraSelector: CameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() + val cameraProviderFuture: ListenableFuture = + ProcessCameraProvider.getInstance(context) + + cameraProviderFuture.addListener({ + preview = Preview.Builder().build().also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + val barcodeAnalyser = BarCodeAnalyser { barcodes -> + barcodes.firstOrNull()?.rawValue?.let(onBarcode) + } + val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { it.setAnalyzer(cameraExecutor, barcodeAnalyser) } + + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) + } catch (e: Exception) { + Log.d("SIMPLEX", "CameraPreview: ${e.localizedMessage}") + } + }, ContextCompat.getMainExecutor(context)) + } + ) +} + +class BarCodeAnalyser( + private val onBarcodeDetected: (barcodes: List) -> Unit, +): ImageAnalysis.Analyzer { + private var lastAnalyzedTimeStamp = 0L + + @ExperimentalGetImage + override fun analyze(image: ImageProxy) { + val currentTimestamp = System.currentTimeMillis() + if (currentTimestamp - lastAnalyzedTimeStamp >= TimeUnit.SECONDS.toMillis(1)) { + image.image?.let { imageToAnalyze -> + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS) + .build() + val barcodeScanner = BarcodeScanning.getClient(options) + val imageToProcess = InputImage.fromMediaImage(imageToAnalyze, image.imageInfo.rotationDegrees) + + barcodeScanner.process(imageToProcess) + .addOnSuccessListener { barcodes -> + if (barcodes.isNotEmpty()) { + onBarcodeDetected(barcodes) + } else { + Log.d("SIMPLEX", "BarcodeAnalyser: No barcode Scanned") + } + } + .addOnFailureListener { exception -> + Log.d("SIMPLEX", "BarcodeAnalyser: Something went wrong $exception") + } + .addOnCompleteListener { + image.close() + } + } + lastAnalyzedTimeStamp = currentTimestamp + } else { + image.close() + } + } +}