Skip to content

Commit

Permalink
Merge branch 'main' into production-server-migration
Browse files Browse the repository at this point in the history
# Conflicts:
#	app/src/main/java/tech/relaycorp/letro/contacts/ui/ManageContactScreen.kt
  • Loading branch information
gnarea committed Sep 24, 2023
2 parents 6ead903 + a9b1340 commit 4a82171
Show file tree
Hide file tree
Showing 13 changed files with 211 additions and 31 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ dependencies {
// Utils:
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation 'com.google.code.gson:gson:2.10.1' // TODO: remove
implementation "com.google.accompanist:accompanist-permissions:0.33.1-alpha"


// Testing
def junitVersion = "5.8.2"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<application
android:name=".App"
android:allowBackup="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tech.relaycorp.letro.R
import tech.relaycorp.letro.contacts.ManageContactScreenContent.Companion.REQUEST_SENT
import tech.relaycorp.letro.contacts.ManageContactViewModel.Type.Companion.EDIT_CONTACT
import tech.relaycorp.letro.contacts.ManageContactViewModel.Type.Companion.NEW_CONTACT
import tech.relaycorp.letro.contacts.model.Contact
Expand Down Expand Up @@ -61,6 +62,10 @@ class ManageContactViewModel @Inject constructor(
val goBackSignal: SharedFlow<Unit>
get() = _goBackSignal

private val _showPermissionGoToSettingsSignal = MutableSharedFlow<Unit>()
val showPermissionGoToSettingsSignal: SharedFlow<Unit>
get() = _showPermissionGoToSettingsSignal

private val contacts: HashSet<Contact> = hashSetOf()

private var editingContact: Contact? = null
Expand Down Expand Up @@ -127,7 +132,7 @@ class ManageContactViewModel @Inject constructor(
viewModelScope.launch {
_uiState.update {
it.copy(
showRequestSentScreen = true,
content = REQUEST_SENT,
)
}
}
Expand All @@ -149,6 +154,19 @@ class ManageContactViewModel @Inject constructor(
}
}

fun onNotificationPermissionResult(isGranted: Boolean) {
viewModelScope.launch {
if (!isGranted) {
_showPermissionGoToSettingsSignal.emit(Unit)
}
_uiState.update {
it.copy(
showNotificationPermissionRequestIfNoPermission = false,
)
}
}
}

private fun checkIfIdIsCorrect(id: String) {
viewModelScope.launch {
val isValidId = id.matches(CORRECT_ID_REGEX)
Expand Down Expand Up @@ -218,7 +236,8 @@ data class PairWithOthersUiState(
val isSentRequestAgainHintVisible: Boolean = false,
val isVeraIdInputEnabled: Boolean = true,
val pairingErrorCaption: PairingErrorCaption? = null,
val showRequestSentScreen: Boolean = false,
val showNotificationPermissionRequestIfNoPermission: Boolean = true,
@ManageContactScreenContent val content: Int = ManageContactScreenContent.MANAGE_CONTACT,
)

@Immutable
Expand All @@ -240,3 +259,11 @@ sealed class ManageContactTexts(
data class PairingErrorCaption(
@StringRes val message: Int,
)

annotation class ManageContactScreenContent {

companion object {
const val MANAGE_CONTACT = 0
const val REQUEST_SENT = 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
Expand All @@ -18,10 +20,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import tech.relaycorp.letro.R
import tech.relaycorp.letro.contacts.ManageContactScreenContent
import tech.relaycorp.letro.contacts.ManageContactViewModel
import tech.relaycorp.letro.contacts.PairWithOthersUiState
import tech.relaycorp.letro.onboarding.actionTaking.ActionTakingScreen
Expand All @@ -30,16 +34,27 @@ import tech.relaycorp.letro.ui.common.LetroButtonMaxWidthFilled
import tech.relaycorp.letro.ui.common.LetroInfoView
import tech.relaycorp.letro.ui.common.LetroOutlinedTextField
import tech.relaycorp.letro.ui.theme.HorizontalScreenPadding
import tech.relaycorp.letro.ui.theme.LetroTheme
import tech.relaycorp.letro.ui.utils.SnackbarStringsProvider
import tech.relaycorp.letro.utils.permission.rememberNotificationPermissionStateCompat

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun ManageContactScreen(
onBackClick: () -> Unit,
onEditContactCompleted: (String) -> Unit,
snackbarHostState: SnackbarHostState,
snackbarStringsProvider: SnackbarStringsProvider,
onGoToSettingsClick: () -> Unit,
viewModel: ManageContactViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsState()

val notificationsPermissionState = rememberNotificationPermissionStateCompat(
onPermissionResult = {
viewModel.onNotificationPermissionResult(it)
},
)

LaunchedEffect(Unit) {
viewModel.onEditContactCompleted.collect {
onEditContactCompleted(it)
Expand All @@ -52,21 +67,49 @@ fun ManageContactScreen(
}
}

if (!uiState.showRequestSentScreen) {
ManageContactView(
onBackClick,
uiState,
viewModel,
)
} else {
ActionTakingScreen(
actionTakingScreenUIStateModel = ActionTakingScreenUIStateModel.PairingRequestSent(
boldPartOfMessage = uiState.veraidId,
onGotItClicked = {
viewModel.onGotItClick()
},
),
)
LaunchedEffect(Unit) {
viewModel.showPermissionGoToSettingsSignal.collect {
val result = snackbarHostState.showSnackbar(
message = snackbarStringsProvider.notificationPermissionDenied,
actionLabel = snackbarStringsProvider.goToSettings,
)
if (result == SnackbarResult.ActionPerformed) {
onGoToSettingsClick()
}
}
}

when (uiState.content) {
ManageContactScreenContent.MANAGE_CONTACT -> {
ManageContactView(
onBackClick,
uiState,
viewModel,
)
}
ManageContactScreenContent.REQUEST_SENT -> {
if (!notificationsPermissionState.status.isGranted && uiState.showNotificationPermissionRequestIfNoPermission) {
ActionTakingScreen(
actionTakingScreenUIStateModel = ActionTakingScreenUIStateModel.PairingRequestSentWithPermissionRequest(
onRequestPermissionClick = {
notificationsPermissionState.launchPermissionRequest()
},
onSkipClicked = {
viewModel.onGotItClick()
},
),
)
} else {
ActionTakingScreen(
actionTakingScreenUIStateModel = ActionTakingScreenUIStateModel.PairingRequestSent(
boldPartOfMessage = uiState.veraidId,
onGotItClicked = {
viewModel.onGotItClick()
},
),
)
}
}
}
}

Expand Down Expand Up @@ -157,15 +200,3 @@ private fun ManageContactView(
)
}
}

@Preview(showBackground = true)
@Composable
private fun UseExistingAccountPreview() {
LetroTheme {
ManageContactScreen(
onBackClick = {},
onEditContactCompleted = {},
viewModel = hiltViewModel(),
)
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/tech/relaycorp/letro/di/NotificationModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tech.relaycorp.letro.di

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import tech.relaycorp.letro.notification.NotificationPermissionManager
import tech.relaycorp.letro.notification.NotificationPermissionManagerImpl
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
interface NotificationModule {

@Binds
@Singleton
fun bindNotificationPermissionManager(
impl: NotificationPermissionManagerImpl,
): NotificationPermissionManager
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package tech.relaycorp.letro.notification

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

interface NotificationPermissionManager {

fun isPermissionGranted(): Boolean
}

class NotificationPermissionManagerImpl @Inject constructor(
@ApplicationContext private val context: Context,
) : NotificationPermissionManager {

override fun isPermissionGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
} else {
true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ fun ActionTakingScreen(
text = stringResource(id = actionTakingScreenUIStateModel.messageStringRes),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
)
} else {
BoldText(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,17 @@ sealed class ActionTakingScreenUIStateModel(
buttonFilledStringRes = R.string.onboarding_pairing_request_sent_button,
onButtonFilledClicked = onGotItClicked,
)

class PairingRequestSentWithPermissionRequest(
onRequestPermissionClick: () -> Unit,
onSkipClicked: () -> Unit,
) : ActionTakingScreenUIStateModel(
titleStringRes = R.string.onboarding_pairing_request_sent_title,
image = R.drawable.pairing_request_sent,
messageStringRes = R.string.we_need_your_permission,
buttonFilledStringRes = R.string.grant_permission,
buttonOutlinedStringRes = R.string.skip,
onButtonFilledClicked = onRequestPermissionClick,
onButtonOutlinedClicked = onSkipClicked,
)
}
9 changes: 9 additions & 0 deletions app/src/main/java/tech/relaycorp/letro/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
Expand Down Expand Up @@ -48,12 +49,20 @@ class MainActivity : ComponentActivity() {
LetroNavHost(
navController = navController,
stringsProvider = stringsProvider,
onGoToSettingsClick = { goToSettings() },
)
}
}
}
}

private fun goToSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
}

private fun observeViewModel() {
lifecycleScope.launch(Dispatchers.Main) {
viewModel.openLinkSignal.collect { link ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import tech.relaycorp.letro.utils.navigation.navigateWithPoppingAllBackStack
fun LetroNavHost(
navController: NavHostController,
stringsProvider: StringsProvider,
onGoToSettingsClick: () -> Unit,
mainViewModel: MainViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(),
) {
Expand Down Expand Up @@ -206,6 +207,9 @@ fun LetroNavHost(
else -> throw IllegalStateException("Unknown screen type: $type")
}
},
snackbarHostState = snackbarHostState,
snackbarStringsProvider = stringsProvider.snackbar,
onGoToSettingsClick = onGoToSettingsClick,
)
}
composable(Route.Home.name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface SnackbarStringsProvider {
val conversationDeleted: String
val conversationArchived: String
val conversationUnarchived: String
val notificationPermissionDenied: String
val goToSettings: String
}

class SnackbarStringsProviderImpl @Inject constructor(
Expand All @@ -34,4 +36,10 @@ class SnackbarStringsProviderImpl @Inject constructor(

override val conversationUnarchived: String
get() = activity.getString(R.string.snackbar_conversation_unarchived)

override val notificationPermissionDenied: String
get() = activity.getString(R.string.we_need_your_permission)

override val goToSettings: String
get() = activity.getString(R.string.go_to_settings)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package tech.relaycorp.letro.utils.permission

import android.Manifest
import android.os.Build
import androidx.compose.runtime.Composable
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun rememberNotificationPermissionStateCompat(
onPermissionResult: (Boolean) -> Unit,
) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
rememberPermissionState(
permission = Manifest.permission.POST_NOTIFICATIONS,
onPermissionResult = onPermissionResult,
)
} else {
object : PermissionState {
override val permission: String
get() = "Mock_For_Api_Before_33"

override val status: PermissionStatus
get() = PermissionStatus.Granted

override fun launchPermissionRequest() {
// Do nothing
}
}
}
Loading

0 comments on commit 4a82171

Please sign in to comment.