Skip to content

Commit

Permalink
Impl scanImage
Browse files Browse the repository at this point in the history
  • Loading branch information
SanmerDev committed Aug 17, 2024
1 parent fec772a commit a2aab8f
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
Expand All @@ -27,6 +28,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import dev.sanmer.authenticator.Const
import dev.sanmer.authenticator.R
Expand All @@ -39,18 +41,26 @@ import dev.sanmer.authenticator.ui.screens.settings.component.SettingItem
import dev.sanmer.authenticator.ui.screens.settings.component.TokenItem
import dev.sanmer.authenticator.ui.screens.settings.component.ToolItem
import dev.sanmer.authenticator.viewmodel.SettingsViewModel
import dev.sanmer.otp.OtpUri.Default.isOtpUri

@Composable
fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
navController: NavController
) {
val uri by viewModel.uri.collectAsStateWithLifecycle()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()

DisposableEffect(uri) {
if (uri.isOtpUri()) navController.navigateSingleTopTo(Screen.Edit(uri))
onDispose(viewModel::rewind)
}

var token by rememberSaveable { mutableStateOf(false) }
if (token) TokenItem(
onDismiss = { token = false },
navController = navController
navController = navController,
scanImage = viewModel::scanImage,
)

var database by rememberSaveable { mutableStateOf(false) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package dev.sanmer.authenticator.ui.screens.settings.component

import android.content.Context
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
Expand All @@ -12,6 +17,7 @@ import androidx.compose.material3.Text
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
Expand All @@ -23,8 +29,15 @@ import dev.sanmer.authenticator.ui.main.Screen
@Composable
fun TokenItem(
onDismiss: () -> Unit,
navController: NavController
navController: NavController,
scanImage: (Context, Uri) -> Unit,
) {
val context = LocalContext.current
val pickImage = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { if (it != null) scanImage(context, it) }
)

ModalBottomSheet(
onDismissRequest = onDismiss,
shape = MaterialTheme.shapes.large.bottom(0.dp),
Expand Down Expand Up @@ -61,6 +74,16 @@ fun TokenItem(
onDismiss()
}
)

SettingItem(
icon = R.drawable.photo_scan,
title = stringResource(id = R.string.settings_scan_image),
onClick = {
pickImage.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import dev.sanmer.authenticator.model.serializer.AuthJson
import dev.sanmer.authenticator.repository.DbRepository
import dev.sanmer.authenticator.ui.CryptoActivity
import dev.sanmer.encoding.isBase32
import dev.sanmer.qrcode.QRCode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
Expand All @@ -23,10 +26,17 @@ class SettingsViewModel @Inject constructor(
private var encrypted = emptyList<Auth>()
private var decrypted = emptyList<Auth>()

private val uriFlow = MutableStateFlow("")
val uri get() = uriFlow.asStateFlow()

init {
Timber.d("SettingsViewModel init")
}

fun rewind() {
uriFlow.value = ""
}

private fun decrypt(
context: Context,
auths: List<Auth>,
Expand Down Expand Up @@ -119,4 +129,15 @@ class SettingsViewModel @Inject constructor(
uri = uri,
auths = decrypted
)

fun scanImage(context: Context, uri: Uri) {
runCatching {
val cr = context.contentResolver
cr.openInputStream(uri).let(::requireNotNull).use(QRCode::decodeFromStream)
}.onSuccess {
uriFlow.value = it
}.onFailure {
Timber.e(it)
}
}
}
48 changes: 48 additions & 0 deletions app/src/main/res/drawable/photo_scan.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15,8h0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round" />
<path
android:pathData="M6,13l2.644,-2.644a1.21,1.21 0,0 1,1.712 0l3.644,3.644"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round" />
<path
android:pathData="M13,13l1.644,-1.644a1.21,1.21 0,0 1,1.712 0l1.644,1.644"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round" />
<path
android:pathData="M4,8v-2a2,2 0,0 1,2 -2h2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round" />
<path
android:pathData="M4,16v2a2,2 0,0 0,2 2h2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round" />
<path
android:pathData="M16,4h2a2,2 0,0 1,2 2v2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round" />
<path
android:pathData="M16,20h2a2,2 0,0 0,2 -2v-2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round" />
</vector>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<string name="settings_tools_desc">Decrypt, encode and decode</string>
<string name="settings_enter">Enter Manually</string>
<string name="settings_scan">Scan QR Code</string>
<string name="settings_scan_image">Scan Image</string>
<string name="settings_import">Import from JSON</string>
<string name="settings_export">Export to JSON</string>
<string name="settings_decrypt">Decrypt a JSON</string>
Expand Down
48 changes: 48 additions & 0 deletions core/src/main/kotlin/dev/sanmer/qrcode/QRCode.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.sanmer.qrcode

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import androidx.annotation.ColorInt
import com.google.zxing.BarcodeFormat
Expand All @@ -9,9 +10,12 @@ import com.google.zxing.DecodeHintType
import com.google.zxing.LuminanceSource
import com.google.zxing.MultiFormatReader
import com.google.zxing.MultiFormatWriter
import com.google.zxing.NotFoundException
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.common.BitMatrix
import com.google.zxing.common.HybridBinarizer
import java.io.InputStream

@Suppress("NOTHING_TO_INLINE")
object QRCode {
Expand Down Expand Up @@ -46,6 +50,50 @@ object QRCode {
)
)

private inline fun Bitmap.resize(maxWidth: Int, maxHeight: Int): Bitmap {
if (maxHeight <= 0 || maxWidth <= 0) {
return this
}

val maxRatio = maxWidth.toFloat() / maxHeight
val ratio = width.toFloat() / height

var width = maxWidth
var height = maxHeight
if (maxRatio > 1) {
width = (maxHeight.toFloat() * ratio).toInt()
} else {
height = (maxWidth.toFloat() / ratio).toInt()
}

return Bitmap.createScaledBitmap(this, width, height, true)
}

fun decodeFromStream(stream: InputStream): String {
var bitmap = requireNotNull(
BitmapFactory.decodeStream(
stream,
null,
BitmapFactory.Options()
)
) { "Unable to decode stream to bitmap" }

for (i in 0..2) {
if (i != 0) {
bitmap = bitmap.resize(bitmap.width / (i * 2), bitmap.height / (i * 2))
}

try {
val pixels = IntArray(bitmap.width * bitmap.height)
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
return decode(source = RGBLuminanceSource(bitmap.width, bitmap.height, pixels))
} catch (_: NotFoundException) {
}
}

throw IllegalArgumentException(stream.toString())
}

private inline fun createBitmap(
matrix: BitMatrix,
@ColorInt foregroundColor: Int,
Expand Down

0 comments on commit a2aab8f

Please sign in to comment.