Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Awala installation progress + Move AwalaNotInstalledScreen + AwalaInitializationInProgressScreen to Root navigation! #61

Merged
merged 4 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'

// Awala
implementation 'com.github.relaycorp:awala-endpoint-android:1.13.22'
implementation 'com.github.relaycorp:awala-endpoint-android:1.13.23'

// Compose
implementation platform('androidx.compose:compose-bom:2023.09.00')
Expand Down
85 changes: 57 additions & 28 deletions app/src/main/java/tech/relaycorp/letro/awala/AwalaManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ package tech.relaycorp.letro.awala
import android.content.Context
import android.util.Base64
import android.util.Log
import androidx.annotation.IntDef
import androidx.annotation.RawRes
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.withContext
Expand All @@ -23,23 +28,30 @@ import tech.relaycorp.awaladroid.endpoint.PublicThirdPartyEndpoint
import tech.relaycorp.awaladroid.endpoint.ThirdPartyEndpoint
import tech.relaycorp.awaladroid.messaging.OutgoingMessage
import tech.relaycorp.letro.R
import tech.relaycorp.letro.awala.AwalaInitializationState.Companion.AWALA_NOT_INSTALLED
import tech.relaycorp.letro.awala.AwalaInitializationState.Companion.AWALA_SET_UP
import tech.relaycorp.letro.awala.AwalaInitializationState.Companion.FIRST_PARTY_ENDPOINT_REGISTRED
import tech.relaycorp.letro.awala.AwalaInitializationState.Companion.GATEWAY_CLIENT_BINDED
import tech.relaycorp.letro.awala.AwalaInitializationState.Companion.INITIALIZED
import tech.relaycorp.letro.awala.AwalaInitializationState.Companion.NOT_INITIALIZED
import tech.relaycorp.letro.awala.message.AwalaOutgoingMessage
import tech.relaycorp.letro.awala.message.MessageRecipient
import tech.relaycorp.letro.awala.message.MessageType
import tech.relaycorp.letro.awala.processor.AwalaMessageProcessor
import tech.relaycorp.letro.ui.navigation.Route
import tech.relaycorp.letro.utils.awala.loadNonNullPrivateThirdPartyEndpoint
import tech.relaycorp.letro.utils.awala.loadNonNullPublicFirstPartyEndpoint
import tech.relaycorp.letro.utils.awala.loadNonNullPublicThirdPartyEndpoint
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject

