Skip to content
Merged
2 changes: 1 addition & 1 deletion app/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:versionCode="106"
android:versionName="2.59">
android:versionName="2.59.1">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this generally gets bumped by hotfix Scripts


<uses-permission android:name="android.permission.NFC"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package org.commcare.android.integrity

import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import com.google.android.play.core.integrity.StandardIntegrityException
import com.google.android.play.core.integrity.StandardIntegrityManager
import com.google.android.play.core.integrity.model.StandardIntegrityErrorCode.*
import kotlinx.coroutines.flow.first
import org.commcare.android.CommCareViewModelProvider
import org.commcare.utils.HashUtils
import org.commcare.dalvik.R;
import org.commcare.dalvik.R
import org.json.JSONObject
import java.util.LinkedList
import java.util.HashMap
Expand Down Expand Up @@ -118,47 +118,39 @@ class IntegrityTokenApiRequestHelper(
}

companion object {
/**
* Suspend function to fetch the integrity token for background/worker use.
* Returns Result.success(token) or Result.failure(exception)
*/
suspend fun fetchIntegrityToken(requestHash: String): Result<Pair<String, String>> = suspendCancellableCoroutine { cont ->
suspend fun fetchIntegrityToken(requestHash: String): Result<Pair<String, String>> {
val viewModel = CommCareViewModelProvider.getIntegrityTokenViewModel()

fun requestToken() {
try {
viewModel.requestIntegrityToken(requestHash, false, object : IntegrityTokenViewModel.IntegrityTokenCallback {
override fun onTokenReceived(
requestHash: String,
integrityTokenResponse: StandardIntegrityManager.StandardIntegrityToken
) {
cont.resume(Result.success(Pair(integrityTokenResponse.token(), requestHash)))
}
override fun onTokenFailure(exception: Exception) {
cont.resume(Result.failure(exception))
}
})
} catch (e: Exception) {
cont.resume(Result.failure(e))
}
}

val observer = object : Observer<IntegrityTokenViewModel.TokenProviderState> {
override fun onChanged(value: IntegrityTokenViewModel.TokenProviderState) {
when (value) {
is IntegrityTokenViewModel.TokenProviderState.Success -> {
viewModel.providerState.removeObserver(this)
requestToken()
}
is IntegrityTokenViewModel.TokenProviderState.Failure -> {
viewModel.providerState.removeObserver(this)
cont.resume(Result.failure(value.exception))
return try {
// wait for provider readiness or failure
when (val state = viewModel.providerStateFlow.first()) {
is IntegrityTokenViewModel.TokenProviderState.Success -> {
suspendCancellableCoroutine { cont ->
viewModel.requestIntegrityToken(
requestHash,
hasRetried = false,
object : IntegrityTokenViewModel.IntegrityTokenCallback {
override fun onTokenReceived(
requestHash: String,
integrityTokenResponse: StandardIntegrityManager.StandardIntegrityToken
) {
cont.resume(Result.success(Pair(integrityTokenResponse.token(), requestHash)))
}

override fun onTokenFailure(exception: Exception) {
cont.resume(Result.failure(exception))
}
}
)
}
}
is IntegrityTokenViewModel.TokenProviderState.Failure -> {
Result.failure(state.exception)
}
}
} catch (e: Exception) {
Result.failure(e)
}
viewModel.providerState.observeForever(observer)
cont.invokeOnCancellation { viewModel.providerState.removeObserver(observer) }
}
}
}
34 changes: 23 additions & 11 deletions app/src/org/commcare/android/integrity/IntegrityTokenViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import com.google.android.play.core.integrity.IntegrityManagerFactory
import com.google.android.play.core.integrity.StandardIntegrityException
import com.google.android.play.core.integrity.StandardIntegrityManager
import com.google.android.play.core.integrity.StandardIntegrityManager.PrepareIntegrityTokenRequest
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenProvider
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenRequest
import com.google.android.play.core.integrity.model.StandardIntegrityErrorCode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.commcare.dalvik.BuildConfig
import org.commcare.util.LogTypes
import org.javarosa.core.services.Logger
Expand All @@ -21,7 +25,9 @@ class IntegrityTokenViewModel(application: Application) : AndroidViewModel(appli
private val _providerState = MutableLiveData<TokenProviderState>()
val providerState: LiveData<TokenProviderState> = _providerState

var integrityTokenProvider: StandardIntegrityTokenProvider? = null
private var integrityTokenProvider: StandardIntegrityTokenProvider? = null

val providerStateFlow: Flow<TokenProviderState> =_providerState.asFlow()

init {
prepareTokenProvider()
Expand All @@ -34,7 +40,7 @@ class IntegrityTokenViewModel(application: Application) : AndroidViewModel(appli
* that you anticipate will need to get the integrity token down the line.
* Also note that each app instance can only prepare the integrity token up to 5 times per minute.
*/
fun prepareTokenProvider() {
private fun prepareTokenProvider() {
val standardIntegrityManager = IntegrityManagerFactory.createStandard(getApplication())
val cloudProjectNumber = BuildConfig.GOOGLE_CLOUD_PROJECT_NUMBER
require(cloudProjectNumber!= -1L) { "Google Cloud Project Number is not defined" }
Expand Down Expand Up @@ -85,18 +91,24 @@ class IntegrityTokenViewModel(application: Application) : AndroidViewModel(appli
prepareTokenProvider()

// Observe the new preparation and retry once
_providerState.observeForever(object : Observer<TokenProviderState> {
override fun onChanged(state: TokenProviderState) {
if (state is TokenProviderState.Success) {
_providerState.removeObserver(this)
viewModelScope.launch {
when (val state = providerStateFlow.first {
it is TokenProviderState.Success || it is TokenProviderState.Failure
}) {

is TokenProviderState.Success -> {
requestIntegrityToken(requestHash, true, callback)
} else if (state is TokenProviderState.Failure) {
_providerState.removeObserver(this)
Logger.log("Error re-preparing token provider after failure", state.exception.message )
}

is TokenProviderState.Failure -> {
Logger.log(
"Error re-preparing token provider after failure",
state.exception.message
)
callback.onTokenFailure(state.exception)
}
}
})
}
} else {
callback.onTokenFailure(exception)
}
Expand Down