Skip to content

Commit

Permalink
Merge branch 'feat/desktop' into 'master'
Browse files Browse the repository at this point in the history
Restyle invitation scanner + Cleanup dependencies

See merge request fmasa/wfrp-master!222
  • Loading branch information
fmasa committed Oct 27, 2021
2 parents 326494c + 930ff08 commit f6f6711
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 102 deletions.
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ android {
"-Xopt-in=androidx.compose.animation.ExperimentalAnimationApi" +
"-Xopt-in=androidx.compose.animation.ExperimentalFoundationApi" +
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi" +
"-Xopt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi" +
"-P" +
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
}
Expand All @@ -111,7 +112,7 @@ dependencies {
implementation("com.google.android.gms:play-services-auth:19.0.0")

// Permission management
implementation("com.sagar:coroutinespermission:2.0.3")
implementation("com.google.accompanist:accompanist-permissions:0.20.0")

// QR code scanning
implementation("com.google.zxing:core:3.3.3")
Expand Down
31 changes: 13 additions & 18 deletions app/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ android {
//
// Firestore emulator setup
//
val propertiesFile = File("local.properties")

val properties = if (propertiesFile.exists())
val properties = if (File("local.properties").exists())
loadProperties("local.properties")
else Properties()

Expand Down Expand Up @@ -51,27 +49,26 @@ dependencies {
api("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07")

// Koin
api("org.koin:koin-android:2.2.0")
api("org.koin:koin-androidx-viewmodel:2.2.0")
api("io.insert-koin:koin-android:3.1.2")

// Coil - image library
implementation("io.coil-kt:coil-compose:1.3.2")

// Firebase-related dependencies
api("com.google.firebase:firebase-analytics:19.0.0")
api("com.firebaseui:firebase-ui-auth:6.2.0")
api("com.google.firebase:firebase-firestore-ktx:23.0.1")
api("com.google.firebase:firebase-analytics-ktx:19.0.0")
api("com.google.firebase:firebase-crashlytics:18.1.0")
api("com.google.firebase:firebase-dynamic-links-ktx:20.1.0")
api("com.google.firebase:firebase-functions-ktx:20.0.1")
api(platform("com.google.firebase:firebase-bom:28.4.2"))
api("com.google.firebase:firebase-analytics-ktx")
api("com.google.firebase:firebase-auth-ktx")
api("com.google.firebase:firebase-firestore-ktx")
api("com.google.firebase:firebase-crashlytics")
api("com.google.firebase:firebase-dynamic-links-ktx")
api("com.google.firebase:firebase-functions-ktx")

// Logging
api("com.jakewharton.timber:timber:4.7.1")

// Coroutines
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0")
api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
api("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.3.5")
api("androidx.lifecycle:lifecycle-livedata-ktx:2.3.1")

Expand All @@ -88,17 +85,15 @@ dependencies {
api("com.revenuecat.purchases:purchases:4.0.2")

// Ads
api("com.google.android.gms:play-services-ads:20.2.0")
api("com.google.android.gms:play-services-ads:20.4.0")

// Shared Preferences DataStore
implementation("androidx.datastore:datastore-preferences:1.0.0-alpha05")
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("com.google.firebase:firebase-auth-ktx:21.0.1")

// HTTP Client
val ktorVersion = "1.6.0"
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-client-serialization:$ktorVersion")

api("com.google.accompanist:accompanist-flowlayout:0.12.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package cz.frantisekmasa.wfrp_master.core.viewModel
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.preferencesKey
import androidx.datastore.preferences.createDataStore
import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
Expand All @@ -24,6 +24,8 @@ import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.getViewModel
import timber.log.Timber

private val Context.settingsDataStore by preferencesDataStore("settings")

class SettingsViewModel(
context: Context,
private val parties: PartyRepository,
Expand All @@ -35,7 +37,7 @@ class SettingsViewModel(
val soundEnabled: LiveData<Boolean> by lazy { getPreference(AppSettings.SOUND_ENABLED, true) }
val personalizedAds: LiveData<Boolean> by lazy { getPreference(AppSettings.PERSONALIZED_ADS, false) }

private val dataStore = context.createDataStore("settings")
private val dataStore = context.settingsDataStore

suspend fun initializeAds() {
val personalizedAds = refreshPersonalizedAdConsent()
Expand Down Expand Up @@ -104,10 +106,10 @@ class SettingsViewModel(
}

private object AppSettings {
val DARK_MODE = preferencesKey<Boolean>("dark_mode")
val SOUND_ENABLED = preferencesKey<Boolean>("sound_enabled")
val GOOGLE_SIGN_IN_DISMISSED = preferencesKey<Boolean>("dismissed_google_sign_in")
val PERSONALIZED_ADS = preferencesKey<Boolean>("personalized_ads")
val DARK_MODE = booleanPreferencesKey("dark_mode")
val SOUND_ENABLED = booleanPreferencesKey("sound_enabled")
val GOOGLE_SIGN_IN_DISMISSED = booleanPreferencesKey("dismissed_google_sign_in")
val PERSONALIZED_ADS = booleanPreferencesKey("personalized_ads")
}

@Composable
Expand Down
198 changes: 126 additions & 72 deletions app/src/main/java/cz/muni/fi/rpg/ui/joinParty/InvitationScannerScreen.kt
Original file line number Diff line number Diff line change
@@ -1,45 +1,56 @@
package cz.muni.fi.rpg.ui.joinParty

import android.Manifest
import android.os.Parcelable
import android.widget.Toast
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.eazypermissions.common.model.PermissionResult
import com.eazypermissions.coroutinespermission.PermissionManager
import androidx.compose.ui.text.style.TextAlign
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.rememberPermissionState
import cz.frantisekmasa.wfrp_master.core.domain.party.Invitation
import cz.frantisekmasa.wfrp_master.core.ui.buttons.BackButton
import cz.frantisekmasa.wfrp_master.core.ui.primitives.HorizontalLine
import cz.frantisekmasa.wfrp_master.core.ui.primitives.Spacing
import cz.frantisekmasa.wfrp_master.core.ui.scaffolding.SubheadBar
import cz.frantisekmasa.wfrp_master.core.ui.viewinterop.LocalActivity
import cz.frantisekmasa.wfrp_master.navigation.Route
import cz.frantisekmasa.wfrp_master.navigation.Routing
import cz.muni.fi.rpg.R
import cz.muni.fi.rpg.ui.common.toast
import cz.muni.fi.rpg.viewModels.JoinPartyViewModel
import cz.muni.fi.rpg.viewModels.provideJoinPartyViewModel
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize

@Composable
fun InvitationScannerScreen(routing: Routing<Route>) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.qr_scan_prompt)) },
title = { Text(stringResource(R.string.title_joinParty)) },
navigationIcon = {
BackButton(onClick = { routing.pop() })
},
Expand All @@ -54,81 +65,124 @@ fun InvitationScannerScreen(routing: Routing<Route>) {
modifier = Modifier.fillMaxHeight()
) {
val viewModel = provideJoinPartyViewModel()
val coroutineScope = rememberCoroutineScope()

var screenState: InvitationScannerScreenState by rememberSaveable {
mutableStateOf(InvitationScannerScreenState.WaitingForPermissions)
}
var invitation: Invitation? by rememberSaveable { mutableStateOf(null) }

when (val state = screenState) {
InvitationScannerScreenState.WaitingForPermissions -> {
val activity = LocalActivity.current

LaunchedEffect(null) {
val permissionResult = PermissionManager.requestPermissions(
activity,
PermissionRequestCode,
Manifest.permission.CAMERA,
)

when (permissionResult) {
is PermissionResult.PermissionGranted -> {
screenState = InvitationScannerScreenState.Scanning
}
else -> {
// TODO: Add more specific wording for types of denial
// see https://github.com/sagar-viradiya/eazypermissions#coroutines-support

activity.toast(
activity.getString(R.string.error_camera_permission_required),
Toast.LENGTH_LONG
)

routing.pop()
}
}
}
}
InvitationScannerScreenState.Scanning -> {
SubheadBar(stringResource(R.string.qr_scan_prompt))
QrCodeScanner(
modifier = Modifier.fillMaxSize(),
onSuccessfulScan = { qrCodeData ->
coroutineScope.launch {
viewModel.deserializeInvitationJson(qrCodeData)?.let {
screenState =
InvitationScannerScreenState.WaitingForUserConfirmation(it)
}
}
},
)
}
is InvitationScannerScreenState.WaitingForUserConfirmation -> {
when {
invitation != null -> {
InvitationConfirmation(
state.invitation,
invitation!!,
viewModel,
onSuccess = {
routing.pop()
},
onError = {
screenState = InvitationScannerScreenState.Scanning
},
onSuccess = { routing.pop() },
onError = { invitation = null },
)
}
else -> {
Scanner(viewModel, onSuccessfulScan = { invitation = it })
}
}
}
}
}

private sealed class InvitationScannerScreenState : Parcelable {
@Parcelize
object WaitingForPermissions : InvitationScannerScreenState()
@Composable
private fun Scanner(viewModel: JoinPartyViewModel, onSuccessfulScan: (Invitation) -> Unit) {
val coroutineScope = rememberCoroutineScope()
val camera = rememberPermissionState(Manifest.permission.CAMERA)

@Parcelize
object Scanning : InvitationScannerScreenState()
when {
camera.hasPermission -> {
SubheadBar(stringResource(R.string.qr_scan_prompt))
QrCodeScanner(
modifier = Modifier.fillMaxSize(),
onSuccessfulScan = { qrCodeData ->
coroutineScope.launch {
viewModel.deserializeInvitationJson(qrCodeData)
?.let(onSuccessfulScan)
}
},
)
}
!camera.permissionRequested || camera.shouldShowRationale -> PermissionRequestScreen(camera)
else -> PermissionDeniedScreen()
}
}

@Composable
private fun PermissionRequestScreen(camera: PermissionState) {
ScreenBody {
if (!camera.permissionRequested) {
SideEffect { camera.launchPermissionRequest() }
}

@Parcelize
data class WaitingForUserConfirmation(val invitation: Invitation) : InvitationScannerScreenState()
Text(
stringResource(R.string.camera_permission_required),
style = MaterialTheme.typography.h6,
)

Rationale()

TextButton(onClick = { camera.launchPermissionRequest() }) {
Text(stringResource(R.string.button_request_permission).uppercase())
}

Alternative()
}
}

private const val PermissionRequestCode = 10
@Composable
private fun PermissionDeniedScreen() {
ScreenBody {
Text(stringResource(R.string.camera_permission_denied), style = MaterialTheme.typography.h6)
Rationale()
Text(stringResource(R.string.camera_permission_instructions), textAlign = TextAlign.Center)

val context = LocalContext.current
TextButton(onClick = { context.openApplicationSettings() }) {
Text(stringResource(R.string.button_open_settings).uppercase())
}

Alternative()
}
}

@Composable
private inline fun ScreenBody(content: @Composable ColumnScope.() -> Unit) {
Column(
Modifier
.fillMaxWidth()
.padding(Spacing.bodyPadding)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
content = content,
)
}

@Composable
private fun Rationale() {
Text(
stringResource(R.string.camera_permission_rationale),
textAlign = TextAlign.Center,
)
}

@Composable
private fun Alternative() {

HorizontalLine()

Text(
stringResource(R.string.camera_permission_alternative),
modifier = Modifier.padding(top = Spacing.mediumLarge),
textAlign = TextAlign.Center,
)
}

private fun Context.openApplicationSettings() {
startActivity(
Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts("package", packageName, null)
}
)
}
9 changes: 9 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,14 @@
<string name="error_file_opening_crashed">Could not open file</string>
<string name="message_avatar_changed">Avatar was changed</string>
<string name="message_avatar_removed">Avatar was removed</string>
<string name="button_open_settings">Open Settings</string>
<string name="camera_permission_denied">Camera permission denied</string>
<string name="camera_permission_required">Camera permission required</string>
<string name="camera_permission_rationale">Access to camera is needed to let you scan QR codes with party invitations.</string>
<string name="camera_permission_alternative">Alternatively you can ask your GM for invitation link.</string>
<string name="camera_permission_instructions">
Please, grant us access on the Settings screen.
</string>
<string name="button_request_permission">Request permission</string>

</resources>
Loading

0 comments on commit f6f6711

Please sign in to comment.