Getting ready for initial release

This commit is contained in:
Henry Hiles 2023-04-02 14:27:24 -04:00
parent 2e86e0d052
commit 156f97bb9d
8 changed files with 249 additions and 210 deletions

View file

@ -67,20 +67,25 @@ android {
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
implementation("androidx.activity:activity-compose:1.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.0")
implementation("androidx.compose.material3:material3:1.1.0-beta01")
implementation("androidx.compose.ui:ui:1.3.3")
implementation("androidx.compose.ui:ui-tooling-preview:1.3.3")
implementation("androidx.compose.material3:material3:1.1.0-alpha06")
implementation("androidx.compose.material:material-icons-extended:1.3.1")
val accompanistVersion = "0.30.0"
implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
implementation("androidx.camera:camera-camera2:1.2.1")
implementation("androidx.camera:camera-lifecycle:1.2.1")
implementation("androidx.camera:camera-view:1.2.1")
val composeVersion = "1.4.0"
implementation("androidx.compose.ui:ui:$composeVersion")
implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
implementation("androidx.compose.material:material-icons-extended:$composeVersion")
debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
debugImplementation("androidx.compose.ui:ui-test-manifest:$composeVersion")
val cameraVersion = "1.2.2"
implementation("androidx.camera:camera-camera2:$cameraVersion")
implementation("androidx.camera:camera-lifecycle:$cameraVersion")
implementation("androidx.camera:camera-view:$cameraVersion")
implementation("com.google.zxing:core:3.3.3")
debugImplementation("androidx.compose.ui:ui-tooling:1.3.3")
debugImplementation("androidx.compose.ui:ui-test-manifest:1.3.3")
}

View file

@ -1,97 +1,56 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package com.henryhiles.qscan
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.webkit.URLUtil
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import com.henryhiles.qscan.components.Camera
import com.henryhiles.qscan.components.alerts.PermissionAlert
import com.henryhiles.qscan.components.alerts.ScannedAlert
import com.henryhiles.qscan.ui.theme.QScanTheme
import com.henryhiles.qscan.utils.Helpers.isURL
const val autoOpenKey = "AUTO_OPEN"
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
QScanTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
modifier = Modifier.fillMaxSize()
) {
Screen()
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Screen() {
var code by remember { mutableStateOf("") }
val context = LocalContext.current
val lifeCycleOwner = LocalLifecycleOwner.current
val activity = lifeCycleOwner as Activity
val activity = context as Activity
val sharedPref = activity.getPreferences(Context.MODE_PRIVATE)
var doNotAsk by remember {
mutableStateOf(
sharedPref.getBoolean(
R.string.should_auto_open_key.toString(),
autoOpenKey,
false
)
)
}
var prompted by remember {
mutableStateOf(false)
}
val uriHandler = LocalUriHandler.current
val cameraProviderFuture = remember {
ProcessCameraProvider.getInstance(context)
}
var hasCamPermission by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(
context,
val cameraPermissionState = rememberPermissionState(
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
)
}
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { granted ->
hasCamPermission = granted
prompted = true
}
)
LaunchedEffect(key1 = true) {
launcher.launch(Manifest.permission.CAMERA)
}
LaunchedEffect(key1 = code) {
if (doNotAsk && URLUtil.isValidUrl(code)) {
@ -102,130 +61,29 @@ fun Screen() {
LaunchedEffect(key1 = doNotAsk) {
with(sharedPref.edit()) {
putBoolean(R.string.should_auto_open_key.toString(), doNotAsk)
putBoolean(autoOpenKey, doNotAsk)
apply()
}
}
Column(modifier = Modifier.fillMaxSize()) {
if (hasCamPermission) {
AndroidView(
factory = { context ->
val previewView = PreviewView(context)
val preview = androidx.camera.core.Preview.Builder().build()
val selector =
CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(
ContextCompat.getMainExecutor(context),
QrCodeAnalyzer { result -> code = result })
try {
cameraProviderFuture.get()
.bindToLifecycle(lifeCycleOwner, selector, preview, imageAnalysis)
} catch (e: Exception) {
e.printStackTrace()
}
previewView
}, modifier = Modifier
.fillMaxSize()
)
if (code != "" && !doNotAsk) {
val isURL = URLUtil.isValidUrl(code)
var tempDoNotAsk by remember { mutableStateOf(false) }
AlertDialog(onDismissRequest = { code = "" }) {
Surface(
shape = MaterialTheme.shapes.large
if (cameraPermissionState.status.isGranted) {
Camera(onScan = { code = it })
if (code != "" && !(doNotAsk && isURL(code)))
ScannedAlert(
onDismiss = { code = "" },
code = code
) { doNotAsk = it }
} else PermissionAlert(
textToShow = if (cameraPermissionState.status.shouldShowRationale) "The camera is important for this app. Please grant the permission."
else "Camera permission is required for this feature to be available. Please grant the permission.",
permissionName = "camera"
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "QR Code Scanned",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.align(CenterHorizontally)
)
SelectionContainer {
Text(
text = if (isURL) "This QR code will take you to $code, are you sure you want to go there?" else "The content of that QR Code is \"$code\"",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp)
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
LabelledCheckBox(
checked = tempDoNotAsk,
onCheckedChange = { tempDoNotAsk = it },
label = "Don't ask again"
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { code = "" }) {
Text(
text = "Cancel",
style = MaterialTheme.typography.labelLarge
)
}
if (isURL)
TextButton(onClick = {
uriHandler.openUri(code)
doNotAsk = tempDoNotAsk
code = ""
}) {
Text(
text = "Open URL",
style = MaterialTheme.typography.labelLarge
)
cameraPermissionState.launchPermissionRequest()
}
}
}
}
}
}
} else if (prompted) AlertDialog(onDismissRequest = {}) {
Surface(
shape = MaterialTheme.shapes.large
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "No camera permission",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.align(CenterHorizontally)
)
SelectionContainer {
Text(
text = "Camera permission is needed for this app to function.",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = {
launcher.launch(Manifest.permission.CAMERA)
}) {
Text(
text = "Grant Permission",
style = MaterialTheme.typography.labelLarge
)
}
}
}
}
}
}
}

