-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8148 from thunderbird/qr_code_scanner
Add QR code scanner UI (part 1)
- Loading branch information
Showing
20 changed files
with
705 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<!-- | ||
TODO: This file only exists for manual testing during development. Remove when integrating the feature into the app. | ||
--> | ||
<manifest | ||
xmlns:android="http://schemas.android.com/apk/res/android" | ||
xmlns:tools="http://schemas.android.com/tools" | ||
> | ||
|
||
<application | ||
android:name=".ui.QrCodeApplication" | ||
tools:ignore="MissingApplicationIcon" | ||
> | ||
|
||
<activity | ||
android:name=".ui.QrCodeScannerActivity" | ||
android:exported="true" | ||
android:theme="@style/Theme.Material3.Light.NoActionBar" | ||
> | ||
<intent-filter> | ||
<action android:name="android.intent.action.MAIN" /> | ||
|
||
<category android:name="android.intent.category.DEFAULT" /> | ||
<category android:name="android.intent.category.LAUNCHER" /> | ||
</intent-filter> | ||
|
||
</activity> | ||
|
||
</application> | ||
|
||
</manifest> |
12 changes: 12 additions & 0 deletions
12
...ode/src/debug/kotlin/app/k9mail/feature/migration/qrcode/ui/NoOpQrCodeScannerViewModel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package app.k9mail.feature.migration.qrcode.ui | ||
|
||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel | ||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerContract.Effect | ||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerContract.Event | ||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerContract.State | ||
|
||
class NoOpQrCodeScannerViewModel( | ||
initialState: State = State(), | ||
) : BaseViewModel<State, Event, Effect>(initialState), QrCodeScannerContract.ViewModel { | ||
override fun event(event: Event) = Unit | ||
} |
18 changes: 18 additions & 0 deletions
18
...src/debug/kotlin/app/k9mail/feature/migration/qrcode/ui/PermissionDeniedContentPreview.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package app.k9mail.feature.migration.qrcode.ui | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes | ||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme | ||
import app.k9mail.core.ui.compose.designsystem.atom.Surface | ||
|
||
@PreviewScreenSizes | ||
@Composable | ||
fun PermissionDeniedContentPreview() { | ||
PreviewWithTheme { | ||
Surface { | ||
PermissionDeniedContent( | ||
onGoToSettingsClick = {}, | ||
) | ||
} | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
...ation/qrcode/src/debug/kotlin/app/k9mail/feature/migration/qrcode/ui/QrCodeApplication.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package app.k9mail.feature.migration.qrcode.ui | ||
|
||
import android.app.Application | ||
import android.content.Context | ||
import app.k9mail.feature.migration.qrcode.qrCodeModule | ||
import org.koin.android.ext.koin.androidContext | ||
import org.koin.core.context.startKoin | ||
|
||
// TODO: This only exists for manual testing during development. Remove when integrating the feature into the app. | ||
class QrCodeApplication : Application() { | ||
override fun attachBaseContext(base: Context?) { | ||
super.attachBaseContext(base) | ||
|
||
startKoin { | ||
androidContext(this@QrCodeApplication) | ||
modules(qrCodeModule) | ||
} | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
...n/qrcode/src/debug/kotlin/app/k9mail/feature/migration/qrcode/ui/QrCodeScannerActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package app.k9mail.feature.migration.qrcode.ui | ||
|
||
import android.os.Bundle | ||
import androidx.activity.ComponentActivity | ||
import androidx.activity.enableEdgeToEdge | ||
import app.k9mail.core.ui.compose.common.activity.setActivityContent | ||
import app.k9mail.core.ui.compose.theme2.k9mail.K9MailTheme2 | ||
|
||
// TODO: This only exists for manual testing during development. Remove when integrating the feature into the app. | ||
class QrCodeScannerActivity : ComponentActivity() { | ||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
|
||
enableEdgeToEdge() | ||
|
||
setActivityContent { | ||
K9MailTheme2 { | ||
QrCodeScannerScreen() | ||
} | ||
} | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
...ode/src/debug/kotlin/app/k9mail/feature/migration/qrcode/ui/QrCodeScannerScreenPreview.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package app.k9mail.feature.migration.qrcode.ui | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme | ||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerContract.State | ||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerContract.UiPermissionState | ||
|
||
@Preview | ||
@Composable | ||
fun QrCodeScannerScreenPreview_permission_unknown() { | ||
PreviewWithTheme { | ||
QrCodeScannerScreen( | ||
viewModel = NoOpQrCodeScannerViewModel( | ||
initialState = State(cameraPermissionState = UiPermissionState.Unknown), | ||
), | ||
) | ||
} | ||
} | ||
|
||
@Preview | ||
@Composable | ||
fun QrCodeScannerScreenPreview_permission_granted() { | ||
PreviewWithTheme { | ||
QrCodeScannerScreen( | ||
viewModel = NoOpQrCodeScannerViewModel( | ||
initialState = State(cameraPermissionState = UiPermissionState.Granted), | ||
), | ||
) | ||
} | ||
} | ||
|
||
@Preview | ||
@Composable | ||
fun QrCodeScannerScreenPreview_permission_denied() { | ||
PreviewWithTheme { | ||
QrCodeScannerScreen( | ||
viewModel = NoOpQrCodeScannerViewModel( | ||
initialState = State(cameraPermissionState = UiPermissionState.Denied), | ||
), | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
||
<uses-feature android:name="android.hardware.camera" android:required="false" /> | ||
<uses-permission android:name="android.permission.CAMERA"/> | ||
|
||
</manifest> |
9 changes: 9 additions & 0 deletions
9
feature/migration/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/QrCodeModule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package app.k9mail.feature.migration.qrcode | ||
|
||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerViewModel | ||
import org.koin.androidx.viewmodel.dsl.viewModel | ||
import org.koin.dsl.module | ||
|
||
val qrCodeModule = module { | ||
viewModel { QrCodeScannerViewModel() } | ||
} |
49 changes: 49 additions & 0 deletions
49
.../qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/ui/PermissionDeniedContent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package app.k9mail.feature.migration.qrcode.ui | ||
|
||
import androidx.compose.foundation.layout.Arrangement | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.Spacer | ||
import androidx.compose.foundation.layout.fillMaxHeight | ||
import androidx.compose.foundation.layout.height | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.platform.testTag | ||
import androidx.compose.ui.res.stringResource | ||
import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonFilled | ||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyLarge | ||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleLarge | ||
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveContent | ||
import app.k9mail.core.ui.compose.theme2.MainTheme | ||
import app.k9mail.feature.migration.qrcode.R | ||
|
||
@Composable | ||
internal fun PermissionDeniedContent( | ||
onGoToSettingsClick: () -> Unit, | ||
) { | ||
ResponsiveContent( | ||
modifier = Modifier.testTag("PermissionDeniedContent"), | ||
) { | ||
Column( | ||
verticalArrangement = Arrangement.Center, | ||
modifier = Modifier | ||
.fillMaxHeight() | ||
.padding(MainTheme.spacings.double), | ||
) { | ||
TextTitleLarge(text = stringResource(R.string.migration_qrcode_permission_denied_title)) | ||
Spacer(modifier = Modifier.height(MainTheme.spacings.double)) | ||
|
||
TextBodyLarge(text = stringResource(R.string.migration_qrcode_permission_denied_message)) | ||
Spacer(modifier = Modifier.height(MainTheme.spacings.triple)) | ||
|
||
ButtonFilled( | ||
text = stringResource(R.string.migration_qrcode_go_to_settings_button_text), | ||
onClick = onGoToSettingsClick, | ||
modifier = Modifier | ||
.align(Alignment.CenterHorizontally) | ||
.testTag("GoToSettingsButton"), | ||
) | ||
} | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
...ion/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/ui/QrCodeScannerContent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package app.k9mail.feature.migration.qrcode.ui | ||
|
||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.safeDrawingPadding | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
import androidx.lifecycle.Lifecycle | ||
import androidx.lifecycle.compose.LifecycleEventEffect | ||
import app.k9mail.core.ui.compose.designsystem.atom.Surface | ||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerContract.Event | ||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerContract.State | ||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerContract.UiPermissionState | ||
|
||
@Composable | ||
internal fun QrCodeScannerContent( | ||
state: State, | ||
onEvent: (Event) -> Unit, | ||
) { | ||
Surface( | ||
modifier = Modifier | ||
.fillMaxSize() | ||
.safeDrawingPadding(), | ||
) { | ||
when (state.cameraPermissionState) { | ||
UiPermissionState.Unknown -> { | ||
// Display empty surface while we're waiting for the camera permission request to return a result | ||
} | ||
UiPermissionState.Granted -> { | ||
QrCodeScannerView() | ||
} | ||
UiPermissionState.Denied -> { | ||
PermissionDeniedContent( | ||
onGoToSettingsClick = { onEvent(Event.GoToSettingsClicked) }, | ||
) | ||
} | ||
UiPermissionState.Waiting -> { | ||
// We've launched Android's app info screen and are now waiting for the user to return to our app. | ||
|
||
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { | ||
// Once the user has returned to the app, notify the view model about it. | ||
onEvent(Event.ReturnedFromAppInfoScreen) | ||
} | ||
} | ||
} | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
...on/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/ui/QrCodeScannerContract.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package app.k9mail.feature.migration.qrcode.ui | ||
|
||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel | ||
|
||
interface QrCodeScannerContract { | ||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect> | ||
|
||
data class State( | ||
val cameraPermissionState: UiPermissionState = UiPermissionState.Unknown, | ||
) | ||
|
||
sealed interface Event { | ||
data object StartScreen : Event | ||
data class CameraPermissionResult(val success: Boolean) : Event | ||
data object GoToSettingsClicked : Event | ||
data object ReturnedFromAppInfoScreen : Event | ||
} | ||
|
||
sealed interface Effect { | ||
data object RequestCameraPermission : Effect | ||
data object GoToAppInfoScreen : Effect | ||
} | ||
|
||
enum class UiPermissionState { | ||
Unknown, | ||
Granted, | ||
Denied, | ||
Waiting, | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
...tion/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/ui/QrCodeScannerScreen.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package app.k9mail.feature.migration.qrcode.ui | ||
|
||
import android.Manifest | ||
import android.content.ActivityNotFoundException | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.net.Uri | ||
import android.provider.Settings | ||
import androidx.activity.compose.ManagedActivityResultLauncher | ||
import androidx.activity.compose.rememberLauncherForActivityResult | ||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.LaunchedEffect | ||
import androidx.compose.ui.platform.LocalContext | ||
import app.k9mail.core.ui.compose.common.mvi.observe | ||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerContract.Effect | ||
import app.k9mail.feature.migration.qrcode.ui.QrCodeScannerContract.Event | ||
import org.koin.androidx.compose.koinViewModel | ||
import timber.log.Timber | ||
|
||
@Composable | ||
fun QrCodeScannerScreen( | ||
viewModel: QrCodeScannerContract.ViewModel = koinViewModel<QrCodeScannerViewModel>(), | ||
) { | ||
val cameraPermissionLauncher = rememberLauncherForActivityResult(RequestPermission()) { success -> | ||
viewModel.event(Event.CameraPermissionResult(success)) | ||
} | ||
|
||
val context = LocalContext.current | ||
|
||
val (state, dispatch) = viewModel.observe { effect -> | ||
when (effect) { | ||
Effect.RequestCameraPermission -> cameraPermissionLauncher.requestCameraPermission() | ||
Effect.GoToAppInfoScreen -> context.goToAppInfoScreen() | ||
} | ||
} | ||
|
||
LaunchedEffect(key1 = Unit) { | ||
dispatch(Event.StartScreen) | ||
} | ||
|
||
QrCodeScannerContent( | ||
state = state.value, | ||
onEvent = dispatch, | ||
) | ||
} | ||
|
||
private fun Context.goToAppInfoScreen() { | ||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { | ||
data = Uri.fromParts("package", packageName, null) | ||
} | ||
|
||
try { | ||
startActivity(intent) | ||
} catch (e: ActivityNotFoundException) { | ||
Timber.e(e, "Error opening Android's app settings") | ||
} | ||
} | ||
|
||
private fun ManagedActivityResultLauncher<String, Boolean>.requestCameraPermission() { | ||
launch(Manifest.permission.CAMERA) | ||
} |
Oops, something went wrong.