From 69eacc7254885a0f07a525bba0def8a338c29ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juliano=20C=C3=A9zar=20Chagas=20Tavares?= Date: Mon, 16 Dec 2024 12:47:21 -0300 Subject: [PATCH] [Android][iOS] Holder Activity Logs (#61) This enables activity logs for the holder's actions. --- example/build.gradle.kts | 4 +- .../com/spruceid/mobilesdkexample/HomeView.kt | 3 + .../spruceid/mobilesdkexample/MainActivity.kt | 9 + .../credentials/AddToWalletView.kt | 17 + .../mobilesdkexample/db/AppDatabase.kt | 22 +- .../com/spruceid/mobilesdkexample/db/Daos.kt | 9 + .../spruceid/mobilesdkexample/db/Entities.kt | 12 + .../mobilesdkexample/db/Repositories.kt | 15 + .../mobilesdkexample/navigation/Screen.kt | 2 + .../navigation/SetupNavGraph.kt | 37 +- .../spruceid/mobilesdkexample/utils/Utils.kt | 71 +++ .../viewmodels/CredentialPacksViewModel.kt | 3 +- .../viewmodels/StatusListViewModel.kt | 2 +- .../viewmodels/WalletActivityLogsViewModel.kt | 66 ++ .../wallet/HandleOID4VCIView.kt | 3 + .../wallet/HandleOID4VPView.kt | 564 ++++++++++-------- .../mobilesdkexample/wallet/WalletHomeView.kt | 32 +- .../WalletSettingsActiviyLogView.kt | 202 +++++++ .../walletsettings/WalletSettingsHomeView.kt | 175 ++++-- example/src/main/res/drawable-hdpi/user.png | Bin 582 -> 0 bytes example/src/main/res/drawable-mdpi/user.png | Bin 348 -> 0 bytes example/src/main/res/drawable-xhdpi/user.png | Bin 507 -> 0 bytes example/src/main/res/drawable-xxhdpi/user.png | Bin 1101 -> 0 bytes .../src/main/res/drawable-xxxhdpi/user.png | Bin 768 -> 0 bytes example/src/main/res/drawable/user.xml | 9 + example/src/main/res/xml/provider_paths.xml | 3 + 26 files changed, 955 insertions(+), 305 deletions(-) create mode 100644 example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/WalletActivityLogsViewModel.kt create mode 100644 example/src/main/java/com/spruceid/mobilesdkexample/walletsettings/WalletSettingsActiviyLogView.kt delete mode 100644 example/src/main/res/drawable-hdpi/user.png delete mode 100644 example/src/main/res/drawable-mdpi/user.png delete mode 100644 example/src/main/res/drawable-xhdpi/user.png delete mode 100644 example/src/main/res/drawable-xxhdpi/user.png delete mode 100644 example/src/main/res/drawable-xxxhdpi/user.png create mode 100644 example/src/main/res/drawable/user.xml diff --git a/example/build.gradle.kts b/example/build.gradle.kts index c0a211b..6b5c4e8 100644 --- a/example/build.gradle.kts +++ b/example/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.spruceid.mobilesdkexample" minSdk = 26 targetSdk = 34 - versionCode = 16 - versionName = "1.2.0" + versionCode = 20 + versionName = "1.3.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/HomeView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/HomeView.kt index bc14f6d..b10ba20 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/HomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/HomeView.kt @@ -43,6 +43,7 @@ import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel +import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel import com.spruceid.mobilesdkexample.wallet.WalletHomeView enum class HomeTabs { @@ -56,6 +57,7 @@ fun HomeView( initialTab: String, verificationMethodsViewModel: VerificationMethodsViewModel, credentialPacksViewModel: CredentialPacksViewModel, + walletActivityLogsViewModel: WalletActivityLogsViewModel, statusListViewModel: StatusListViewModel, helpersViewModel: HelpersViewModel ) { @@ -81,6 +83,7 @@ fun HomeView( WalletHomeView( navController, credentialPacksViewModel = credentialPacksViewModel, + walletActivityLogsViewModel = walletActivityLogsViewModel, statusListViewModel = statusListViewModel, helpersViewModel = helpersViewModel ) diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt b/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt index 952b974..b249fba 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt @@ -16,6 +16,7 @@ import com.spruceid.mobile.sdk.ConnectionLiveData import com.spruceid.mobilesdkexample.db.AppDatabase import com.spruceid.mobilesdkexample.db.VerificationActivityLogsRepository import com.spruceid.mobilesdkexample.db.VerificationMethodsRepository +import com.spruceid.mobilesdkexample.db.WalletActivityLogsRepository import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.navigation.SetupNavGraph import com.spruceid.mobilesdkexample.ui.theme.ColorBase1 @@ -28,6 +29,8 @@ import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewMode import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModelFactory import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModelFactory +import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel +import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModelFactory class MainActivity : ComponentActivity() { private lateinit var navController: NavHostController @@ -82,6 +85,10 @@ class MainActivity : ComponentActivity() { VerificationActivityLogsViewModelFactory((application as MainApplication).verificationActivityLogsRepository) } + val walletActivityLogsViewModel: WalletActivityLogsViewModel by viewModels { + WalletActivityLogsViewModelFactory((application as MainApplication).walletActivityLogsRepository) + } + val credentialPacksViewModel: CredentialPacksViewModel by viewModels { CredentialPacksViewModelFactory(application as MainApplication) } @@ -100,6 +107,7 @@ class MainActivity : ComponentActivity() { navController, verificationMethodsViewModel = verificationMethodsViewModel, verificationActivityLogsViewModel = verificationActivityLogsViewModel, + walletActivityLogsViewModel = walletActivityLogsViewModel, credentialPacksViewModel = credentialPacksViewModel, statusListViewModel = statusListViewModel, helpersViewModel = helpersViewModel @@ -117,4 +125,5 @@ class MainApplication : Application() { val verificationMethodsRepository by lazy { VerificationMethodsRepository(db.verificationMethodsDao()) } val verificationActivityLogsRepository by lazy { VerificationActivityLogsRepository(db.verificationActivityLogsDao()) } + val walletActivityLogsRepository by lazy { WalletActivityLogsRepository(db.walletActivityLogsDao()) } } \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/credentials/AddToWalletView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/AddToWalletView.kt index dadcf4f..32f9d3d 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/credentials/AddToWalletView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/AddToWalletView.kt @@ -26,14 +26,18 @@ import androidx.navigation.NavHostController import com.spruceid.mobile.sdk.CredentialPack import com.spruceid.mobilesdkexample.ErrorView import com.spruceid.mobilesdkexample.LoadingView +import com.spruceid.mobilesdkexample.db.WalletActivityLogs import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.ui.theme.ColorEmerald700 import com.spruceid.mobilesdkexample.ui.theme.ColorRose600 import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter import com.spruceid.mobilesdkexample.utils.credentialDisplaySelector +import com.spruceid.mobilesdkexample.utils.getCredentialIdTitleAndIssuer +import com.spruceid.mobilesdkexample.utils.getCurrentSqlDate import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel +import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.launch @@ -43,6 +47,7 @@ fun AddToWalletView( navController: NavHostController, rawCredential: String, credentialPacksViewModel: CredentialPacksViewModel, + walletActivityLogsViewModel: WalletActivityLogsViewModel, statusListViewModel: StatusListViewModel ) { var credentialItem by remember { mutableStateOf(null) } @@ -70,6 +75,18 @@ fun AddToWalletView( val credentialPack = CredentialPack() credentialPack.tryAddRawCredential(rawCredential) credentialPacksViewModel.saveCredentialPack(credentialPack) + val credentialInfo = getCredentialIdTitleAndIssuer(credentialPack) + walletActivityLogsViewModel.saveWalletActivityLog( + walletActivityLogs = WalletActivityLogs( + credentialPackId = credentialPack.id().toString(), + credentialId = credentialInfo.first, + credentialTitle = credentialInfo.second, + issuer = credentialInfo.third, + action = "Claimed", + dateTime = getCurrentSqlDate(), + additionalInformation = "" + ) + ) } catch (e: Exception) { error = e.localizedMessage } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/db/AppDatabase.kt b/example/src/main/java/com/spruceid/mobilesdkexample/db/AppDatabase.kt index 708d79e..60923b9 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/db/AppDatabase.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/db/AppDatabase.kt @@ -10,14 +10,16 @@ import androidx.sqlite.db.SupportSQLiteDatabase @Database( entities = [ + WalletActivityLogs::class, VerificationActivityLogs::class, RawCredentials::class, VerificationMethods::class ], - version = 4 + version = 5 ) @TypeConverters(*[DateConverter::class]) abstract class AppDatabase : RoomDatabase() { + abstract fun walletActivityLogsDao(): WalletActivityLogsDao abstract fun verificationActivityLogsDao(): VerificationActivityLogsDao abstract fun rawCredentialsDao(): RawCredentialsDao abstract fun verificationMethodsDao(): VerificationMethodsDao @@ -36,6 +38,7 @@ abstract class AppDatabase : RoomDatabase() { ) .addMigrations(MIGRATION_2_3) .addMigrations(MIGRATION_3_4) + .addMigrations(MIGRATION_4_5) .allowMainThreadQueries() .build() dbInstance = instance @@ -74,3 +77,20 @@ val MIGRATION_3_4 = object : Migration(3, 4) { ) } } + +val MIGRATION_4_5 = object : Migration(4, 5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + "CREATE TABLE `wallet_activity_logs` (" + + "`id` INTEGER NOT NULL, " + + "`credentialPackId` TEXT NOT NULL, " + + "`credentialId` TEXT NOT NULL, " + + "`credentialTitle` TEXT NOT NULL, " + + "`issuer` TEXT NOT NULL, " + + "`action` TEXT NOT NULL, " + + "`dateTime` INTEGER NOT NULL, " + + "`additionalInformation` TEXT NOT NULL, " + + "PRIMARY KEY(`id`))" + ) + } +} diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/db/Daos.kt b/example/src/main/java/com/spruceid/mobilesdkexample/db/Daos.kt index f8c28eb..1b689c2 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/db/Daos.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/db/Daos.kt @@ -4,6 +4,15 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +@Dao +interface WalletActivityLogsDao { + @Insert + suspend fun insertWalletActivity(walletActivityLogs: WalletActivityLogs) + + @Query("SELECT * FROM wallet_activity_logs ORDER BY dateTime DESC") + fun getAllWalletActivityLogs(): List +} + @Dao interface VerificationActivityLogsDao { @Insert diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/db/Entities.kt b/example/src/main/java/com/spruceid/mobilesdkexample/db/Entities.kt index 5c14eaf..c9987c3 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/db/Entities.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/db/Entities.kt @@ -4,6 +4,18 @@ import androidx.room.Entity import androidx.room.PrimaryKey import java.sql.Date +@Entity(tableName = "wallet_activity_logs") +data class WalletActivityLogs( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val credentialPackId: String, + val credentialId: String, + val credentialTitle: String, + val issuer: String, + val action: String, + val dateTime: Date, + val additionalInformation: String, +) + @Entity(tableName = "verification_activity_logs") data class VerificationActivityLogs( @PrimaryKey(autoGenerate = true) val id: Long = 0, diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/db/Repositories.kt b/example/src/main/java/com/spruceid/mobilesdkexample/db/Repositories.kt index 4dbc671..c4441c3 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/db/Repositories.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/db/Repositories.kt @@ -3,6 +3,21 @@ package com.spruceid.mobilesdkexample.db import androidx.annotation.WorkerThread import java.sql.Date +class WalletActivityLogsRepository(private val walletActivityLogsDao: WalletActivityLogsDao) { + val walletActivityLogs: List = + walletActivityLogsDao.getAllWalletActivityLogs() + + @WorkerThread + suspend fun insertWalletActivityLog(walletActivityLogs: WalletActivityLogs) { + walletActivityLogsDao.insertWalletActivity(walletActivityLogs) + } + + @WorkerThread + suspend fun getWalletActivityLogs(): List { + return walletActivityLogsDao.getAllWalletActivityLogs() + } +} + class VerificationActivityLogsRepository(private val verificationActivityLogsDao: VerificationActivityLogsDao) { val verificationActivityLogs: List = verificationActivityLogsDao.getAllVerificationActivityLogs() diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt index 10268d9..16f8d63 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt @@ -10,6 +10,7 @@ const val VERIFIER_SETTINGS_HOME_PATH = "verifier_settings_home" const val VERIFIER_SETTINGS_ACTIVITY_LOG = "verifier_settings_activity_log" const val ADD_VERIFICATION_METHOD_PATH = "add_verification_method" const val WALLET_SETTINGS_HOME_PATH = "wallet_settings_home" +const val WALLET_SETTINGS_ACTIVITY_LOG = "wallet_settings_activity_log" const val ADD_TO_WALLET_PATH = "add_to_wallet/{rawCredential}" const val SCAN_QR_PATH = "scan_qr" const val HANDLE_OID4VCI_PATH = "oid4vci/{url}" @@ -26,6 +27,7 @@ sealed class Screen(val route: String) { object VerifierSettingsActivityLogScreen : Screen(VERIFIER_SETTINGS_ACTIVITY_LOG) object AddVerificationMethodScreen : Screen(ADD_VERIFICATION_METHOD_PATH) object WalletSettingsHomeScreen : Screen(WALLET_SETTINGS_HOME_PATH) + object WalletSettingsActivityLogScreen : Screen(WALLET_SETTINGS_ACTIVITY_LOG) object AddToWalletScreen : Screen(ADD_TO_WALLET_PATH) object ScanQRScreen : Screen(SCAN_QR_PATH) object HandleOID4VCI : Screen(HANDLE_OID4VCI_PATH) diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt index e286ce1..4caea61 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt @@ -22,9 +22,11 @@ import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel +import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel import com.spruceid.mobilesdkexample.wallet.DispatchQRView import com.spruceid.mobilesdkexample.wallet.HandleOID4VCIView import com.spruceid.mobilesdkexample.wallet.HandleOID4VPView +import com.spruceid.mobilesdkexample.walletsettings.WalletSettingsActivityLogScreen import com.spruceid.mobilesdkexample.walletsettings.WalletSettingsHomeView @Composable @@ -32,6 +34,7 @@ fun SetupNavGraph( navController: NavHostController, verificationMethodsViewModel: VerificationMethodsViewModel, verificationActivityLogsViewModel: VerificationActivityLogsViewModel, + walletActivityLogsViewModel: WalletActivityLogsViewModel, credentialPacksViewModel: CredentialPacksViewModel, statusListViewModel: StatusListViewModel, helpersViewModel: HelpersViewModel @@ -51,6 +54,7 @@ fun SetupNavGraph( initialTab = tab, verificationMethodsViewModel = verificationMethodsViewModel, credentialPacksViewModel = credentialPacksViewModel, + walletActivityLogsViewModel = walletActivityLogsViewModel, statusListViewModel = statusListViewModel, helpersViewModel = helpersViewModel ) @@ -123,7 +127,22 @@ fun SetupNavGraph( } composable( route = Screen.WalletSettingsHomeScreen.route, - ) { WalletSettingsHomeView(navController, credentialPacksViewModel) } + ) { + WalletSettingsHomeView( + navController, + credentialPacksViewModel, + walletActivityLogsViewModel + ) + } + composable( + route = Screen.WalletSettingsActivityLogScreen.route, + ) { + WalletSettingsActivityLogScreen( + navController, + walletActivityLogsViewModel = walletActivityLogsViewModel, + helpersViewModel = helpersViewModel + ) + } composable( route = Screen.AddToWalletScreen.route, deepLinks = @@ -134,6 +153,7 @@ fun SetupNavGraph( navController, rawCredential, credentialPacksViewModel, + walletActivityLogsViewModel, statusListViewModel ) } @@ -144,7 +164,13 @@ fun SetupNavGraph( route = Screen.HandleOID4VCI.route, ) { backStackEntry -> val url = backStackEntry.arguments?.getString("url")!! - HandleOID4VCIView(navController, url, credentialPacksViewModel, statusListViewModel) + HandleOID4VCIView( + navController, + url, + credentialPacksViewModel, + walletActivityLogsViewModel, + statusListViewModel + ) } composable( route = Screen.HandleOID4VP.route, @@ -154,7 +180,12 @@ fun SetupNavGraph( if (!url.startsWith("openid4vp")) { url = "openid4vp://$url" } - HandleOID4VPView(navController, url, credentialPacksViewModel) + HandleOID4VPView( + navController, + url, + credentialPacksViewModel, + walletActivityLogsViewModel + ) } } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt b/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt index ce6b762..53148e1 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt @@ -14,6 +14,7 @@ import com.spruceid.mobile.sdk.CredentialPack import com.spruceid.mobile.sdk.rs.JsonVc import com.spruceid.mobile.sdk.rs.JwtVc import com.spruceid.mobile.sdk.rs.Mdoc +import com.spruceid.mobile.sdk.rs.ParsedCredential import com.spruceid.mobile.sdk.rs.Uuid import com.spruceid.mobile.sdk.rs.Vcdm2SdJwt import com.spruceid.mobilesdkexample.credentials.GenericCredentialItem @@ -207,3 +208,73 @@ fun envelopVerifiableSdJwtCredential(sdJwt: String): String { return jsonString.removeEscaping() } } + +/** + * Given a credential pack, it returns a triple with the credential id, title and issuer. + * @param credentialPack the credential pack with credentials + * @param credential optional credential parameter + * + * @return a triple of strings Triple + */ +fun getCredentialIdTitleAndIssuer( + credentialPack: CredentialPack, + credential: ParsedCredential? = null +): Triple { + val claims = credentialPack.findCredentialClaims(listOf("name", "type", "issuer")) + + var cred = if (credential != null) { + claims.entries.firstNotNullOf { claim -> + if (claim.key == credential.id()) { + claim + } else { + null + } + } + } else { + claims.entries.firstNotNullOf { claim -> + val c = credentialPack.getCredentialById(claim.key) + if ( + c?.asSdJwt() != null || + c?.asJwtVc() != null || + c?.asJsonVc() != null + ) { + claim + } else { + null + } + } + } + + val credentialKey = cred.key + val credentialValue = cred.value + + var title = "" + try { + title = credentialValue.get("name").toString() + if (title.isBlank()) { + val arrayTypes = credentialValue.getJSONArray("type") + for (i in 0 until arrayTypes.length()) { + if (arrayTypes.get(i).toString() != "VerifiableCredential") { + title = arrayTypes.get(i).toString().splitCamelCase() + break + } + } + } + } catch (_: Exception) { + } + + var issuer = "" + try { + issuer = credentialValue.getJSONObject("issuer").getString("name").toString() + } catch (_: Exception) { + } + + if (issuer.isBlank()) { + try { + issuer = credentialValue.getJSONObject("issuer").getString("id").toString() + } catch (_: Exception) { + } + } + + return Triple(credentialKey, title, issuer) +} diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/CredentialPacksViewModel.kt b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/CredentialPacksViewModel.kt index 76bb8fc..fd49c6a 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/CredentialPacksViewModel.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/CredentialPacksViewModel.kt @@ -39,9 +39,10 @@ class CredentialPacksViewModel(application: Application) : AndroidViewModel(appl _credentialPacks.value = tmpCredentialPacksList } - fun deleteAllCredentialPacks() { + fun deleteAllCredentialPacks(onDeleteCredentialPack: ((CredentialPack) -> Unit)? = null) { _credentialPacks.value.forEach { credentialPack -> credentialPack.remove(storageManager) + onDeleteCredentialPack?.invoke(credentialPack) } _credentialPacks.value = emptyList() } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/StatusListViewModel.kt b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/StatusListViewModel.kt index 9685c7c..41e3b1c 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/StatusListViewModel.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/StatusListViewModel.kt @@ -14,7 +14,7 @@ import java.util.UUID class StatusListViewModel(application: Application) : AndroidViewModel(application) { private val _statusLists = MutableStateFlow(mutableMapOf()) val statusLists = _statusLists.asStateFlow() - private val _hasConnection = MutableStateFlow(false) + private val _hasConnection = MutableStateFlow(true) val hasConnection = _hasConnection.asStateFlow() suspend fun fetchStatus(credentialPack: CredentialPack): CredentialStatusList { diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/WalletActivityLogsViewModel.kt b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/WalletActivityLogsViewModel.kt new file mode 100644 index 0000000..147e31b --- /dev/null +++ b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/WalletActivityLogsViewModel.kt @@ -0,0 +1,66 @@ +package com.spruceid.mobilesdkexample.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.spruceid.mobilesdkexample.db.WalletActivityLogs +import com.spruceid.mobilesdkexample.db.WalletActivityLogsRepository +import com.spruceid.mobilesdkexample.utils.formatSqlDateTime +import com.spruceid.mobilesdkexample.utils.removeCommas +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class WalletActivityLogsViewModel(private val walletActivityLogsRepository: WalletActivityLogsRepository) : + ViewModel() { + private val _walletActivityLogs = MutableStateFlow(listOf()) + val walletActivityLogs = _walletActivityLogs.asStateFlow() + + init { + viewModelScope.launch { + _walletActivityLogs.value = + walletActivityLogsRepository.walletActivityLogs + } + } + + suspend fun saveWalletActivityLog(walletActivityLogs: WalletActivityLogs) { + walletActivityLogsRepository.insertWalletActivityLog(walletActivityLogs) + _walletActivityLogs.value = + walletActivityLogsRepository.getWalletActivityLogs() + } + + fun generateWalletActivityLogCSV(logs: List? = null): String { + val heading = + "ID, Credential Pack Id, Credential Id, Credential Title, Issuer, Action, Date Time, Additional Information\n" + + val rows = logs?.joinToString("\n") { + "${it.id}, " + + "${it.credentialPackId}, " + + "${it.credentialId}, " + + "${it.credentialTitle.removeCommas()}, " + + "${it.issuer.removeCommas()}, " + + "${it.action}, " + + "${formatSqlDateTime(it.dateTime).removeCommas()}, " + + it.additionalInformation + } + ?: walletActivityLogs.value.joinToString("\n") { + "${it.id}, " + + "${it.credentialPackId}, " + + "${it.credentialId}, " + + "${it.credentialTitle.removeCommas()}, " + + "${it.issuer.removeCommas()}, " + + "${it.action}, " + + "${formatSqlDateTime(it.dateTime).removeCommas()}, " + + it.additionalInformation + } + + return heading + rows + } +} + +class WalletActivityLogsViewModelFactory(private val repository: WalletActivityLogsRepository) : + ViewModelProvider.NewInstanceFactory() { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + WalletActivityLogsViewModel(repository) as T +} diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt index 6a63ca8..9f02747 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt @@ -25,6 +25,7 @@ import com.spruceid.mobilesdkexample.R import com.spruceid.mobilesdkexample.credentials.AddToWalletView import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel +import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO @@ -39,6 +40,7 @@ fun HandleOID4VCIView( navController: NavHostController, url: String, credentialPacksViewModel: CredentialPacksViewModel, + walletActivityLogsViewModel: WalletActivityLogsViewModel, statusListViewModel: StatusListViewModel ) { var loading by remember { mutableStateOf(false) } @@ -159,6 +161,7 @@ fun HandleOID4VCIView( navController = navController, rawCredential = credential!!, credentialPacksViewModel = credentialPacksViewModel, + walletActivityLogsViewModel = walletActivityLogsViewModel, statusListViewModel = statusListViewModel ) } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt index 31f4e46..16ae721 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt @@ -11,8 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -57,6 +55,7 @@ import com.spruceid.mobile.sdk.rs.RequestedField import com.spruceid.mobilesdkexample.ErrorView import com.spruceid.mobilesdkexample.LoadingView import com.spruceid.mobilesdkexample.R +import com.spruceid.mobilesdkexample.db.WalletActivityLogs import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.ui.theme.ColorBase300 import com.spruceid.mobilesdkexample.ui.theme.ColorBase50 @@ -66,8 +65,11 @@ import com.spruceid.mobilesdkexample.ui.theme.ColorStone300 import com.spruceid.mobilesdkexample.ui.theme.ColorStone600 import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter +import com.spruceid.mobilesdkexample.utils.getCredentialIdTitleAndIssuer +import com.spruceid.mobilesdkexample.utils.getCurrentSqlDate import com.spruceid.mobilesdkexample.utils.trustedDids import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel +import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -81,8 +83,8 @@ class Signer(keyId: String?) : PresentationSigner { override suspend fun sign(payload: ByteArray): ByteArray { val signature = - keyManager.signPayload(keyId, payload) - ?: throw IllegalStateException("Failed to sign payload") + keyManager.signPayload(keyId, payload) + ?: throw IllegalStateException("Failed to sign payload") return signature } @@ -114,9 +116,10 @@ class Signer(keyId: String?) : PresentationSigner { @Composable fun HandleOID4VPView( - navController: NavController, - url: String, - credentialPacksViewModel: CredentialPacksViewModel + navController: NavController, + url: String, + credentialPacksViewModel: CredentialPacksViewModel, + walletActivityLogsViewModel: WalletActivityLogsViewModel ) { val scope = rememberCoroutineScope() val credentialPacks = credentialPacksViewModel.credentialPacks @@ -141,12 +144,12 @@ fun HandleOID4VPView( withContext(Dispatchers.IO) { val signer = Signer("reference-app/default-signing") holder = - Holder.newWithCredentials( - credentials, - trustedDids, - signer, - getVCPlaygroundOID4VCIContext(ctx) - ) + Holder.newWithCredentials( + credentials, + trustedDids, + signer, + getVCPlaygroundOID4VCIContext(ctx) + ) val newurl = url.replace("authorize", "") permissionRequest = holder!!.authorizationRequest(newurl) } @@ -161,9 +164,9 @@ fun HandleOID4VPView( if (err != null) { ErrorView( - errorTitle = "Error Presenting Credential", - errorDetails = err!!, - onClose = { navController.navigate(Screen.HomeScreen.route) { popUpTo(0) } } + errorTitle = "Error Presenting Credential", + errorDetails = err!!, + onClose = { navController.navigate(Screen.HomeScreen.route) { popUpTo(0) } } ) } else { if (permissionRequest == null) { @@ -171,149 +174,177 @@ fun HandleOID4VPView( } else if (permissionResponse == null) { if (permissionRequest!!.credentials().isNotEmpty()) { CredentialSelector( - credentials = permissionRequest!!.credentials(), - credentialClaims = credentialClaims, - getRequestedFields = { credential -> - permissionRequest!!.requestedFields(credential) - }, - onContinue = { selectedCredentials -> - scope.launch { - try { - permissionResponse = - permissionRequest!!.createPermissionResponse( - selectedCredentials - ) - } catch (e: Exception) { - err = e.localizedMessage - } + credentials = permissionRequest!!.credentials(), + credentialClaims = credentialClaims, + getRequestedFields = { credential -> + permissionRequest!!.requestedFields(credential) + }, + onContinue = { selectedCredentials -> + scope.launch { + try { + // TODO: support multiple presentation + selectedCredential = selectedCredentials.first() + permissionResponse = + permissionRequest!!.createPermissionResponse( + selectedCredentials + ) + } catch (e: Exception) { + err = e.localizedMessage } - }, - onCancel = { onBack() } + } + }, + onCancel = { onBack() } ) } else { ErrorView( - errorTitle = "No matching credential(s)", - errorDetails = - "There are no credentials in your wallet that match the verification request you have scanned", - closeButtonLabel = "Cancel" + errorTitle = "No matching credential(s)", + errorDetails = + "There are no credentials in your wallet that match the verification request you have scanned", + closeButtonLabel = "Cancel" ) { onBack() } } } else { - LazyColumn { - items(permissionResponse!!.selectedCredentials()) { selectedCredential -> - DataFieldSelector( - requestedFields = - permissionRequest!!.requestedFields(selectedCredential), - onContinue = { - scope.launch { - try { - holder!!.submitPermissionResponse(permissionResponse!!) - onBack() - } catch (e: Exception) { - err = e.localizedMessage - } - } - }, - onCancel = { onBack() } - ) - } - } + DataFieldSelector( + requestedFields = + permissionRequest!!.requestedFields(selectedCredential!!), + onContinue = { + scope.launch { + try { + holder!!.submitPermissionResponse(permissionResponse!!) + val credentialPack = + credentialPacks.value.firstOrNull { credentialPack -> + credentialPack.getCredentialById(selectedCredential!!.id()) != null + }!! + val credentialInfo = + getCredentialIdTitleAndIssuer(credentialPack) + walletActivityLogsViewModel.saveWalletActivityLog( + walletActivityLogs = WalletActivityLogs( + credentialPackId = credentialPack.id().toString(), + credentialId = credentialInfo.first, + credentialTitle = credentialInfo.second, + issuer = credentialInfo.third, + action = "Verification", + dateTime = getCurrentSqlDate(), + additionalInformation = "" + ) + ) + onBack() + } catch (e: Exception) { + err = e.localizedMessage + } + } + }, + onCancel = { onBack() } + ) } } } @Composable fun DataFieldSelector( - requestedFields: List, - onContinue: () -> Unit, - onCancel: () -> Unit + requestedFields: List, + onContinue: () -> Unit, + onCancel: () -> Unit ) { val bullet = "\u2022" val paragraphStyle = ParagraphStyle(textIndent = TextIndent(restLine = 12.sp)) val mockDataField = - requestedFields.map { field -> field.name()?.replaceFirstChar(Char::titlecase) ?: "" } + requestedFields.map { field -> field.name()?.replaceFirstChar(Char::titlecase) ?: "" } - Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).padding(top = 48.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(top = 48.dp) + ) { Text( - buildAnnotatedString { - withStyle(style = SpanStyle(color = Color.Blue)) { append("Verifier") } - append(" is requesting access to the following information") - }, - fontFamily = Inter, - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - color = ColorStone950, - modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), - textAlign = TextAlign.Center + buildAnnotatedString { + withStyle(style = SpanStyle(color = Color.Blue)) { append("Verifier") } + append(" is requesting access to the following information") + }, + fontFamily = Inter, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + color = ColorStone950, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + textAlign = TextAlign.Center ) Column( - modifier = - Modifier.fillMaxSize() - .verticalScroll(rememberScrollState()) - .weight(weight = 1f, fill = false) + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .weight(weight = 1f, fill = false) ) { Text( - buildAnnotatedString { - mockDataField.forEach { - withStyle(style = paragraphStyle) { - append(bullet) - append("\t\t") - append(it) - } + buildAnnotatedString { + mockDataField.forEach { + withStyle(style = paragraphStyle) { + append(bullet) + append("\t\t") + append(it) } - }, + } + }, ) } Row( - modifier = - Modifier.fillMaxWidth().padding(vertical = 12.dp).navigationBarsPadding(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + .navigationBarsPadding(), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Button( - onClick = { onCancel() }, - shape = RoundedCornerShape(6.dp), - colors = - ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = ColorStone950, - ), - modifier = - Modifier.fillMaxWidth() - .border( - width = 1.dp, - color = ColorStone300, - shape = RoundedCornerShape(6.dp) - ) - .weight(1f) + onClick = { onCancel() }, + shape = RoundedCornerShape(6.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = ColorStone950, + ), + modifier = + Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = ColorStone300, + shape = RoundedCornerShape(6.dp) + ) + .weight(1f) ) { Text( - text = "Cancel", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - color = ColorStone950, + text = "Cancel", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = ColorStone950, ) } Button( - onClick = { onContinue() }, - shape = RoundedCornerShape(6.dp), - colors = ButtonDefaults.buttonColors(containerColor = ColorEmerald900), - modifier = - Modifier.fillMaxWidth() - .background( - color = ColorEmerald900, - shape = RoundedCornerShape(6.dp), - ) - .weight(1f) + onClick = { onContinue() }, + shape = RoundedCornerShape(6.dp), + colors = ButtonDefaults.buttonColors(containerColor = ColorEmerald900), + modifier = + Modifier + .fillMaxWidth() + .background( + color = ColorEmerald900, + shape = RoundedCornerShape(6.dp), + ) + .weight(1f) ) { Text( - text = "Approve", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - color = ColorBase50, + text = "Approve", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = ColorBase50, ) } } @@ -322,12 +353,12 @@ fun DataFieldSelector( @Composable fun CredentialSelector( - credentials: List, - credentialClaims: Map, - getRequestedFields: (ParsedCredential) -> List, - onContinue: (List) -> Unit, - onCancel: () -> Unit, - allowMultiple: Boolean = false + credentials: List, + credentialClaims: Map, + getRequestedFields: (ParsedCredential) -> List, + onContinue: (List) -> Unit, + onCancel: () -> Unit, + allowMultiple: Boolean = false ) { val selectedCredentials = remember { mutableStateListOf() } @@ -349,7 +380,8 @@ fun CredentialSelector( credentialClaims[credential.id()]?.getString("name").let { return it.toString() } - } catch (_: Exception) {} + } catch (_: Exception) { + } try { credentialClaims[credential.id()]?.getJSONArray("type").let { @@ -360,117 +392,131 @@ fun CredentialSelector( } return "" } - } catch (_: Exception) {} + } catch (_: Exception) { + } return "" } - Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).padding(top = 48.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(top = 48.dp) + ) { Text( - text = "Select the credential${if (allowMultiple) "(s)" else ""} to share", - fontFamily = Inter, - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - color = ColorStone950, - modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), - textAlign = TextAlign.Center + text = "Select the credential${if (allowMultiple) "(s)" else ""} to share", + fontFamily = Inter, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + color = ColorStone950, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + textAlign = TextAlign.Center ) if (allowMultiple) { Text( - text = "Select All", - fontFamily = Inter, - fontWeight = FontWeight.Normal, - fontSize = 15.sp, - color = ColorBlue600, - modifier = - Modifier.clickable { - // TODO: implement select all - } + text = "Select All", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, + color = ColorBlue600, + modifier = + Modifier.clickable { + // TODO: implement select all + } ) } Column( - modifier = - Modifier.fillMaxSize() - .verticalScroll(rememberScrollState()) - .weight(weight = 1f, fill = false) + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .weight(weight = 1f, fill = false) ) { credentials.forEach { credential -> CredentialSelectorItem( - credential = credential, - requestedFields = getRequestedFields(credential), - getCredentialTitle = { cred -> getCredentialTitle(cred) }, - isChecked = credential in selectedCredentials, - selectCredential = { cred -> selectCredential(cred) }, - removeCredential = { cred -> removeCredential(cred) }, + credential = credential, + requestedFields = getRequestedFields(credential), + getCredentialTitle = { cred -> getCredentialTitle(cred) }, + isChecked = credential in selectedCredentials, + selectCredential = { cred -> selectCredential(cred) }, + removeCredential = { cred -> removeCredential(cred) }, ) } } Row( - modifier = - Modifier.fillMaxWidth().padding(vertical = 12.dp).navigationBarsPadding(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + .navigationBarsPadding(), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Button( - onClick = { onCancel() }, - shape = RoundedCornerShape(6.dp), - colors = - ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = ColorStone950, - ), - modifier = - Modifier.fillMaxWidth() - .border( - width = 1.dp, - color = ColorStone300, - shape = RoundedCornerShape(6.dp) - ) - .weight(1f) + onClick = { onCancel() }, + shape = RoundedCornerShape(6.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = ColorStone950, + ), + modifier = + Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = ColorStone300, + shape = RoundedCornerShape(6.dp) + ) + .weight(1f) ) { Text( - text = "Cancel", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - color = ColorStone950, + text = "Cancel", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = ColorStone950, ) } Button( - onClick = { + onClick = { + if (selectedCredentials.isNotEmpty()) { + onContinue(selectedCredentials) + } + }, + shape = RoundedCornerShape(6.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = + if (selectedCredentials.isNotEmpty()) { + ColorStone600 + } else { + Color.Gray + } + ), + modifier = + Modifier + .fillMaxWidth() + .background( + color = if (selectedCredentials.isNotEmpty()) { - onContinue(selectedCredentials) - } - }, - shape = RoundedCornerShape(6.dp), - colors = - ButtonDefaults.buttonColors( - containerColor = - if (selectedCredentials.isNotEmpty()) { - ColorStone600 - } else { - Color.Gray - } - ), - modifier = - Modifier.fillMaxWidth() - .background( - color = - if (selectedCredentials.isNotEmpty()) { - ColorStone600 - } else { - Color.Gray - }, - shape = RoundedCornerShape(6.dp), - ) - .weight(1f) + ColorStone600 + } else { + Color.Gray + }, + shape = RoundedCornerShape(6.dp), + ) + .weight(1f) ) { Text( - text = "Continue", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - color = ColorBase50, + text = "Continue", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = ColorBase50, ) } } @@ -479,84 +525,88 @@ fun CredentialSelector( @Composable fun CredentialSelectorItem( - credential: ParsedCredential, - requestedFields: List, - getCredentialTitle: (ParsedCredential) -> String, - isChecked: Boolean, - selectCredential: (ParsedCredential) -> Unit, - removeCredential: (ParsedCredential) -> Unit + credential: ParsedCredential, + requestedFields: List, + getCredentialTitle: (ParsedCredential) -> String, + isChecked: Boolean, + selectCredential: (ParsedCredential) -> Unit, + removeCredential: (ParsedCredential) -> Unit ) { var expanded by remember { mutableStateOf(false) } val bullet = "\u2022" val paragraphStyle = ParagraphStyle(textIndent = TextIndent(restLine = 12.sp)) val mockDataField = - requestedFields.map { field -> field.name()?.replaceFirstChar(Char::titlecase) ?: "" } + requestedFields.map { field -> field.name()?.replaceFirstChar(Char::titlecase) ?: "" } Column( - modifier = - Modifier.fillMaxWidth() - .padding(vertical = 8.dp) - .border( - width = 1.dp, - color = ColorBase300, - shape = RoundedCornerShape(8.dp) - ) + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .border( + width = 1.dp, + color = ColorBase300, + shape = RoundedCornerShape(8.dp) + ) ) { Row( - modifier = Modifier.fillMaxWidth().padding(end = 8.dp).padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically + modifier = Modifier + .fillMaxWidth() + .padding(end = 8.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically ) { Checkbox( - checked = isChecked, - onCheckedChange = { isChecked -> - if (isChecked) { - selectCredential(credential) - } else { - removeCredential(credential) - } - }, - colors = - CheckboxDefaults.colors( - checkedColor = ColorBlue600, - uncheckedColor = ColorStone300 - ) + checked = isChecked, + onCheckedChange = { isChecked -> + if (isChecked) { + selectCredential(credential) + } else { + removeCredential(credential) + } + }, + colors = + CheckboxDefaults.colors( + checkedColor = ColorBlue600, + uncheckedColor = ColorStone300 + ) ) Text( - text = getCredentialTitle(credential), - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - color = ColorStone950, - modifier = Modifier.weight(1f) + text = getCredentialTitle(credential), + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + color = ColorStone950, + modifier = Modifier.weight(1f) ) if (expanded) { Image( - painter = painterResource(id = R.drawable.collapse), - contentDescription = stringResource(id = R.string.collapse), - modifier = Modifier.clickable { expanded = false } + painter = painterResource(id = R.drawable.collapse), + contentDescription = stringResource(id = R.string.collapse), + modifier = Modifier.clickable { expanded = false } ) } else { Image( - painter = painterResource(id = R.drawable.expand), - contentDescription = stringResource(id = R.string.expand), - modifier = Modifier.clickable { expanded = true } + painter = painterResource(id = R.drawable.expand), + contentDescription = stringResource(id = R.string.expand), + modifier = Modifier.clickable { expanded = true } ) } } if (expanded) { Text( - buildAnnotatedString { - mockDataField.forEach { - withStyle(style = paragraphStyle) { - append(bullet) - append("\t\t") - append(it) - } + buildAnnotatedString { + mockDataField.forEach { + withStyle(style = paragraphStyle) { + append(bullet) + append("\t\t") + append(it) } - }, - modifier = Modifier.padding(16.dp) + } + }, + modifier = Modifier.padding(16.dp) ) } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt index 322a17a..fc50af6 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt @@ -40,21 +40,27 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.spruceid.mobilesdkexample.LoadingView import com.spruceid.mobilesdkexample.R import com.spruceid.mobilesdkexample.credentials.GenericCredentialItem +import com.spruceid.mobilesdkexample.db.WalletActivityLogs import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.ui.theme.ColorBase150 import com.spruceid.mobilesdkexample.ui.theme.ColorStone400 import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter +import com.spruceid.mobilesdkexample.utils.getCredentialIdTitleAndIssuer +import com.spruceid.mobilesdkexample.utils.getCurrentSqlDate import com.spruceid.mobilesdkexample.utils.getFileContent import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import kotlinx.coroutines.launch import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel +import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel +import kotlinx.coroutines.launch @Composable fun WalletHomeView( navController: NavController, credentialPacksViewModel: CredentialPacksViewModel, + walletActivityLogsViewModel: WalletActivityLogsViewModel, statusListViewModel: StatusListViewModel, helpersViewModel: HelpersViewModel ) { @@ -66,8 +72,9 @@ fun WalletHomeView( WalletHomeHeader(navController = navController) WalletHomeBody( credentialPacksViewModel = credentialPacksViewModel, - statusListViewModel = statusListViewModel, - helpersViewModel = helpersViewModel + helpersViewModel = helpersViewModel, + walletActivityLogsViewModel = walletActivityLogsViewModel, + statusListViewModel = statusListViewModel ) } } @@ -130,6 +137,7 @@ fun WalletHomeHeader(navController: NavController) { @Composable fun WalletHomeBody( credentialPacksViewModel: CredentialPacksViewModel, + walletActivityLogsViewModel: WalletActivityLogsViewModel, helpersViewModel: HelpersViewModel, statusListViewModel: StatusListViewModel ) { @@ -172,6 +180,26 @@ fun WalletHomeBody( statusListViewModel = statusListViewModel, onDelete = { credentialPacksViewModel.deleteCredentialPack(credentialPack) + scope.launch { + credentialPack.list().forEach { credential -> + val credentialInfo = + getCredentialIdTitleAndIssuer( + credentialPack, + credential + ) + walletActivityLogsViewModel.saveWalletActivityLog( + walletActivityLogs = WalletActivityLogs( + credentialPackId = credentialPack.id().toString(), + credentialId = credentialInfo.first, + credentialTitle = credentialInfo.second, + issuer = credentialInfo.third, + action = "Deleted", + dateTime = getCurrentSqlDate(), + additionalInformation = "" + ) + ) + } + } }, onExport = { credentialTitle -> helpersViewModel.exportText( diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/walletsettings/WalletSettingsActiviyLogView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/walletsettings/WalletSettingsActiviyLogView.kt new file mode 100644 index 0000000..f62cc9a --- /dev/null +++ b/example/src/main/java/com/spruceid/mobilesdkexample/walletsettings/WalletSettingsActiviyLogView.kt @@ -0,0 +1,202 @@ +package com.spruceid.mobilesdkexample.walletsettings + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.scale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.spruceid.mobilesdkexample.R +import com.spruceid.mobilesdkexample.db.WalletActivityLogs +import com.spruceid.mobilesdkexample.ui.theme.ColorBase1 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone300 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone400 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone600 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 +import com.spruceid.mobilesdkexample.ui.theme.Inter +import com.spruceid.mobilesdkexample.utils.formatSqlDateTime +import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel +import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel + +@Composable +fun WalletSettingsActivityLogScreen( + navController: NavController, + walletActivityLogsViewModel: WalletActivityLogsViewModel, + helpersViewModel: HelpersViewModel +) { + val walletActivityLogs by walletActivityLogsViewModel.walletActivityLogs.collectAsState() + + Column( + Modifier + .padding(all = 20.dp) + .padding(top = 20.dp) + ) { + WalletSettingsActivityLogScreenHeader( + onBack = { + navController.popBackStack() + } + ) + WalletSettingsActivityLogScreenBody( + walletActivityLogs = walletActivityLogs, + export = { logs -> + helpersViewModel.exportText( + walletActivityLogsViewModel.generateWalletActivityLogCSV(logs = logs), + "wallet_activity_logs.csv", + "text/csv" + ) + } + ) + } +} + +@Composable +fun WalletSettingsActivityLogScreenHeader(onBack: () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .height(36.dp) + .clickable { + onBack() + } + ) { + Image( + painter = painterResource(id = R.drawable.chevron), + contentDescription = stringResource(id = R.string.chevron), + modifier = Modifier + .rotate(180f) + .scale(0.4f) + ) + Text( + text = "Activity Log", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + color = ColorStone950 + ) + Spacer(Modifier.weight(1f)) + } +} + +@Composable +fun WalletSettingsActivityLogScreenBody( + walletActivityLogs: List, + export: (List) -> Unit +) { + Column( + Modifier + .padding(top = 10.dp) + .navigationBarsPadding(), + ) { + if (walletActivityLogs.isEmpty()) { + Column( + Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "No Activity Log Found", + fontFamily = Inter, + fontSize = 20.sp, + fontWeight = FontWeight.Normal, + color = ColorStone400 + ) + } + } else { + LazyColumn( + Modifier + .padding(top = 10.dp) + .fillMaxSize() + .weight(weight = 1f, fill = false), + ) { + items(walletActivityLogs) { log -> + Column { + Text( + text = log.credentialTitle, + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + fontSize = 17.sp, + color = ColorStone950, + modifier = Modifier.padding(bottom = 4.dp), + ) + Text( + text = log.action, + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, + color = ColorStone600, + modifier = Modifier.padding(bottom = 4.dp), + ) + Text( + text = formatSqlDateTime(log.dateTime), + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, + color = ColorStone600, + modifier = Modifier.padding(bottom = 4.dp), + ) + HorizontalDivider(modifier = Modifier.padding(bottom = 12.dp)) + } + } + } + Button( + onClick = { + export(walletActivityLogs) + }, + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = ColorStone300, + shape = RoundedCornerShape(100.dp) + ), + colors = ButtonDefaults.buttonColors( + containerColor = ColorBase1, + contentColor = ColorStone950, + ), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.export), + contentDescription = stringResource(id = R.string.export), + modifier = Modifier.padding(end = 5.dp), + ) + Text( + text = "Export", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + color = ColorStone950, + ) + } + } + } + } +} \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/walletsettings/WalletSettingsHomeView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/walletsettings/WalletSettingsHomeView.kt index 94e7e0c..af70ba8 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/walletsettings/WalletSettingsHomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/walletsettings/WalletSettingsHomeView.kt @@ -1,12 +1,18 @@ package com.spruceid.mobilesdkexample.walletsettings import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -14,9 +20,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -24,75 +31,166 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.spruceid.mobilesdkexample.R +import com.spruceid.mobilesdkexample.db.WalletActivityLogs import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.ui.theme.ColorRose600 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone50 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone600 import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter +import com.spruceid.mobilesdkexample.utils.getCredentialIdTitleAndIssuer +import com.spruceid.mobilesdkexample.utils.getCurrentSqlDate import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel +import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch @Composable fun WalletSettingsHomeView( navController: NavController, - credentialPacksViewModel: CredentialPacksViewModel + credentialPacksViewModel: CredentialPacksViewModel, + walletActivityLogsViewModel: WalletActivityLogsViewModel ) { - fun back() { - navController.navigate( - Screen.HomeScreen.route.replace("{tab}", "wallet") - ) { - popUpTo(0) - } - } - Column( Modifier .padding(all = 20.dp) .padding(top = 20.dp) ) { - WalletSettingsHomeHeader(onBack = ::back) - WalletSettingsHomeBody(credentialPacksViewModel) + WalletSettingsHomeHeader( + onBack = { + navController.navigate( + Screen.HomeScreen.route.replace("{tab}", "wallet") + ) { + popUpTo(0) + } + } + ) + WalletSettingsHomeBody( + navController = navController, + credentialPacksViewModel = credentialPacksViewModel, + walletActivityLogsViewModel = walletActivityLogsViewModel + ) } } @Composable -fun WalletSettingsHomeHeader( - onBack: () -> Unit -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable { - onBack() - } - ) { - Image( - painter = painterResource(id = R.drawable.chevron), - contentDescription = stringResource(id = R.string.chevron), - modifier = Modifier - .rotate(180f) - .scale(0.7f) - ) +fun WalletSettingsHomeHeader(onBack: () -> Unit) { + Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = "Wallet Settings", + text = "Preferences", fontFamily = Inter, fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, - color = ColorStone950, - modifier = Modifier.padding(start = 10.dp) + fontSize = 20.sp, + color = ColorStone950 ) Spacer(Modifier.weight(1f)) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(36.dp) + .height(36.dp) + .padding(start = 4.dp) + .clip(shape = RoundedCornerShape(8.dp)) + .background(ColorStone950) + .clickable { + onBack() + } + ) { + Image( + painter = painterResource(id = R.drawable.user), + contentDescription = stringResource(id = R.string.user), + colorFilter = ColorFilter.tint(ColorStone50), + modifier = Modifier + .width(20.dp) + .height(20.dp) + ) + } } - } @Composable -fun WalletSettingsHomeBody(credentialPacksViewModel: CredentialPacksViewModel) { +fun WalletSettingsHomeBody( + navController: NavController, + credentialPacksViewModel: CredentialPacksViewModel, + walletActivityLogsViewModel: WalletActivityLogsViewModel +) { Column( Modifier - .padding(horizontal = 20.dp) - .padding(top = 10.dp), + .padding(top = 10.dp) + .navigationBarsPadding(), ) { + Box( + Modifier + .fillMaxWidth() + .clickable { + navController.navigate(Screen.WalletSettingsActivityLogScreen.route) + }, + ) { + Column { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.verification_activity_log), + contentDescription = stringResource(id = R.string.verification_activity_log), + modifier = Modifier.padding(end = 5.dp), + ) + Text( + text = "Activity Log", + fontFamily = Inter, + fontWeight = FontWeight.Medium, + fontSize = 17.sp, + color = ColorStone950, + modifier = Modifier.padding(bottom = 5.dp, top = 5.dp), + ) + } + + Image( + painter = painterResource(id = R.drawable.chevron), + contentDescription = stringResource(id = R.string.chevron), + modifier = Modifier.scale(0.5f) + ) + } + + Text( + text = "View and export activity history", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, + color = ColorStone600, + ) + } + } + Spacer(Modifier.weight(1f)) Button( onClick = { - credentialPacksViewModel.deleteAllCredentialPacks() + credentialPacksViewModel.deleteAllCredentialPacks(onDeleteCredentialPack = { credentialPack -> + GlobalScope.launch { + credentialPack.list().forEach { credential -> + val credentialInfo = + getCredentialIdTitleAndIssuer( + credentialPack, + credential + ) + walletActivityLogsViewModel.saveWalletActivityLog( + walletActivityLogs = WalletActivityLogs( + credentialPackId = credentialPack.id().toString(), + credentialId = credentialInfo.first, + credentialTitle = credentialInfo.second, + issuer = credentialInfo.third, + action = "Deleted", + dateTime = getCurrentSqlDate(), + additionalInformation = "" + ) + ) + } + } + }) }, shape = RoundedCornerShape(5.dp), colors = ButtonDefaults.buttonColors( @@ -101,6 +199,7 @@ fun WalletSettingsHomeBody(credentialPacksViewModel: CredentialPacksViewModel) { ), modifier = Modifier .fillMaxWidth() + .padding(top = 30.dp) ) { Text( text = "Delete all added credentials", @@ -110,4 +209,4 @@ fun WalletSettingsHomeBody(credentialPacksViewModel: CredentialPacksViewModel) { ) } } -} \ No newline at end of file +} diff --git a/example/src/main/res/drawable-hdpi/user.png b/example/src/main/res/drawable-hdpi/user.png deleted file mode 100644 index bfbc914f44fd27f207f03e611b2141eaf0e007c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 582 zcmV-M0=fN(P)1yK}bFMb~xKQBnW@cXWgC~tfBd*nIgD?y~>h=03_=4wlwOT#vcDq}AR|{JQjlr27oq$7Mvxu{I zI?H#pyjA$Ve~Ks!84c}(rbKK=8Lh_#9Nv}&wBf%<8Htp=^syeGJWdlCP0BFEtUImO z7`&m>#gxG`WWBKov|eMdbF>iy##*m2*v>NyWFp6d)@uxt+RlI8OKH8v zV7Rq`tYq4?USmLEtPNx(8$;_g23z(&Y2(PSx<+J~_DqUfWMWyi)=;d~F-DP=>koKa z7>wtw5?K{Jt93I64(mboTbu37XwV564>6lSmZ|+Tt+0Y^u%JE9I|9#}&E^G)`x-^< zk@D=Kuq!w_u=*=;5>R~DYPBw5>=tvSkH4SMRj+LQ$(bax=S|L@z(>kZw$thCYrUxf zgBwv49Ya~bw$^jTlyGoBoz(5R?xwaog(OMVI9cIP#9L~Gwg~+RX4Va!D=2|=?Z*f_ z;7c6$lWABKWthGq0HKbS14s7Y^@A2#7HOM4(3cXrWe48gEh8$wk+u=3d(_r0H~3Ya z&WtH3^!Y`R`8Up@IQmr5|IC}kbEpXQGbVlEM_^wcI zU3VD-!9jTUJntd@_rC8h5aaocpp=@3;VBHmXB82{9M;tWvioNKxk#%wK?&GQN0gBD zMxf4Yq@yHrWW5n+CL8I9=Cj@ih}cL+B*S_m@Y-yoBg)BoBhWZD(h(bi^+xo*O@UrN zv=Yb;B|x3gOf(<;K?z%D{zol1jx&>b&*gVh9M5UlTp7P$3^B}!<9N#YO4znNl13^q zJM)e)5gKTdBpI`>m1Wrg!6wIpj>V;<-aD-Lptd~mS4s(Y`6BWVaIh;%;%l6mHx6_~ uS>S5%VApJPu{33VPef6)>SGaj!utjB-#ARAT7Q550000IRtvJ?!)U&1sPI(6yPNmKv-@2JnnAvw#Aojg59#Rmt$#(F2|>FK2G zc7jNfAn z>Z;UvRiREFGVTOB??OLOKlPo>T zF!Q~0EhAyZ)^F_#4vI?ZCTp;7iXA^Gf~|eQ@z^@6E@A_=So?w_^Sre$Bb@<6vi1ds z)<>7tVBb_b=GMO8u!e@FwJ$iVfuU*b3l7{mk~OI4Mq2xVgM|^1tiitN>N0CzaHMU% z0wiZ7Y1UxhBv~bCwe|%E={;A@8tj|VHqR&!DHN^2zDdCxWsjN`1FeRT>kzFLyeG

MZ0s|hjinx0_oQCioo@=^zg#{aJ$=M%Mn*+ zMZa6Ei_-4a;;NJF5*}JNG3guKNY@qQI|M}(vUE*E+gr0+Jp|615K5S9Vric+E5zYD zypDxX>rbz zlan`gylA1t=xdq8Tf~q_ed_gk9YoQ$dQN3>=hR<_Bm6DL%_u9ZFy)d%kK?ZGh5j#3 zkbTj@H)jmSVob&s;a*K9lNZ*$;NXgm$Kwyf#eIC36-}`Zm&cJ#K_s zsgb@@i}AYDb@ui{5M-cD469{C3rd(mB(^!dPt{n@u|x~uSaf;s9(p>p2Q{7S;;hL$ zzFSn*_JF2S%ak(&A_f0r4`@1FwjJF&7O^o2RKX-r1(QG(OafIfnpY`fBbuO1G%w@E zMpu_b^OjC*94u~8jf&ef$H&J#{?PPrb76(Uph;1S0e(TPtl{D2!iuPJ52ybJ5(ku~ T4W4)U00000NkvXXu0mjfR5BrE diff --git a/example/src/main/res/drawable-xxxhdpi/user.png b/example/src/main/res/drawable-xxxhdpi/user.png deleted file mode 100644 index 2c4cfe7dda80dd1d4190cb8c501d45e3b3890b33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 768 zcmV+b1ONPqP)Sgcke76}$emBy8;RQdn!NXY04#7O&Ia%QM|fYaE5mz)dm zCE9*@G0JAMd5!M(`)za>hF{UX??0j;gg?UH(d@Zz(d~BoDJp+Hxc~#G3<`w;E)@77 zyRQ2lWgsVD07_t}2;&eU3s=DOIrv=Jp4Gtsl)&V=P@t-!9kPAR!vK`XhMZY3G+oV* z!~m4Q@-NhzY;QHStJ!5L15gGVp@oi{5*tC;e)li{C9+HqFi`f#9SlGjtIN(y zHnTxv0Lqj)UM_7jlQ9EOCfMD0owXldeaim0BQOA6WXHTI`{NGTA#ck5xI=cxo3cOd zkR9@-?2kJznaPehFfATrf847A?Fkf zzzA6W?RL8#(S^c2BsPN0FEj6j0T=^?!cdVNWh@R={rKt=&tY(^c#q<+O&-EY<)PfauluRy;jWR?vnv&OefS&&Yu=1zF y)+VlSG5{r~06=S(Y7Ls|Ivqnx7(j6s1NaNn_KTw^ZXWjl0000 + + diff --git a/example/src/main/res/xml/provider_paths.xml b/example/src/main/res/xml/provider_paths.xml index f742886..c424de8 100644 --- a/example/src/main/res/xml/provider_paths.xml +++ b/example/src/main/res/xml/provider_paths.xml @@ -3,6 +3,9 @@ +