diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d77df988..eb1bc358a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, diff --git a/docs/whatsNew/WHATS_NEW_EN.md b/docs/whatsNew/WHATS_NEW_EN.md index 5ddf8ff87..7276162cf 100644 --- a/docs/whatsNew/WHATS_NEW_EN.md +++ b/docs/whatsNew/WHATS_NEW_EN.md @@ -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. diff --git a/ui-lib/build.gradle.kts b/ui-lib/build.gradle.kts index b08eb51e6..8b3458cfc 100644 --- a/ui-lib/build.gradle.kts +++ b/ui-lib/build.gradle.kts @@ -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) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/DateSourceModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/DateSourceModule.kt index e6c910c88..8a96699fc 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/DateSourceModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/DateSourceModule.kt @@ -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 } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt index 0aff200c3..519fede71 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt @@ -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) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt index 673663407..4adc845a9 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt @@ -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(null) - private val addressBookRepository by inject() + // private val addressBookRepository by inject() - private val googleSignInLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - when (result.resultCode) { - RESULT_OK -> { - addressBookRepository.onGoogleSignInSuccess() - } - - RESULT_CANCELED -> { - val status = result.data?.extras?.getParcelable("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("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.googleRemoteConsentRequest.collect { intent -> - googleConsentLauncher.launch(intent) - } - } + // lifecycleScope.launch { + // addressBookRepository.googleSignInRequest.collect { + // requestGoogleSignIn() + // } + // } + // + // 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. diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/LocalAddressBookDataSource.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/LocalAddressBookDataSource.kt index 3f97b62de..8afe10003 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/LocalAddressBookDataSource.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/LocalAddressBookDataSource.kt @@ -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) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/RemoteAddressBookDataSource.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/RemoteAddressBookDataSource.kt index 58f49375e..fd417a062 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/RemoteAddressBookDataSource.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/RemoteAddressBookDataSource.kt @@ -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" diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/AddressBookRepository.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/AddressBookRepository.kt index ccdf3a8fc..23d397f8d 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/AddressBookRepository.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/AddressBookRepository.kt @@ -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 - val googleSignInRequest: Flow + // val googleSignInRequest: Flow - val googleRemoteConsentRequest: Flow + // val googleRemoteConsentRequest: Flow 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(null) - private var internalOperation: InternalOperation? = null + // private var internalOperation: InternalOperation? = null override val addressBook: Flow = 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() + // override val googleSignInRequest = MutableSharedFlow() - override val googleRemoteConsentRequest = MutableSharedFlow() + // override val googleRemoteConsentRequest = MutableSharedFlow() - private val internalOperationCompleted = MutableSharedFlow() + // private val internalOperationCompleted = MutableSharedFlow() override suspend fun saveContact( name: String, @@ -112,67 +92,74 @@ class AddressBookRepositoryImpl( InternalOperation.Delete(contact = contact) ) - 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 suspend fun deleteAddressBook() = + withNonCancellableSemaphore { + 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) - ) { - 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 } + // 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 } + // } } - 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 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 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 diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/DeleteAddressBookUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/DeleteAddressBookUseCase.kt new file mode 100644 index 000000000..82ce89536 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/DeleteAddressBookUseCase.kt @@ -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() + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt index 07c9b4f18..f22b0911b 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/WalletViewModel.kt @@ -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 = callbackFlow { viewModelScope.launch { @@ -228,6 +219,7 @@ class WalletViewModel( val encryptedPrefsCleared = encryptedPreferenceProvider() .clearPreferences() + deleteAddressBookUseCase() Twig.info { "Both preferences cleared: ${standardPrefsCleared && encryptedPrefsCleared}" }