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 - Address Book, Create/Update/Delete Contact, Create Contact by QR screens added
### Added ### Added
- Address book local and remote storage support - Address book local storage support
- New Integrations screen in settings - New Integrations screen in settings
- New QR Code detail screen has been added - 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, - 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 ### Added
- New Integrations screen in settings - 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 - 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. - 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)
api(libs.androidx.fragment.compose) api(libs.androidx.fragment.compose)
api(libs.androidx.activity) api(libs.androidx.activity)
api(libs.google.http.client.gson) { // api(libs.google.http.client.gson) {
exclude(group = "io.grpc") // exclude(group = "io.grpc")
} // }
api(libs.google.api.client.android) { // api(libs.google.api.client.android) {
exclude(group = "org.apache.httpcomponents") // exclude(group = "org.apache.httpcomponents")
exclude(group = "io.grpc") // exclude(group = "io.grpc")
} // }
api(libs.google.api.services.drive) { // api(libs.google.api.services.drive) {
exclude(group = "org.apache.httpcomponents") // exclude(group = "org.apache.httpcomponents")
exclude(group = "io.grpc") // exclude(group = "io.grpc")
} // }
api(libs.play.services.auth) { // api(libs.play.services.auth) {
exclude(group = "io.grpc") // exclude(group = "io.grpc")
} // }
api(libs.bundles.androidx.biometric) api(libs.bundles.androidx.biometric)
androidTestImplementation(projects.testLib) 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.LocalAddressBookDataSource
import co.electriccoin.zcash.ui.common.datasource.LocalAddressBookDataSourceImpl 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.core.module.dsl.singleOf
import org.koin.dsl.bind import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
@ -11,5 +9,5 @@ import org.koin.dsl.module
val dataSourceModule = val dataSourceModule =
module { module {
singleOf(::LocalAddressBookDataSourceImpl) bind LocalAddressBookDataSource::class 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 package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase 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.DeleteContactUseCase
import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase
import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase
@ -46,6 +47,7 @@ val useCaseModule =
singleOf(::RescanBlockchainUseCase) singleOf(::RescanBlockchainUseCase)
singleOf(::GetTransparentAddressUseCase) singleOf(::GetTransparentAddressUseCase)
singleOf(::ObserveAddressBookContactsUseCase) singleOf(::ObserveAddressBookContactsUseCase)
singleOf(::DeleteAddressBookUseCase)
singleOf(::ValidateContactAddressUseCase) singleOf(::ValidateContactAddressUseCase)
singleOf(::ValidateContactNameUseCase) singleOf(::ValidateContactNameUseCase)
singleOf(::SaveContactUseCase) singleOf(::SaveContactUseCase)

View File

@ -7,7 +7,6 @@ import android.content.pm.ActivityInfo
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth 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.extension.setContentCompat
import co.electriccoin.zcash.ui.common.model.OnboardingState import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.model.WalletRestoringState 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.AuthenticationUIState
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel 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.support.WrapSupport
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
import co.electriccoin.zcash.work.WorkIds 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.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -85,34 +77,34 @@ class MainActivity : FragmentActivity() {
val configurationOverrideFlow = MutableStateFlow<ConfigurationOverride?>(null) val configurationOverrideFlow = MutableStateFlow<ConfigurationOverride?>(null)
private val addressBookRepository by inject<AddressBookRepositoryImpl>() // private val addressBookRepository by inject<AddressBookRepositoryImpl>()
private val googleSignInLauncher = // private val googleSignInLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> // registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
when (result.resultCode) { // when (result.resultCode) {
RESULT_OK -> { // RESULT_OK -> {
addressBookRepository.onGoogleSignInSuccess() // addressBookRepository.onGoogleSignInSuccess()
} // }
//
RESULT_CANCELED -> { // RESULT_CANCELED -> {
val status = result.data?.extras?.getParcelable<Status>("googleSignInStatus") // val status = result.data?.extras?.getParcelable<Status>("googleSignInStatus")
addressBookRepository.onGoogleSignInCancelled(status) // addressBookRepository.onGoogleSignInCancelled(status)
} // }
//
else -> { // else -> {
addressBookRepository.onGoogleSignInError() // addressBookRepository.onGoogleSignInError()
} // }
} // }
} // }
//
private val googleConsentLauncher = // private val googleConsentLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> // registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
when (result.resultCode) { // when (result.resultCode) {
RESULT_OK -> requestGoogleSignIn() // RESULT_OK -> requestGoogleSignIn()
RESULT_CANCELED -> addressBookRepository.onGoogleSignInCancelled(null) // RESULT_CANCELED -> addressBookRepository.onGoogleSignInCancelled(null)
else -> addressBookRepository.onGoogleSignInError() // else -> addressBookRepository.onGoogleSignInError()
} // }
} // }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -125,31 +117,31 @@ class MainActivity : FragmentActivity() {
monitorForBackgroundSync() monitorForBackgroundSync()
lifecycleScope.launch { // lifecycleScope.launch {
addressBookRepository.googleSignInRequest.collect { // addressBookRepository.googleSignInRequest.collect {
requestGoogleSignIn() // requestGoogleSignIn()
} // }
} // }
//
lifecycleScope.launch { // lifecycleScope.launch {
addressBookRepository.googleRemoteConsentRequest.collect { intent -> // addressBookRepository.googleRemoteConsentRequest.collect { intent ->
googleConsentLauncher.launch(intent) // googleConsentLauncher.launch(intent)
} // }
} // }
} }
private fun requestGoogleSignIn() { // private fun requestGoogleSignIn() {
val googleSignInClient = // val googleSignInClient =
GoogleSignIn.getClient( // GoogleSignIn.getClient(
this@MainActivity, // this@MainActivity,
GoogleSignInOptions // GoogleSignInOptions
.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) // .Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestScopes(Scope(Scopes.DRIVE_APPFOLDER)) // .requestScopes(Scope(Scopes.DRIVE_APPFOLDER))
.build() // .build()
) // )
//
googleSignInLauncher.launch(googleSignInClient.signInIntent) // googleSignInLauncher.launch(googleSignInClient.signInIntent)
} // }
/** /**
* Sets whether the screen rotation is enabled or screen orientation is locked in the portrait mode. * 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 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.AddressBook
import co.electriccoin.zcash.ui.common.model.AddressBookContact import co.electriccoin.zcash.ui.common.model.AddressBookContact
import co.electriccoin.zcash.ui.common.provider.AddressBookProvider import co.electriccoin.zcash.ui.common.provider.AddressBookProvider
@ -25,6 +26,8 @@ interface LocalAddressBookDataSource {
suspend fun deleteContact(addressBookContact: AddressBookContact): AddressBook suspend fun deleteContact(addressBookContact: AddressBookContact): AddressBook
suspend fun saveContacts(contacts: AddressBook) suspend fun saveContacts(contacts: AddressBook)
suspend fun deleteAddressBook()
} }
class LocalAddressBookDataSourceImpl( class LocalAddressBookDataSourceImpl(
@ -128,6 +131,11 @@ class LocalAddressBookDataSourceImpl(
this@LocalAddressBookDataSourceImpl.addressBook = contacts this@LocalAddressBookDataSourceImpl.addressBook = contacts
} }
override suspend fun deleteAddressBook() {
addressBookStorageProvider.getStorageFile()?.deleteSuspend()
addressBook = null
}
private fun readLocalFileToAddressBook(): AddressBook? { private fun readLocalFileToAddressBook(): AddressBook? {
val file = addressBookStorageProvider.getStorageFile() ?: return null val file = addressBookStorageProvider.getStorageFile() ?: return null
return addressBookProvider.readAddressBookFromFile(file) return addressBookProvider.readAddressBookFromFile(file)

View File

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

View File

@ -2,50 +2,28 @@
package co.electriccoin.zcash.ui.common.repository 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.spackle.Twig
import co.electriccoin.zcash.ui.common.datasource.LocalAddressBookDataSource 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.AddressBook
import co.electriccoin.zcash.ui.common.model.AddressBookContact 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.Dispatchers
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow 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.onSubscription
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import java.io.IOException
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.seconds
interface AddressBookRepository { interface AddressBookRepository {
val addressBook: Flow<AddressBook?> val addressBook: Flow<AddressBook?>
val googleSignInRequest: Flow<Unit> // val googleSignInRequest: Flow<Unit>
val googleRemoteConsentRequest: Flow<Intent> // val googleRemoteConsentRequest: Flow<Intent>
suspend fun saveContact( suspend fun saveContact(
name: String, name: String,
@ -60,26 +38,28 @@ interface AddressBookRepository {
suspend fun deleteContact(contact: AddressBookContact) suspend fun deleteContact(contact: AddressBookContact)
fun onGoogleSignInSuccess() suspend fun deleteAddressBook()
fun onGoogleSignInCancelled(status: Status?) // fun onGoogleSignInSuccess()
//
fun onGoogleSignInError() // fun onGoogleSignInCancelled(status: Status?)
//
// fun onGoogleSignInError()
} }
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class AddressBookRepositoryImpl( class AddressBookRepositoryImpl(
private val localAddressBookDataSource: LocalAddressBookDataSource, private val localAddressBookDataSource: LocalAddressBookDataSource,
private val remoteAddressBookDataSource: RemoteAddressBookDataSource, // private val remoteAddressBookDataSource: RemoteAddressBookDataSource,
private val context: Context // private val context: Context
) : AddressBookRepository { ) : AddressBookRepository {
private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) // private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
private val semaphore = Mutex() private val semaphore = Mutex()
private val addressBookCache = MutableStateFlow<AddressBook?>(null) private val addressBookCache = MutableStateFlow<AddressBook?>(null)
private var internalOperation: InternalOperation? = null // private var internalOperation: InternalOperation? = null
override val addressBook: Flow<AddressBook?> = override val addressBook: Flow<AddressBook?> =
addressBookCache addressBookCache
@ -88,13 +68,13 @@ class AddressBookRepositoryImpl(
ensureSynchronization() 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( override suspend fun saveContact(
name: String, name: String,
@ -112,67 +92,74 @@ class AddressBookRepositoryImpl(
InternalOperation.Delete(contact = contact) InternalOperation.Delete(contact = contact)
) )
override fun onGoogleSignInSuccess() { override suspend fun deleteAddressBook() =
scope.launch { withNonCancellableSemaphore {
withNonCancellableSemaphore { localAddressBookDataSource.deleteAddressBook()
internalOperation?.let { addressBookCache.update { null }
Twig.info { "Google sign in success" }
executeInternalOperation(operation = it)
this@AddressBookRepositoryImpl.internalOperation = null
internalOperationCompleted.emit(it)
}
}
} }
}
override fun onGoogleSignInCancelled(status: Status?) { // override fun onGoogleSignInSuccess() {
scope.launch { // scope.launch {
withNonCancellableSemaphore { // withNonCancellableSemaphore {
Twig.info { "Google sign in cancelled, $status" } // internalOperation?.let {
internalOperation?.let { // Twig.info { "Google sign in success" }
executeInternalOperation(operation = it) // executeInternalOperation(operation = it)
this@AddressBookRepositoryImpl.internalOperation = null // this@AddressBookRepositoryImpl.internalOperation = null
internalOperationCompleted.emit(it) // internalOperationCompleted.emit(it)
} // }
} // }
} // }
} // }
//
override fun onGoogleSignInError() { // override fun onGoogleSignInCancelled(status: Status?) {
scope.launch { // scope.launch {
withNonCancellableSemaphore { // withNonCancellableSemaphore {
internalOperation?.let { // Twig.info { "Google sign in cancelled, $status" }
Twig.info { "Address Book: onGoogleSignInError" } // internalOperation?.let {
executeInternalOperation(operation = it) // executeInternalOperation(operation = it)
this@AddressBookRepositoryImpl.internalOperation = null // this@AddressBookRepositoryImpl.internalOperation = null
internalOperationCompleted.emit(it) // 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( private suspend fun ensureSynchronization(
forceUpdate: Boolean = false, forceUpdate: Boolean = false,
operation: InternalOperation? = null operation: InternalOperation? = null
) { ) {
if (forceUpdate || addressBookCache.value == null) { if (forceUpdate || addressBookCache.value == null) {
val remote = // val remote =
executeRemoteAddressBookSafe { // executeRemoteAddressBookSafe {
val contacts = remoteAddressBookDataSource.fetchContacts() // val contacts = remoteAddressBookDataSource.fetchContacts()
Twig.info { "Address Book: ensureSynchronization - remote address book loaded" } // Twig.info { "Address Book: ensureSynchronization - remote address book loaded" }
contacts // contacts
} // }
val merged = val merged =
mergeContacts( mergeContacts(
local = localAddressBookDataSource.getContacts(), local = localAddressBookDataSource.getContacts(),
remote = remote, // remote = remote,
remote = null,
fromOperation = operation fromOperation = operation
) )
localAddressBookDataSource.saveContacts(merged) localAddressBookDataSource.saveContacts(merged)
executeRemoteAddressBookSafe { // executeRemoteAddressBookSafe {
remoteAddressBookDataSource.uploadContacts() // remoteAddressBookDataSource.uploadContacts()
Twig.info { "Address Book: ensureSynchronization - remote address book uploaded" } // Twig.info { "Address Book: ensureSynchronization - remote address book uploaded" }
} // }
addressBookCache.update { merged } addressBookCache.update { merged }
} }
} }
@ -208,38 +195,39 @@ class AddressBookRepositoryImpl(
} }
private suspend fun withGoogleDrivePermission(internalOperation: InternalOperation) { private suspend fun withGoogleDrivePermission(internalOperation: InternalOperation) {
val remoteConsent = getRemoteConsent() // val remoteConsent = getRemoteConsent()
if (hasGoogleDrivePermission() && remoteConsent in // if (hasGoogleDrivePermission() && remoteConsent in
listOf(RemoteConsentResult.HasRemoteConsent, RemoteConsentResult.Error) // listOf(RemoteConsentResult.HasRemoteConsent, RemoteConsentResult.Error)
) { // ) {
withNonCancellableSemaphore { withNonCancellableSemaphore {
executeInternalOperation(operation = internalOperation) 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() = // private suspend fun hasGoogleDrivePermission() =
withContext(Dispatchers.IO) { // withContext(Dispatchers.IO) {
GoogleSignIn.hasPermissions(GoogleSignIn.getLastSignedInAccount(context), Scope(GOOGLE_DRIVE_SCOPE)) // 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) { private suspend fun executeInternalOperation(operation: InternalOperation) {
ensureSynchronization()
val local = val local =
when (operation) { when (operation) {
is InternalOperation.Delete -> { is InternalOperation.Delete -> {
@ -262,11 +250,11 @@ class AddressBookRepositoryImpl(
} }
} }
addressBookCache.update { local } addressBookCache.update { local }
scope.launch { // scope.launch {
withNonCancellableSemaphore { // withNonCancellableSemaphore {
ensureSynchronization(forceUpdate = true, operation = operation) // ensureSynchronization(forceUpdate = true, operation = operation)
} // }
} // }
} }
private suspend fun withNonCancellableSemaphore(block: suspend () -> Unit) { private suspend fun withNonCancellableSemaphore(block: suspend () -> Unit) {
@ -275,37 +263,37 @@ class AddressBookRepositoryImpl(
} }
} }
@Suppress("TooGenericExceptionCaught") // @Suppress("TooGenericExceptionCaught")
private suspend fun <T> executeRemoteAddressBookSafe(block: suspend () -> T): T? { // private suspend fun <T> executeRemoteAddressBookSafe(block: suspend () -> T): T? {
if (hasGoogleDrivePermission().not()) { // if (hasGoogleDrivePermission().not()) {
return null // return null
} // }
//
return try { // return try {
block() // block()
} catch (e: UserRecoverableAuthException) { // } catch (e: UserRecoverableAuthException) {
Twig.error(e) { "Address Book: remote execution failed" } // Twig.error(e) { "Address Book: remote execution failed" }
null // null
} catch (e: UserRecoverableAuthIOException) { // } catch (e: UserRecoverableAuthIOException) {
Twig.error(e) { "Address Book: remote execution failed" } // Twig.error(e) { "Address Book: remote execution failed" }
null // null
} catch (e: GoogleAuthException) { // } catch (e: GoogleAuthException) {
Twig.error(e) { "Address Book: remote execution failed" } // Twig.error(e) { "Address Book: remote execution failed" }
null // null
} catch (e: GoogleJsonResponseException) { // } catch (e: GoogleJsonResponseException) {
Twig.error(e) { "Address Book: remote execution failed" } // Twig.error(e) { "Address Book: remote execution failed" }
null // null
} catch (e: IOException) { // } catch (e: IOException) {
Twig.error(e) { "Address Book: remote execution failed" } // Twig.error(e) { "Address Book: remote execution failed" }
null // null
} catch (e: IllegalArgumentException) { // } catch (e: IllegalArgumentException) {
Twig.error(e) { "Address Book: remote execution failed" } // Twig.error(e) { "Address Book: remote execution failed" }
null // null
} catch (e: Exception) { // } catch (e: Exception) {
Twig.error(e) { "Address Book: remote execution failed" } // Twig.error(e) { "Address Book: remote execution failed" }
null // null
} // }
} // }
} }
private sealed interface InternalOperation { private sealed interface InternalOperation {
@ -316,4 +304,4 @@ private sealed interface InternalOperation {
data class Delete(val contact: AddressBookContact) : 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.BalanceRepository
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
import co.electriccoin.zcash.ui.common.repository.WalletRepository 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.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
import co.electriccoin.zcash.ui.screen.account.ext.getSortHeight 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.filterNotNull
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -62,6 +62,7 @@ class WalletViewModel(
private val encryptedPreferenceProvider: EncryptedPreferenceProvider, private val encryptedPreferenceProvider: EncryptedPreferenceProvider,
private val standardPreferenceProvider: StandardPreferenceProvider, private val standardPreferenceProvider: StandardPreferenceProvider,
private val getAvailableServers: GetDefaultServersProvider, private val getAvailableServers: GetDefaultServersProvider,
private val deleteAddressBookUseCase: DeleteAddressBookUseCase,
) : AndroidViewModel(application) { ) : AndroidViewModel(application) {
val navigationCommand = exchangeRateRepository.navigationCommand 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> = private fun clearAppStateFlow(): Flow<Boolean> =
callbackFlow { callbackFlow {
viewModelScope.launch { viewModelScope.launch {
@ -228,6 +219,7 @@ class WalletViewModel(
val encryptedPrefsCleared = val encryptedPrefsCleared =
encryptedPreferenceProvider() encryptedPreferenceProvider()
.clearPreferences() .clearPreferences()
deleteAddressBookUseCase()
Twig.info { "Both preferences cleared: ${standardPrefsCleared && encryptedPrefsCleared}" } Twig.info { "Both preferences cleared: ${standardPrefsCleared && encryptedPrefsCleared}" }