interface AwalaManager {
val awalaInitializationState: StateFlow<Int>
val awalaUnsuccessfulBindings: SharedFlow<Unit>
suspend fun sendMessage(
outgoingMessage: AwalaOutgoingMessage,
recipient: MessageRecipient,
)
suspend fun isAwalaInstalled(currentScreen: Route): Boolean
fun initializeGatewayAsync()

suspend fun authorizeUsers(
// TODO: after MVP handle several first party endpoints
thirdPartyPublicKey: ByteArray,
Expand All @@ -63,11 +75,14 @@ class AwalaManagerImpl @Inject constructor(
@OptIn(DelicateCoroutinesApi::class)
private val messageReceivingThreadContext = newSingleThreadContext("AwalaManagerMessageReceiverThread")

private var isAwalaSetUp = AtomicBoolean(false)
private val _awalaInitializationState = MutableStateFlow<Int>(AwalaInitializationState.NOT_INITIALIZED)
override val awalaInitializationState: StateFlow<Int>
get() = _awalaInitializationState
private var awalaSetupJob: Job? = null

@Volatile
private var isAwalaInstalledOnDevice: Boolean? = null
private val _awalaUnsuccessfulBindings = MutableSharedFlow<Unit>()
override val awalaUnsuccessfulBindings: SharedFlow<Unit>
get() = _awalaUnsuccessfulBindings

private var isReceivingMessages = false

Expand All @@ -79,8 +94,8 @@ class AwalaManagerImpl @Inject constructor(
withContext(awalaThreadContext) {
Log.i(TAG, "Setting up Awala")
Awala.setUp(context)
checkIfAwalaAppInstalled()
isAwalaSetUp.compareAndSet(false, true)
_awalaInitializationState.emit(AWALA_SET_UP)
initializeGateway()
awalaSetupJob = null
}
}
Expand Down Expand Up @@ -113,20 +128,6 @@ class AwalaManagerImpl @Inject constructor(
}
}

override suspend fun isAwalaInstalled(currentScreen: Route): Boolean {
val isInstalled = withContext(awalaThreadContext) {
if (!isAwalaSetUp.get()) {
awalaSetupJob?.join()
}
if (currentScreen == Route.AwalaNotInstalled) {
checkIfAwalaAppInstalled()
} else {
isAwalaInstalledOnDevice ?: checkIfAwalaAppInstalled()
}
}
return isInstalled
}

override suspend fun authorizeUsers(thirdPartyPublicKey: ByteArray) {
withContext(awalaThreadContext) {
val firstPartyEndpoint = loadFirstPartyEndpoint()
Expand Down Expand Up @@ -212,23 +213,32 @@ class AwalaManagerImpl @Inject constructor(
private suspend fun configureAwala() {
withContext(awalaThreadContext) {
registerFirstPartyEndpointIfNeeded()
_awalaInitializationState.emit(FIRST_PARTY_ENDPOINT_REGISTRED)
importServerThirdPartyEndpointIfNeeded()
_awalaInitializationState.emit(INITIALIZED)
Log.d(TAG, "Awala is initialized")
}
}

private suspend fun checkIfAwalaAppInstalled(): Boolean {
return withContext(awalaThreadContext) {
override fun initializeGatewayAsync() {
awalaScope.launch(awalaThreadContext) {
initializeGateway()
}
}

private suspend fun initializeGateway() {
withContext(awalaThreadContext) {
try {
Log.i(TAG, "GatewayClient binding...")
GatewayClient.bind()
_awalaInitializationState.emit(GATEWAY_CLIENT_BINDED)
Log.i(TAG, "GatewayClient bound")
configureAwala()
} catch (exp: GatewayBindingException) {
[email protected] = false
return@withContext false
Log.i(TAG, "GatewayClient cannot be bound: $exp")
_awalaUnsuccessfulBindings.emit(Unit)
_awalaInitializationState.emit(AWALA_NOT_INSTALLED)
}
[email protected] = true
true
}
}

Expand Down Expand Up @@ -305,3 +315,22 @@ class AwalaManagerImpl @Inject constructor(
}

internal class InvalidConnectionParams(cause: Throwable) : Exception(cause)

@IntDef(
AWALA_NOT_INSTALLED,
NOT_INITIALIZED,
AWALA_SET_UP,
GATEWAY_CLIENT_BINDED,
FIRST_PARTY_ENDPOINT_REGISTRED,
INITIALIZED,
)
annotation class AwalaInitializationState {
companion object {
const val AWALA_NOT_INSTALLED = -1
const val NOT_INITIALIZED = 0
const val AWALA_SET_UP = 1
const val GATEWAY_CLIENT_BINDED = 2
const val FIRST_PARTY_ENDPOINT_REGISTRED = 3
const val INITIALIZED = 4
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package tech.relaycorp.letro.awala.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
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 tech.relaycorp.letro.R
import tech.relaycorp.letro.ui.theme.LetroColor

@Composable
fun AwalaInitializationInProgress(
texts: Array<String>,
viewModel: AwalaInitializationInProgressViewModel = hiltViewModel(),
) {
val currentTextIndex by viewModel.stringsIndexPointer.collectAsState()

Box(modifier = Modifier.fillMaxSize()) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.background(
color = LetroColor.SurfaceContainerHigh,
)
.padding(
vertical = 16.dp,
),
) {
Text(
text = stringResource(id = R.string.app_name),
style = MaterialTheme.typography.titleMedium,
color = LetroColor.OnSurfaceContainerHigh,
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(id = R.string.we_setting_things_up),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = texts[currentTextIndex % texts.size],
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(24.dp))
LinearProgressIndicator(
trackColor = LetroColor.SurfaceContainer,
color = MaterialTheme.colorScheme.primary,
strokeCap = StrokeCap.Round,
)
}
}
}

@Preview(showBackground = true)
@Composable
private fun AwalaInstallationProgressView_Preview() {
AwalaInitializationInProgress(
arrayOf("Hello"),
hiltViewModel(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package tech.relaycorp.letro.awala.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import tech.relaycorp.letro.di.MainModule
import javax.inject.Inject

@HiltViewModel
class AwalaInitializationInProgressViewModel @Inject constructor(
@MainModule.AwalaInitializationStringsIndexPointer private val _stringsIndexPointer: MutableStateFlow<Int>,
) : ViewModel() {

val stringsIndexPointer: StateFlow<Int> = _stringsIndexPointer

init {
viewModelScope.launch(Dispatchers.IO) {
while (true) {
delay(1_000L)
_stringsIndexPointer.emit(_stringsIndexPointer.value + 1)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tech.relaycorp.letro.awala
package tech.relaycorp.letro.awala.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand All @@ -12,33 +12,51 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import tech.relaycorp.letro.R
import tech.relaycorp.letro.main.MainViewModel
import tech.relaycorp.letro.ui.common.LetroButtonMaxWidthFilled
import tech.relaycorp.letro.ui.navigation.Route
import tech.relaycorp.letro.ui.theme.HorizontalScreenPadding
import tech.relaycorp.letro.ui.theme.LetroTheme
import tech.relaycorp.letro.utils.compose.rememberLifecycleEvent

@Composable
fun AwalaNotInstalledScreen(
mainViewModel: MainViewModel,
awalaInitializationTexts: Array<String>,
onInstallAwalaClick: () -> Unit,
awalaNotInstalledViewModel: AwalaNotInstalledViewModel = hiltViewModel(),
) {
val lifecycleEvent = rememberLifecycleEvent()

LaunchedEffect(lifecycleEvent) {
if (lifecycleEvent == Lifecycle.Event.ON_RESUME) {
mainViewModel.onScreenResumed(Route.AwalaNotInstalled)
awalaNotInstalledViewModel.onScreenResumed()
}
}

val showAwalaInitialization by awalaNotInstalledViewModel.isAwalaInitializingShown.collectAsState()

if (showAwalaInitialization) {
AwalaInitializationInProgress(
texts = awalaInitializationTexts,
)
} else {
InstallAwalaScreen(
onInstallAwalaClick = onInstallAwalaClick,
)
}
}

@Composable
private fun InstallAwalaScreen(
onInstallAwalaClick: () -> Unit,
) {
Box(
modifier = Modifier
.fillMaxSize()
Expand Down Expand Up @@ -87,11 +105,3 @@ fun AwalaNotInstalledScreen(
}
}
}

@Preview(showBackground = true)
@Composable
private fun GatewayNotInstalledPreview() {
LetroTheme {
AwalaNotInstalledScreen(hiltViewModel()) {}
}
}
Loading
Loading