View file

@ -0,0 +1,53 @@
package com.henryhiles.qscan.components
import android.content.Context
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import com.henryhiles.qscan.QrCodeAnalyzer
@Composable
fun Camera(onScan: (result: String) -> Unit) {
val lifeCycleOwner = LocalLifecycleOwner.current
val cameraProviderFuture = remember {
ProcessCameraProvider.getInstance(lifeCycleOwner as Context)
}
AndroidView(
factory = { context ->
val previewView = PreviewView(context)
val preview = Preview.Builder().build()
val selector =
CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(
ContextCompat.getMainExecutor(context),
QrCodeAnalyzer(onScan)
)
try {
cameraProviderFuture.get()
.bindToLifecycle(lifeCycleOwner, selector, preview, imageAnalysis)
} catch (e: Exception) {
e.printStackTrace()
}
previewView
}, modifier = Modifier
.fillMaxSize()
)
}

View file

@ -0,0 +1,50 @@
package com.henryhiles.qscan.components.alerts
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@Composable
fun PermissionAlert(textToShow: String, permissionName: String, onGrantRequest: () -> Unit) {
AlertDialog(
title = {
Text(
text = "${permissionName.replaceFirstChar { it.uppercaseChar() }} permission required",
)
},
onDismissRequest = {},
confirmButton = {
TextButton(onClick = onGrantRequest) {
Text(
text = "Grant Permission",
)
}
},
text = {
SelectionContainer {
Text(
text = textToShow,
)
}
}
)
}
//Surface(
//shape = MaterialTheme.shapes.large
//) {
// Column(modifier = Modifier.padding(16.dp)) {
//
// Row(
// modifier = Modifier.fillMaxWidth(),
// horizontalArrangement = Arrangement.End
// ) {
// }
// }
//}

View file

@ -0,0 +1,65 @@
package com.henryhiles.qscan.components.alerts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import com.henryhiles.qscan.LabelledCheckBox
import com.henryhiles.qscan.utils.Helpers.isURL
@Composable
fun ScannedAlert(onDismiss: () -> Unit, code: String, onChangeDoNotAsk: (Boolean) -> Unit) {
val uriHandler = LocalUriHandler.current
var tempDoNotAsk by remember { mutableStateOf(false) }
AlertDialog(
title = {
Text(
text = "QR Code Scanned",
)
}, onDismissRequest = onDismiss, dismissButton = {
TextButton(onClick = onDismiss) {
Text(
text = "Cancel",
)
}
},
text = {
Column {
SelectionContainer {
Text(
text = if (isURL(code)) "This QR code will take you to $code, are you sure you want to go there?" else "The content of that QR Code is \"$code\"",
)
}
Spacer(modifier = Modifier.height(16.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
LabelledCheckBox(
checked = tempDoNotAsk,
onCheckedChange = { tempDoNotAsk = it },
label = "Don't ask again"
)
}
}
},
confirmButton = {
if (isURL(code))
TextButton(onClick = {
uriHandler.openUri(code)
onChangeDoNotAsk(tempDoNotAsk)
onDismiss()
}) {
Text(
text = "Open URL",
)
}
})
}

View file

@ -0,0 +1,9 @@
package com.henryhiles.qscan.utils
import android.webkit.URLUtil
object Helpers {
fun isURL(value: String): Boolean {
return URLUtil.isValidUrl(value)
}
}

View file

@ -1,4 +1,3 @@
<resources>
<string name="app_name">QScan</string>
<string name="should_auto_open_key">AUTO_OPEN</string>
</resources>