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 { dependencies {
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.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") val accompanistVersion = "0.30.0"
implementation("androidx.compose.ui:ui-tooling-preview:1.3.3") implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
implementation("androidx.compose.material3:material3:1.1.0-alpha06")
implementation("androidx.compose.material:material-icons-extended:1.3.1")
implementation("androidx.camera:camera-camera2:1.2.1") val composeVersion = "1.4.0"
implementation("androidx.camera:camera-lifecycle:1.2.1") implementation("androidx.compose.ui:ui:$composeVersion")
implementation("androidx.camera:camera-view:1.2.1") 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") 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,231 +1,89 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package com.henryhiles.qscan package com.henryhiles.qscan
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column
import androidx.camera.core.CameraSelector import androidx.compose.foundation.layout.fillMaxSize
import androidx.camera.core.ImageAnalysis import androidx.compose.material3.Surface
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.runtime.* 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.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp import com.google.accompanist.permissions.ExperimentalPermissionsApi
import androidx.compose.ui.viewinterop.AndroidView import com.google.accompanist.permissions.isGranted
import androidx.core.content.ContextCompat 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.ui.theme.QScanTheme
import com.henryhiles.qscan.utils.Helpers.isURL
const val autoOpenKey = "AUTO_OPEN"
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
QScanTheme { QScanTheme {
// A surface container using the 'background' color from the theme
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize()
color = MaterialTheme.colorScheme.background
) { ) {
Screen() var code by remember { mutableStateOf("") }
} val context = LocalContext.current
} val activity = context as Activity
} val sharedPref = activity.getPreferences(Context.MODE_PRIVATE)
} var doNotAsk by remember {
} mutableStateOf(
sharedPref.getBoolean(
@OptIn(ExperimentalMaterial3Api::class) autoOpenKey,
@Composable false
fun Screen() {
var code by remember { mutableStateOf("") }
val context = LocalContext.current
val lifeCycleOwner = LocalLifecycleOwner.current
val activity = lifeCycleOwner as Activity
val sharedPref = activity.getPreferences(Context.MODE_PRIVATE)
var doNotAsk by remember {
mutableStateOf(
sharedPref.getBoolean(
R.string.should_auto_open_key.toString(),
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,
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)) {
uriHandler.openUri(code)
code = ""
}
}
LaunchedEffect(key1 = doNotAsk) {
with(sharedPref.edit()) {
putBoolean(R.string.should_auto_open_key.toString(), 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
) {
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
)
}
}
}
}
}
}
} 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)
) )
} }
val uriHandler = LocalUriHandler.current
val cameraPermissionState = rememberPermissionState(
Manifest.permission.CAMERA
)
Row( LaunchedEffect(key1 = code) {
modifier = Modifier.fillMaxWidth(), if (doNotAsk && URLUtil.isValidUrl(code)) {
horizontalArrangement = Arrangement.End uriHandler.openUri(code)
) { code = ""
TextButton(onClick = { }
launcher.launch(Manifest.permission.CAMERA) }
}) {
Text( LaunchedEffect(key1 = doNotAsk) {
text = "Grant Permission", with(sharedPref.edit()) {
style = MaterialTheme.typography.labelLarge putBoolean(autoOpenKey, doNotAsk)
) apply()
}
}
Column(modifier = Modifier.fillMaxSize()) {
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"
) {
cameraPermissionState.launchPermissionRequest()
} }
} }
} }
} }
} }
} }
} }

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> <resources>
<string name="app_name">QScan</string> <string name="app_name">QScan</string>
<string name="should_auto_open_key">AUTO_OPEN</string>
</resources> </resources>