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
This commit is contained in:
Evgeny Poberezkin
2022-02-19 22:22:07 +00:00
committed by GitHub
parent f57a7009a3
commit d48d4ed8f9
18 changed files with 392 additions and 99 deletions
+14 -3
View File
@@ -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"
}
@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.simplex.app">
<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -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<SimplexViewModel>()
@@ -39,6 +41,7 @@ class SimplexViewModel(application: Application) : AndroidViewModel(application)
val chatModel = getApplication<SimplexApp>().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")
}
@@ -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) {
@@ -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
)
)
@@ -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 {
@@ -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()
@@ -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) {
@@ -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)
@@ -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(
@@ -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() }
)
}
@@ -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
)
}
@@ -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) }
@@ -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)
}
@@ -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 = {},
)
}
}
@@ -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() {
@@ -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
@@ -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<Preview?>(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> =
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<Barcode>) -> 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()
}
}
}