Remote address book storage disabled & address book removal (#1639)

* Commented out address book

* Commented out address book

* Address book deletion
This commit is contained in:
Milan 2024-10-18 09:43:25 +02:00 committed by GitHub
parent 8652b91a99
commit 711feb4251
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 424 additions and 430 deletions

View File

@ -17,7 +17,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
- Address Book, Create/Update/Delete Contact, Create Contact by QR screens added
### Added
- Address book local and remote storage support
- Address book local storage support
- New Integrations screen in settings
- New QR Code detail screen has been added
- The new Request ZEC screens have been added. They provide a way to build ZIP 321 Uri consisting of the amount,

View File

@ -21,7 +21,7 @@ directly impact users rather than highlighting other key architectural updates.*
### Added
- New Integrations screen in settings
- Address book local and remote storage support
- Address book local storage support
- New QR Code detail screen has been added
- The new Request ZEC screens have been added. They provide a way to build ZIP 321 Uri consisting of the amount, message, and receiver address and then creates a QR code image of it.

View File

@ -140,20 +140,20 @@ dependencies {
api(libs.androidx.fragment)
api(libs.androidx.fragment.compose)
api(libs.androidx.activity)
api(libs.google.http.client.gson) {
exclude(group = "io.grpc")
}
api(libs.google.api.client.android) {
exclude(group = "org.apache.httpcomponents")
exclude(group = "io.grpc")
}
api(libs.google.api.services.drive) {
exclude(group = "org.apache.httpcomponents")
exclude(group = "io.grpc")
}
api(libs.play.services.auth) {
exclude(group = "io.grpc")
}
// api(libs.google.http.client.gson) {
// exclude(group = "io.grpc")
// }
// api(libs.google.api.client.android) {
// exclude(group = "org.apache.httpcomponents")
// exclude(group = "io.grpc")
// }
// api(libs.google.api.services.drive) {
// exclude(group = "org.apache.httpcomponents")
// exclude(group = "io.grpc")
// }
// api(libs.play.services.auth) {
// exclude(group = "io.grpc")
// }
api(libs.bundles.androidx.biometric)
androidTestImplementation(projects.testLib)

View File

@ -2,8 +2,6 @@ package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.datasource.LocalAddressBookDataSource
import co.electriccoin.zcash.ui.common.datasource.LocalAddressBookDataSourceImpl
import co.electriccoin.zcash.ui.common.datasource.RemoteAddressBookDataSource
import co.electriccoin.zcash.ui.common.datasource.RemoteAddressBookDataSourceImpl
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
@ -11,5 +9,5 @@ import org.koin.dsl.module
val dataSourceModule =
module {
singleOf(::LocalAddressBookDataSourceImpl) bind LocalAddressBookDataSource::class
singleOf(::RemoteAddressBookDataSourceImpl) bind RemoteAddressBookDataSource::class
// singleOf(::RemoteAddressBookDataSourceImpl) bind RemoteAddressBookDataSource::class
}

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase
import co.electriccoin.zcash.ui.common.usecase.DeleteAddressBookUseCase
import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase
import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase
import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase
@ -46,6 +47,7 @@ val useCaseModule =
singleOf(::RescanBlockchainUseCase)
singleOf(::GetTransparentAddressUseCase)
singleOf(::ObserveAddressBookContactsUseCase)
singleOf(::DeleteAddressBookUseCase)
singleOf(::ValidateContactAddressUseCase)
singleOf(::ValidateContactNameUseCase)
singleOf(::SaveContactUseCase)

View File

@ -7,7 +7,6 @@ import android.content.pm.ActivityInfo
import android.os.Bundle
import android.os.SystemClock
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
@ -33,7 +32,6 @@ import co.electriccoin.zcash.ui.common.compose.BindCompLocalProvider
import co.electriccoin.zcash.ui.common.extension.setContentCompat
import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.repository.AddressBookRepositoryImpl
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationUIState
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
@ -55,18 +53,12 @@ import co.electriccoin.zcash.ui.screen.securitywarning.WrapSecurityWarning
import co.electriccoin.zcash.ui.screen.support.WrapSupport
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
import co.electriccoin.zcash.work.WorkIds
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.Scopes
import com.google.android.gms.common.api.Scope
import com.google.android.gms.common.api.Status
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@ -85,34 +77,34 @@ class MainActivity : FragmentActivity() {
val configurationOverrideFlow = MutableStateFlow<ConfigurationOverride?>(null)
private val addressBookRepository by inject<AddressBookRepositoryImpl>()
// private val addressBookRepository by inject<AddressBookRepositoryImpl>()
private val googleSignInLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
when (result.resultCode) {
RESULT_OK -> {
addressBookRepository.onGoogleSignInSuccess()
}
RESULT_CANCELED -> {
val status = result.data?.extras?.getParcelable<Status>("googleSignInStatus")
addressBookRepository.onGoogleSignInCancelled(status)
}
else -> {
addressBookRepository.onGoogleSignInError()
}
}
}
private val googleConsentLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
when (result.resultCode) {
RESULT_OK -> requestGoogleSignIn()
RESULT_CANCELED -> addressBookRepository.onGoogleSignInCancelled(null)
else -> addressBookRepository.onGoogleSignInError()
}
}
// private val googleSignInLauncher =
// registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
// when (result.resultCode) {
// RESULT_OK -> {
// addressBookRepository.onGoogleSignInSuccess()
// }
//
// RESULT_CANCELED -> {
// val status = result.data?.extras?.getParcelable<Status>("googleSignInStatus")
// addressBookRepository.onGoogleSignInCancelled(status)
// }
//
// else -> {
// addressBookRepository.onGoogleSignInError()
// }
// }
// }
//
// private val googleConsentLauncher =
// registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
// when (result.resultCode) {
// RESULT_OK -> requestGoogleSignIn()
// RESULT_CANCELED -> addressBookRepository.onGoogleSignInCancelled(null)
// else -> addressBookRepository.onGoogleSignInError()
// }
// }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -125,31 +117,31 @@ class MainActivity : FragmentActivity() {
monitorForBackgroundSync()
lifecycleScope.launch {
addressBookRepository.googleSignInRequest.collect {
requestGoogleSignIn()
}
// lifecycleScope.launch {
// addressBookRepository.googleSignInRequest.collect {
// requestGoogleSignIn()
// }
// }
//
// lifecycleScope.launch {
// addressBookRepository.googleRemoteConsentRequest.collect { intent ->
// googleConsentLauncher.launch(intent)
// }
// }
}
lifecycleScope.launch {
addressBookRepository.googleRemoteConsentRequest.collect { intent ->
googleConsentLauncher.launch(intent)
}
}
}
private fun requestGoogleSignIn() {
val googleSignInClient =
GoogleSignIn.getClient(
this@MainActivity,
GoogleSignInOptions
.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestScopes(Scope(Scopes.DRIVE_APPFOLDER))
.build()
)
googleSignInLauncher.launch(googleSignInClient.signInIntent)
}
// private fun requestGoogleSignIn() {
// val googleSignInClient =
// GoogleSignIn.getClient(
// this@MainActivity,
// GoogleSignInOptions
// .Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
// .requestScopes(Scope(Scopes.DRIVE_APPFOLDER))
// .build()
// )
//
// googleSignInLauncher.launch(googleSignInClient.signInIntent)
// }
/**
* Sets whether the screen rotation is enabled or screen orientation is locked in the portrait mode.

View File

@ -1,5 +1,6 @@
package co.electriccoin.zcash.ui.common.datasource
import co.electriccoin.zcash.spackle.io.deleteSuspend
import co.electriccoin.zcash.ui.common.model.AddressBook
import co.electriccoin.zcash.ui.common.model.AddressBookContact
import co.electriccoin.zcash.ui.common.provider.AddressBookProvider
@ -25,6 +26,8 @@ interface LocalAddressBookDataSource {
suspend fun deleteContact(addressBookContact: AddressBookContact): AddressBook
suspend fun saveContacts(contacts: AddressBook)
suspend fun deleteAddressBook()
}
class LocalAddressBookDataSourceImpl(
@ -128,6 +131,11 @@ class LocalAddressBookDataSourceImpl(
this@LocalAddressBookDataSourceImpl.addressBook = contacts
}
override suspend fun deleteAddressBook() {
addressBookStorageProvider.getStorageFile()?.deleteSuspend()
addressBook = null
}
private fun readLocalFileToAddressBook(): AddressBook? {
val file = addressBookStorageProvider.getStorageFile() ?: return null
return addressBookProvider.readAddressBookFromFile(file)

View File

@ -1,197 +1,197 @@
@file:Suppress("DEPRECATION")
package co.electriccoin.zcash.ui.common.datasource
import android.content.Context
import android.content.Intent
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.BuildConfig
import co.electriccoin.zcash.ui.common.datasource.RemoteAddressBookDataSource.RemoteConsentResult
import co.electriccoin.zcash.ui.common.model.AddressBook
import co.electriccoin.zcash.ui.common.provider.AddressBookProvider
import co.electriccoin.zcash.ui.common.provider.AddressBookStorageProvider
import com.google.android.gms.auth.UserRecoverableAuthException
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.common.Scopes
import com.google.api.client.extensions.android.http.AndroidHttp
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.google.api.client.googleapis.json.GoogleJsonResponseException
import com.google.api.client.http.FileContent
import com.google.api.client.http.HttpStatusCodes
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.drive.Drive
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import com.google.api.services.drive.model.File as GoogleDriveFile
interface RemoteAddressBookDataSource {
@Throws(
UserRecoverableAuthException::class,
UserRecoverableAuthIOException::class,
IOException::class,
IllegalArgumentException::class,
GoogleJsonResponseException::class,
)
suspend fun fetchContacts(): AddressBook?
@Throws(
UserRecoverableAuthException::class,
UserRecoverableAuthIOException::class,
IOException::class,
IllegalArgumentException::class,
GoogleJsonResponseException::class,
)
suspend fun uploadContacts()
suspend fun getRemoteConsent(): RemoteConsentResult
sealed interface RemoteConsentResult {
data object HasRemoteConsent : RemoteConsentResult
data class NoRemoteConsent(val intent: Intent?) : RemoteConsentResult
data object Error : RemoteConsentResult
}
}
class RemoteAddressBookDataSourceImpl(
private val context: Context,
private val addressBookStorageProvider: AddressBookStorageProvider,
private val addressBookProvider: AddressBookProvider,
) : RemoteAddressBookDataSource {
override suspend fun fetchContacts(): AddressBook? =
withContext(Dispatchers.IO) {
fun fetchRemoteFile(service: Drive): GoogleDriveFile? {
return try {
service.files().list().setSpaces(DRIVE_PRIVATE_APP_FOLDER).execute().files
.find { it.name == DRIVE_ADDRESS_BOOK_FILE_NAME }
} catch (e: GoogleJsonResponseException) {
Twig.info(e) { "No files found on google drive name $DRIVE_ADDRESS_BOOK_FILE_NAME" }
null
}
}
fun downloadRemoteFile(
service: Drive,
file: GoogleDriveFile
): File? {
return try {
val localFile = addressBookStorageProvider.getOrCreateTempStorageFile()
localFile.outputStream().use { outputStream ->
service.files().get(file.id).executeMediaAndDownloadTo(outputStream)
}
localFile
} catch (e: GoogleJsonResponseException) {
Twig.info(e) { "No files found on google drive name $DRIVE_ADDRESS_BOOK_FILE_NAME" }
null
}
}
var localTempFile: File? = null
return@withContext try {
val drive = createGoogleDriveService()
val remoteFile = fetchRemoteFile(drive)
if (remoteFile == null) {
Twig.info { "No address book file found to upload" }
return@withContext null
}
localTempFile = downloadRemoteFile(drive, remoteFile) ?: return@withContext null
addressBookProvider.readAddressBookFromFile(localTempFile)
} finally {
localTempFile?.delete()
}
}
override suspend fun uploadContacts() =
withContext(Dispatchers.IO) {
fun deleteExistingRemoteFiles(service: Drive) {
try {
val files =
service.files().list().setSpaces(DRIVE_PRIVATE_APP_FOLDER).execute().files
.filter { it.name == DRIVE_ADDRESS_BOOK_FILE_NAME }
files.forEach {
service.files().delete(it.id).execute()
}
} catch (e: GoogleJsonResponseException) {
if (e.statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
Twig.info(e) { "No files found on google drive name $DRIVE_ADDRESS_BOOK_FILE_NAME" }
} else {
throw e
}
}
}
fun createRemoteFile(
file: File,
service: Drive
) {
val metadata =
GoogleDriveFile()
.setParents(listOf(DRIVE_PRIVATE_APP_FOLDER))
.setMimeType("application/octet-stream")
.setName(file.name)
val fileContent = FileContent("application/octet-stream", file)
service.files().create(metadata, fileContent).execute()
}
val drive = createGoogleDriveService()
val localFile = addressBookStorageProvider.getStorageFile()
if (localFile == null) {
Twig.info { "No address book file found to upload" }
return@withContext
}
deleteExistingRemoteFiles(drive)
createRemoteFile(localFile, drive)
}
@Suppress("TooGenericExceptionCaught", "SwallowedException")
override suspend fun getRemoteConsent(): RemoteConsentResult =
withContext(Dispatchers.IO) {
val drive = createGoogleDriveService()
try {
drive.files().list().setSpaces(DRIVE_PRIVATE_APP_FOLDER).execute()
RemoteConsentResult.HasRemoteConsent
} catch (e: UserRecoverableAuthException) {
RemoteConsentResult.NoRemoteConsent(e.intent)
} catch (e: UserRecoverableAuthIOException) {
RemoteConsentResult.NoRemoteConsent(e.intent)
} catch (e: Exception) {
RemoteConsentResult.Error
}
}
private fun createGoogleDriveService(): Drive {
val account = GoogleSignIn.getLastSignedInAccount(context)
val credentials =
GoogleAccountCredential.usingOAuth2(context, listOf(Scopes.DRIVE_APPFOLDER))
.apply {
selectedAccount = account?.account ?: allAccounts.firstOrNull()
}
return Drive
.Builder(
AndroidHttp.newCompatibleTransport(),
GsonFactory(),
credentials
)
.setApplicationName(if (BuildConfig.DEBUG) "secant-android-debug" else "Zashi")
.build()
}
}
private const val DRIVE_PRIVATE_APP_FOLDER = "appDataFolder"
private const val DRIVE_ADDRESS_BOOK_FILE_NAME = "address_book"
@file:Suppress("standard:no-empty-file", "no-empty-file")
//
// package co.electriccoin.zcash.ui.common.datasource
//
// import android.content.Context
// import android.content.Intent
// import co.electriccoin.zcash.spackle.Twig
// import co.electriccoin.zcash.ui.BuildConfig
// import co.electriccoin.zcash.ui.common.datasource.RemoteAddressBookDataSource.RemoteConsentResult
// import co.electriccoin.zcash.ui.common.model.AddressBook
// import co.electriccoin.zcash.ui.common.provider.AddressBookProvider
// import co.electriccoin.zcash.ui.common.provider.AddressBookStorageProvider
// import com.google.android.gms.auth.UserRecoverableAuthException
// import com.google.android.gms.auth.api.signin.GoogleSignIn
// import com.google.android.gms.common.Scopes
// import com.google.api.client.extensions.android.http.AndroidHttp
// import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
// import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
// import com.google.api.client.googleapis.json.GoogleJsonResponseException
// import com.google.api.client.http.FileContent
// import com.google.api.client.http.HttpStatusCodes
// import com.google.api.client.json.gson.GsonFactory
// import com.google.api.services.drive.Drive
// import kotlinx.coroutines.Dispatchers
// import kotlinx.coroutines.withContext
// import java.io.File
// import java.io.IOException
// import com.google.api.services.drive.model.File as GoogleDriveFile
//
// interface RemoteAddressBookDataSource {
// @Throws(
// UserRecoverableAuthException::class,
// UserRecoverableAuthIOException::class,
// IOException::class,
// IllegalArgumentException::class,
// GoogleJsonResponseException::class,
// )
// suspend fun fetchContacts(): AddressBook?
//
// @Throws(
// UserRecoverableAuthException::class,
// UserRecoverableAuthIOException::class,
// IOException::class,
// IllegalArgumentException::class,
// GoogleJsonResponseException::class,
// )
// suspend fun uploadContacts()
//
// suspend fun getRemoteConsent(): RemoteConsentResult
//
// sealed interface RemoteConsentResult {
// data object HasRemoteConsent : RemoteConsentResult
//
// data class NoRemoteConsent(val intent: Intent?) : RemoteConsentResult
//
// data object Error : RemoteConsentResult
// }
// }
//
// class RemoteAddressBookDataSourceImpl(
// private val context: Context,
// private val addressBookStorageProvider: AddressBookStorageProvider,
// private val addressBookProvider: AddressBookProvider,
// ) : RemoteAddressBookDataSource {
// override suspend fun fetchContacts(): AddressBook? =
// withContext(Dispatchers.IO) {
// fun fetchRemoteFile(service: Drive): GoogleDriveFile? {
// return try {
// service.files().list().setSpaces(DRIVE_PRIVATE_APP_FOLDER).execute().files
// .find { it.name == DRIVE_ADDRESS_BOOK_FILE_NAME }
// } catch (e: GoogleJsonResponseException) {
// Twig.info(e) { "No files found on google drive name $DRIVE_ADDRESS_BOOK_FILE_NAME" }
// null
// }
// }
//
// fun downloadRemoteFile(
// service: Drive,
// file: GoogleDriveFile
// ): File? {
// return try {
// val localFile = addressBookStorageProvider.getOrCreateTempStorageFile()
//
// localFile.outputStream().use { outputStream ->
// service.files().get(file.id).executeMediaAndDownloadTo(outputStream)
// }
//
// localFile
// } catch (e: GoogleJsonResponseException) {
// Twig.info(e) { "No files found on google drive name $DRIVE_ADDRESS_BOOK_FILE_NAME" }
// null
// }
// }
//
// var localTempFile: File? = null
//
// return@withContext try {
// val drive = createGoogleDriveService()
// val remoteFile = fetchRemoteFile(drive)
//
// if (remoteFile == null) {
// Twig.info { "No address book file found to upload" }
// return@withContext null
// }
//
// localTempFile = downloadRemoteFile(drive, remoteFile) ?: return@withContext null
//
// addressBookProvider.readAddressBookFromFile(localTempFile)
// } finally {
// localTempFile?.delete()
// }
// }
//
// override suspend fun uploadContacts() =
// withContext(Dispatchers.IO) {
// fun deleteExistingRemoteFiles(service: Drive) {
// try {
// val files =
// service.files().list().setSpaces(DRIVE_PRIVATE_APP_FOLDER).execute().files
// .filter { it.name == DRIVE_ADDRESS_BOOK_FILE_NAME }
// files.forEach {
// service.files().delete(it.id).execute()
// }
// } catch (e: GoogleJsonResponseException) {
// if (e.statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
// Twig.info(e) { "No files found on google drive name $DRIVE_ADDRESS_BOOK_FILE_NAME" }
// } else {
// throw e
// }
// }
// }
//
// fun createRemoteFile(
// file: File,
// service: Drive
// ) {
// val metadata =
// GoogleDriveFile()
// .setParents(listOf(DRIVE_PRIVATE_APP_FOLDER))
// .setMimeType("application/octet-stream")
// .setName(file.name)
// val fileContent = FileContent("application/octet-stream", file)
//
// service.files().create(metadata, fileContent).execute()
// }
//
// val drive = createGoogleDriveService()
// val localFile = addressBookStorageProvider.getStorageFile()
//
// if (localFile == null) {
// Twig.info { "No address book file found to upload" }
// return@withContext
// }
//
// deleteExistingRemoteFiles(drive)
// createRemoteFile(localFile, drive)
// }
//
// @Suppress("TooGenericExceptionCaught", "SwallowedException")
// override suspend fun getRemoteConsent(): RemoteConsentResult =
// withContext(Dispatchers.IO) {
// val drive = createGoogleDriveService()
//
// try {
// drive.files().list().setSpaces(DRIVE_PRIVATE_APP_FOLDER).execute()
// RemoteConsentResult.HasRemoteConsent
// } catch (e: UserRecoverableAuthException) {
// RemoteConsentResult.NoRemoteConsent(e.intent)
// } catch (e: UserRecoverableAuthIOException) {
// RemoteConsentResult.NoRemoteConsent(e.intent)
// } catch (e: Exception) {
// RemoteConsentResult.Error
// }
// }
//
// private fun createGoogleDriveService(): Drive {
// val account = GoogleSignIn.getLastSignedInAccount(context)
//
// val credentials =
// GoogleAccountCredential.usingOAuth2(context, listOf(Scopes.DRIVE_APPFOLDER))
// .apply {
// selectedAccount = account?.account ?: allAccounts.firstOrNull()
// }
//
// return Drive
// .Builder(
// AndroidHttp.newCompatibleTransport(),
// GsonFactory(),
// credentials
// )
// .setApplicationName(if (BuildConfig.DEBUG) "secant-android-debug" else "Zashi")
// .build()
// }
// }
//
// private const val DRIVE_PRIVATE_APP_FOLDER = "appDataFolder"
// private const val DRIVE_ADDRESS_BOOK_FILE_NAME = "address_book"

View File

@ -2,50 +2,28 @@
package co.electriccoin.zcash.ui.common.repository
import android.content.Context
import android.content.Intent
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.datasource.LocalAddressBookDataSource
import co.electriccoin.zcash.ui.common.datasource.RemoteAddressBookDataSource
import co.electriccoin.zcash.ui.common.datasource.RemoteAddressBookDataSource.RemoteConsentResult
import co.electriccoin.zcash.ui.common.model.AddressBook
import co.electriccoin.zcash.ui.common.model.AddressBookContact
import com.google.android.gms.auth.GoogleAuthException
import com.google.android.gms.auth.UserRecoverableAuthException
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.common.Scopes
import com.google.android.gms.common.api.Scope
import com.google.android.gms.common.api.Status
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.google.api.client.googleapis.json.GoogleJsonResponseException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import java.io.IOException
import kotlin.math.max
import kotlin.time.Duration.Companion.seconds
interface AddressBookRepository {
val addressBook: Flow<AddressBook?>
val googleSignInRequest: Flow<Unit>
// val googleSignInRequest: Flow<Unit>
val googleRemoteConsentRequest: Flow<Intent>
// val googleRemoteConsentRequest: Flow<Intent>
suspend fun saveContact(
name: String,
@ -60,26 +38,28 @@ interface AddressBookRepository {
suspend fun deleteContact(contact: AddressBookContact)
fun onGoogleSignInSuccess()
suspend fun deleteAddressBook()
fun onGoogleSignInCancelled(status: Status?)
fun onGoogleSignInError()
// fun onGoogleSignInSuccess()
//
// fun onGoogleSignInCancelled(status: Status?)
//
// fun onGoogleSignInError()
}
@Suppress("TooManyFunctions")
class AddressBookRepositoryImpl(
private val localAddressBookDataSource: LocalAddressBookDataSource,
private val remoteAddressBookDataSource: RemoteAddressBookDataSource,
private val context: Context
// private val remoteAddressBookDataSource: RemoteAddressBookDataSource,
// private val context: Context
) : AddressBookRepository {
private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
// private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
private val semaphore = Mutex()
private val addressBookCache = MutableStateFlow<AddressBook?>(null)
private var internalOperation: InternalOperation? = null
// private var internalOperation: InternalOperation? = null
override val addressBook: Flow<AddressBook?> =
addressBookCache
@ -88,13 +68,13 @@ class AddressBookRepositoryImpl(
ensureSynchronization()
}
}
.stateIn(scope = scope, started = SharingStarted.WhileSubscribed(60.seconds), initialValue = null)
// .stateIn(scope = scope, started = SharingStarted.WhileSubscribed(60.seconds), initialValue = null)
override val googleSignInRequest = MutableSharedFlow<Unit>()
// override val googleSignInRequest = MutableSharedFlow<Unit>()
override val googleRemoteConsentRequest = MutableSharedFlow<Intent>()
// override val googleRemoteConsentRequest = MutableSharedFlow<Intent>()
private val internalOperationCompleted = MutableSharedFlow<InternalOperation>()
// private val internalOperationCompleted = MutableSharedFlow<InternalOperation>()
override suspend fun saveContact(
name: String,
@ -112,67 +92,74 @@ class AddressBookRepositoryImpl(
InternalOperation.Delete(contact = contact)
)
override fun onGoogleSignInSuccess() {
scope.launch {
override suspend fun deleteAddressBook() =
withNonCancellableSemaphore {
internalOperation?.let {
Twig.info { "Google sign in success" }
executeInternalOperation(operation = it)
this@AddressBookRepositoryImpl.internalOperation = null
internalOperationCompleted.emit(it)
}
}
}
localAddressBookDataSource.deleteAddressBook()
addressBookCache.update { null }
}
override fun onGoogleSignInCancelled(status: Status?) {
scope.launch {
withNonCancellableSemaphore {
Twig.info { "Google sign in cancelled, $status" }
internalOperation?.let {
executeInternalOperation(operation = it)
this@AddressBookRepositoryImpl.internalOperation = null
internalOperationCompleted.emit(it)
}
}
}
}
override fun onGoogleSignInError() {
scope.launch {
withNonCancellableSemaphore {
internalOperation?.let {
Twig.info { "Address Book: onGoogleSignInError" }
executeInternalOperation(operation = it)
this@AddressBookRepositoryImpl.internalOperation = null
internalOperationCompleted.emit(it)
}
}
}
}
// override fun onGoogleSignInSuccess() {
// scope.launch {
// withNonCancellableSemaphore {
// internalOperation?.let {
// Twig.info { "Google sign in success" }
// executeInternalOperation(operation = it)
// this@AddressBookRepositoryImpl.internalOperation = null
// internalOperationCompleted.emit(it)
// }
// }
// }
// }
//
// override fun onGoogleSignInCancelled(status: Status?) {
// scope.launch {
// withNonCancellableSemaphore {
// Twig.info { "Google sign in cancelled, $status" }
// internalOperation?.let {
// executeInternalOperation(operation = it)
// this@AddressBookRepositoryImpl.internalOperation = null
// internalOperationCompleted.emit(it)
// }
// }
// }
// }
//
// override fun onGoogleSignInError() {
// scope.launch {
// withNonCancellableSemaphore {
// internalOperation?.let {
// Twig.info { "Address Book: onGoogleSignInError" }
// executeInternalOperation(operation = it)
// this@AddressBookRepositoryImpl.internalOperation = null
// internalOperationCompleted.emit(it)
// }
// }
// }
// }
private suspend fun ensureSynchronization(
forceUpdate: Boolean = false,
operation: InternalOperation? = null
) {
if (forceUpdate || addressBookCache.value == null) {
val remote =
executeRemoteAddressBookSafe {
val contacts = remoteAddressBookDataSource.fetchContacts()
Twig.info { "Address Book: ensureSynchronization - remote address book loaded" }
contacts
}
// val remote =
// executeRemoteAddressBookSafe {
// val contacts = remoteAddressBookDataSource.fetchContacts()
// Twig.info { "Address Book: ensureSynchronization - remote address book loaded" }
// contacts
// }
val merged =
mergeContacts(
local = localAddressBookDataSource.getContacts(),
remote = remote,
// remote = remote,
remote = null,
fromOperation = operation
)
localAddressBookDataSource.saveContacts(merged)
executeRemoteAddressBookSafe {
remoteAddressBookDataSource.uploadContacts()
Twig.info { "Address Book: ensureSynchronization - remote address book uploaded" }
}
// executeRemoteAddressBookSafe {
// remoteAddressBookDataSource.uploadContacts()
// Twig.info { "Address Book: ensureSynchronization - remote address book uploaded" }
// }
addressBookCache.update { merged }
}
}
@ -208,38 +195,39 @@ class AddressBookRepositoryImpl(
}
private suspend fun withGoogleDrivePermission(internalOperation: InternalOperation) {
val remoteConsent = getRemoteConsent()
// val remoteConsent = getRemoteConsent()
if (hasGoogleDrivePermission() && remoteConsent in
listOf(RemoteConsentResult.HasRemoteConsent, RemoteConsentResult.Error)
) {
// if (hasGoogleDrivePermission() && remoteConsent in
// listOf(RemoteConsentResult.HasRemoteConsent, RemoteConsentResult.Error)
// ) {
withNonCancellableSemaphore {
executeInternalOperation(operation = internalOperation)
}
} else {
withNonCancellableSemaphore {
if (remoteConsent is RemoteConsentResult.NoRemoteConsent && remoteConsent.intent != null) {
Twig.info { "Address Book: withGoogleDrivePermission - request consent" }
this.internalOperation = internalOperation
googleRemoteConsentRequest.emit(remoteConsent.intent)
} else {
Twig.info { "Address Book: withGoogleDrivePermission - request permission" }
this.internalOperation = internalOperation
googleSignInRequest.emit(Unit)
}
}
internalOperationCompleted.first { it == internalOperation }
}
// } else {
// withNonCancellableSemaphore {
// if (remoteConsent is RemoteConsentResult.NoRemoteConsent && remoteConsent.intent != null) {
// Twig.info { "Address Book: withGoogleDrivePermission - request consent" }
// this.internalOperation = internalOperation
// googleRemoteConsentRequest.emit(remoteConsent.intent)
// } else {
// Twig.info { "Address Book: withGoogleDrivePermission - request permission" }
// this.internalOperation = internalOperation
// googleSignInRequest.emit(Unit)
// }
// }
// internalOperationCompleted.first { it == internalOperation }
// }
}
private suspend fun hasGoogleDrivePermission() =
withContext(Dispatchers.IO) {
GoogleSignIn.hasPermissions(GoogleSignIn.getLastSignedInAccount(context), Scope(GOOGLE_DRIVE_SCOPE))
}
// private suspend fun hasGoogleDrivePermission() =
// withContext(Dispatchers.IO) {
// GoogleSignIn.hasPermissions(GoogleSignIn.getLastSignedInAccount(context), Scope(GOOGLE_DRIVE_SCOPE))
// }
private suspend fun getRemoteConsent() = remoteAddressBookDataSource.getRemoteConsent()
// private suspend fun getRemoteConsent() = remoteAddressBookDataSource.getRemoteConsent()
private suspend fun executeInternalOperation(operation: InternalOperation) {
ensureSynchronization()
val local =
when (operation) {
is InternalOperation.Delete -> {
@ -262,11 +250,11 @@ class AddressBookRepositoryImpl(
}
}
addressBookCache.update { local }
scope.launch {
withNonCancellableSemaphore {
ensureSynchronization(forceUpdate = true, operation = operation)
}
}
// scope.launch {
// withNonCancellableSemaphore {
// ensureSynchronization(forceUpdate = true, operation = operation)
// }
// }
}
private suspend fun withNonCancellableSemaphore(block: suspend () -> Unit) {
@ -275,37 +263,37 @@ class AddressBookRepositoryImpl(
}
}
@Suppress("TooGenericExceptionCaught")
private suspend fun <T> executeRemoteAddressBookSafe(block: suspend () -> T): T? {
if (hasGoogleDrivePermission().not()) {
return null
}
return try {
block()
} catch (e: UserRecoverableAuthException) {
Twig.error(e) { "Address Book: remote execution failed" }
null
} catch (e: UserRecoverableAuthIOException) {
Twig.error(e) { "Address Book: remote execution failed" }
null
} catch (e: GoogleAuthException) {
Twig.error(e) { "Address Book: remote execution failed" }
null
} catch (e: GoogleJsonResponseException) {
Twig.error(e) { "Address Book: remote execution failed" }
null
} catch (e: IOException) {
Twig.error(e) { "Address Book: remote execution failed" }
null
} catch (e: IllegalArgumentException) {
Twig.error(e) { "Address Book: remote execution failed" }
null
} catch (e: Exception) {
Twig.error(e) { "Address Book: remote execution failed" }
null
}
}
// @Suppress("TooGenericExceptionCaught")
// private suspend fun <T> executeRemoteAddressBookSafe(block: suspend () -> T): T? {
// if (hasGoogleDrivePermission().not()) {
// return null
// }
//
// return try {
// block()
// } catch (e: UserRecoverableAuthException) {
// Twig.error(e) { "Address Book: remote execution failed" }
// null
// } catch (e: UserRecoverableAuthIOException) {
// Twig.error(e) { "Address Book: remote execution failed" }
// null
// } catch (e: GoogleAuthException) {
// Twig.error(e) { "Address Book: remote execution failed" }
// null
// } catch (e: GoogleJsonResponseException) {
// Twig.error(e) { "Address Book: remote execution failed" }
// null
// } catch (e: IOException) {
// Twig.error(e) { "Address Book: remote execution failed" }
// null
// } catch (e: IllegalArgumentException) {
// Twig.error(e) { "Address Book: remote execution failed" }
// null
// } catch (e: Exception) {
// Twig.error(e) { "Address Book: remote execution failed" }
// null
// }
// }
}
private sealed interface InternalOperation {
@ -316,4 +304,4 @@ private sealed interface InternalOperation {
data class Delete(val contact: AddressBookContact) : InternalOperation
}
private const val GOOGLE_DRIVE_SCOPE = Scopes.DRIVE_APPFOLDER
// private const val GOOGLE_DRIVE_SCOPE = Scopes.DRIVE_APPFOLDER

View File

@ -0,0 +1,14 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DeleteAddressBookUseCase(
private val addressBookRepository: AddressBookRepository
) {
suspend operator fun invoke() =
withContext(Dispatchers.IO) {
addressBookRepository.deleteAddressBook()
}
}

View File

@ -28,6 +28,7 @@ import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
import co.electriccoin.zcash.ui.common.repository.BalanceRepository
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
import co.electriccoin.zcash.ui.common.repository.WalletRepository
import co.electriccoin.zcash.ui.common.usecase.DeleteAddressBookUseCase
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
import co.electriccoin.zcash.ui.screen.account.ext.getSortHeight
@ -44,7 +45,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -62,6 +62,7 @@ class WalletViewModel(
private val encryptedPreferenceProvider: EncryptedPreferenceProvider,
private val standardPreferenceProvider: StandardPreferenceProvider,
private val getAvailableServers: GetDefaultServersProvider,
private val deleteAddressBookUseCase: DeleteAddressBookUseCase,
) : AndroidViewModel(application) {
val navigationCommand = exchangeRateRepository.navigationCommand
@ -209,16 +210,6 @@ class WalletViewModel(
}
}
/**
* This method only has an effect if the synchronizer currently is loaded.
*/
fun rescanBlockchain() {
viewModelScope.launch {
walletCoordinator.rescanBlockchain()
persistWalletRestoringState(WalletRestoringState.RESTORING)
}
}
private fun clearAppStateFlow(): Flow<Boolean> =
callbackFlow {
viewModelScope.launch {
@ -228,6 +219,7 @@ class WalletViewModel(
val encryptedPrefsCleared =
encryptedPreferenceProvider()
.clearPreferences()
deleteAddressBookUseCase()
Twig.info { "Both preferences cleared: ${standardPrefsCleared && encryptedPrefsCleared}" }