diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/navEditor.xml b/.idea/navEditor.xml
index 74443cb..7f0ad94 100644
--- a/.idea/navEditor.xml
+++ b/.idea/navEditor.xml
@@ -272,7 +272,12 @@
-
+
+
+
+
+
+
@@ -287,14 +292,26 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -303,16 +320,25 @@
+
+
+
-
+
-
-
+
+
diff --git a/.idea/other.xml b/.idea/other.xml
index 4604c44..94c96f6 100644
--- a/.idea/other.xml
+++ b/.idea/other.xml
@@ -69,6 +69,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -91,6 +113,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -102,6 +135,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -146,6 +190,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -235,6 +290,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
index 7650e83..11ca1c1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,6 +5,7 @@ plugins {
id 'com.google.protobuf'
id 'kotlin-parcelize'
id 'com.google.android.gms.oss-licenses-plugin'
+ id 'com.google.devtools.ksp'
}
def version = '1.8'
@@ -66,9 +67,17 @@ protobuf {
}
dependencies {
+ api "androidx.security:security-crypto:1.1.0-alpha06"
+
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
+
+ //Room for data storage
+ api "androidx.room:room-runtime:$room_version"
+ api "androidx.room:room-ktx:$room_version"
+ ksp "androidx.room:room-compiler:$room_version"
+
//AndroidX
def lifecycle_version = "2.8.4"
implementation 'androidx.core:core-ktx:1.13.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a7675a1..564adbb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,10 +14,12 @@
+
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ClassicPowerMenu.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ClassicPowerMenu.kt
index f8a2f74..dd3a0ea 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ClassicPowerMenu.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ClassicPowerMenu.kt
@@ -4,7 +4,6 @@ import android.content.Context
import android.os.HandlerThread
import android.os.Looper
import androidx.lifecycle.lifecycleScope
-import androidx.navigation.Navigation
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger
import com.android.systemui.controls.CustomIconCache
@@ -37,8 +36,6 @@ import com.kieronquinn.app.classicpowermenu.components.monet.MonetColorProvider
import com.kieronquinn.app.classicpowermenu.components.navigation.*
import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.WalletActivityStarterImpl
import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.WalletCardBlurProviderImpl
-import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.LoyaltyCardsRepository
-import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.LoyaltyCardsRepositoryImpl
import com.kieronquinn.app.classicpowermenu.components.settings.Settings
import com.kieronquinn.app.classicpowermenu.components.settings.SettingsImpl
import com.kieronquinn.app.classicpowermenu.components.starter.PowerMenuStarter
@@ -72,8 +69,21 @@ import com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswalle
import com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.autoswitchservice.SettingsQuickAccessWalletAutoSwitchServiceViewModelImpl
import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.autoswitch.AutoSwitchServicesRepository
import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.autoswitch.AutoSwitchServicesRepositoryImpl
-import com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.rearrange.SettingsQuickAccessWalletRearrangeViewModel
-import com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.rearrange.SettingsQuickAccessWalletRearrangeViewModelImpl
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.GoogleApiRepository
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.GoogleApiRepositoryImpl
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.GoogleWalletRepository
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.GoogleWalletRepositoryImpl
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.ValuablesDatabaseRepository
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.ValuablesDatabaseRepositoryImpl
+import com.kieronquinn.app.classicpowermenu.components.settings.EncryptedSettings
+import com.kieronquinn.app.classicpowermenu.components.settings.EncryptedSettingsImpl
+import com.kieronquinn.app.classicpowermenu.components.settings.RoomEncryptedSettingsRepository
+import com.kieronquinn.app.classicpowermenu.components.settings.RoomEncryptedSettingsRepositoryImpl
+import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.database.WalletDatabase
+import com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.loyaltycards.SignInWithGoogleViewModel
+import com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.loyaltycards.SignInWithGoogleViewModelImpl
+import com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.manage.SettingsQuickAccessWalletManageViewModel
+import com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.manage.SettingsQuickAccessWalletManageViewModelImpl
import com.kieronquinn.app.classicpowermenu.ui.screens.settings.rootcheck.SettingsRootCheckViewModel
import com.kieronquinn.app.classicpowermenu.ui.screens.settings.rootcheck.SettingsRootCheckViewModelImpl
import com.kieronquinn.app.classicpowermenu.ui.screens.settings.update.UpdateAvailableBottomSheetViewModel
@@ -125,7 +135,12 @@ class ClassicPowerMenu: LifecycleApplication() {
single { PowerMenuNavigationImpl() }
single { ContainerNavigationImpl() }
single { AppNavigationImpl() }
+ single { WalletDatabase.getDatabase(get()) }
+ single { ValuablesDatabaseRepositoryImpl(get()) }
+ single { get() as RoomEncryptedSettingsRepository }
single { SettingsImpl(get()) }
+ single { EncryptedSettingsImpl(get()) }
+ single { RoomEncryptedSettingsRepositoryImpl(get()) }
single { UpdateChecker() }
}
}
@@ -133,8 +148,9 @@ class ClassicPowerMenu: LifecycleApplication() {
private val walletModule = module {
single { WalletActivityStarterImpl(get(), get()) }
single { WalletCardBlurProviderImpl(get(), get()) }
- single { LoyaltyCardsRepositoryImpl(get(), get(), get()) }
single { AutoSwitchServicesRepositoryImpl(get(), get()) }
+ single { GoogleWalletRepositoryImpl(get(), get(), get(), get(), get()) }
+ single { GoogleApiRepositoryImpl(get(), get()) }
}
private val monetModule = module {
@@ -159,10 +175,11 @@ class ClassicPowerMenu: LifecycleApplication() {
viewModel { SettingsMainViewModelImpl(get(), get(), get()) }
viewModel { SettingsPowerOptionsViewModelImpl(get(), get()) }
viewModel { SettingsPowerOptionsRearrangeViewModelImpl(get(), get()) }
- viewModel { SettingsQuickAccessWalletViewModelImpl(get(), get(), get(), get()) }
+ viewModel { SettingsQuickAccessWalletViewModelImpl(get(), get(), get(), get(), get(), get(), get()) }
viewModel { SettingsDeviceControlsViewModelImpl(get(), get()) }
- viewModel { SettingsQuickAccessWalletRearrangeViewModelImpl(get(), get(), get()) }
+ viewModel { SettingsQuickAccessWalletManageViewModelImpl(get(), get(), get(), get()) }
viewModel { SettingsQuickAccessWalletAutoSwitchServiceViewModelImpl(get(), get(), get()) }
+ viewModel { SignInWithGoogleViewModelImpl(get(), get(), get()) }
viewModel { SettingsDeveloperOptionsViewModelImpl(get()) }
viewModel { SettingsRootCheckViewModelImpl(get()) }
viewModel { UpdateAvailableBottomSheetViewModelImpl(get()) }
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/GooglePayConstants.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/GooglePayConstants.kt
index 7ae48a0..37f0393 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/GooglePayConstants.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/GooglePayConstants.kt
@@ -1,6 +1,5 @@
package com.kieronquinn.app.classicpowermenu.components.quickaccesswallet
-import android.content.ComponentName
import android.content.pm.PackageManager
import com.kieronquinn.app.classicpowermenu.utils.extensions.isAppInstalled
@@ -8,7 +7,6 @@ object GooglePayConstants {
const val WALLET_NFC_REL_PACKAGE_NAME = "com.google.android.apps.walletnfcrel"
const val NBU_PAISA_PACKAGE_NAME = "com.google.android.apps.nbu.paisa.user"
- val WALLET_DEEP_LINK_COMPONENT = ComponentName(WALLET_NFC_REL_PACKAGE_NAME, "com.google.commerce.tapandpay.android.deeplink.DeepLinkActivity")
const val WALLET_PREF_NAME = "global_prefs"
const val WALLET_CURRENT_ACCOUNT_ID_PREF_KEY = "current_account_id"
const val WALLET_DEEP_LINK_VALUABLE = "https://pay.google.com/gp/v/valuable/%s?vs=gp_lp"
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/DatabaseRepository.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/DatabaseRepository.kt
new file mode 100644
index 0000000..159691a
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/DatabaseRepository.kt
@@ -0,0 +1,60 @@
+package com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards
+
+import com.kieronquinn.app.classicpowermenu.components.settings.RoomEncryptedSettingsRepository
+import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.database.WalletDatabase
+import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.database.WalletValuable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+
+interface ValuablesDatabaseRepository {
+
+ fun getWalletValuables(): Flow>
+ fun getWalletValuableById(id: String): Flow
+
+ suspend fun addWalletValuable(walletValuable: WalletValuable)
+ suspend fun deleteWalletValuable(id: String)
+ suspend fun deleteAllRecords()
+
+}
+
+class ValuablesDatabaseRepositoryImpl(
+ database: WalletDatabase
+): ValuablesDatabaseRepository, RoomEncryptedSettingsRepository.RoomEncryptionFailedCallback {
+
+ private val walletValuable = database.walletValuableDao()
+ private val databaseLock = Mutex()
+
+ override fun getWalletValuables() = walletValuable.getAll()
+
+ override fun getWalletValuableById(id: String): Flow {
+ return walletValuable.getValuableById(id)
+ }
+
+ override suspend fun addWalletValuable(
+ walletValuable: WalletValuable
+ ) = withContext(Dispatchers.IO) {
+ databaseLock.withLock {
+ this@ValuablesDatabaseRepositoryImpl.walletValuable.insert(walletValuable)
+ }
+ }
+
+ override suspend fun deleteWalletValuable(id: String) = withContext(Dispatchers.IO) {
+ databaseLock.withLock {
+ this@ValuablesDatabaseRepositoryImpl.walletValuable.delete(id)
+ }
+ }
+
+ override suspend fun deleteAllRecords() = withContext(Dispatchers.IO) {
+ databaseLock.withLock {
+ this@ValuablesDatabaseRepositoryImpl.walletValuable.clear()
+ }
+ }
+
+ override fun onEncryptionFailed() {
+ walletValuable.clear()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/GoogleApiRepository.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/GoogleApiRepository.kt
new file mode 100644
index 0000000..54bc56d
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/GoogleApiRepository.kt
@@ -0,0 +1,191 @@
+package com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards
+
+import android.content.Context
+import android.content.pm.PackageManager.NameNotFoundException
+import android.os.Build
+import com.kieronquinn.app.classicpowermenu.utils.Result
+import com.kieronquinn.app.classicpowermenu.utils.extensions.getPackageInfoCompat
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.GoogleApiRepository.Scope
+import com.kieronquinn.app.classicpowermenu.components.settings.EncryptedSettings
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import okhttp3.ResponseBody
+import retrofit2.Call
+import retrofit2.Retrofit
+import retrofit2.http.Field
+import retrofit2.http.FormUrlEncoded
+import retrofit2.http.Header
+import retrofit2.http.POST
+import java.util.Locale
+
+interface GoogleApiRepository {
+
+ suspend fun getAasToken(oauthToken: String): String?
+ suspend fun getToken(scope: Scope): String?
+ fun isSignedIn(): Boolean
+
+ enum class Scope(val service: String, val clientSig: String, val packageName: String) {
+ AC2DM(
+ "ac2dm",
+ "38918a453d07199354f8b19af05ec6562ced5788",
+ "com.google.android.gms"
+ ),
+ WALLET(
+ "oauth2:https://www.googleapis.com/auth/tapandpay",
+ "38918a453d07199354f8b19af05ec6562ced5788",
+ "com.google.android.gms"
+ )
+ }
+
+}
+
+class GoogleApiRepositoryImpl(val context: Context, val encryptedSettings: EncryptedSettings): GoogleApiRepository {
+
+ companion object {
+ private const val PACKAGE_GMS = "com.google.android.gms"
+ private const val VERSION_GMS_DEFAULT = "19629032"
+ }
+
+ private val service = Retrofit.Builder()
+ .baseUrl("http://localhost/")
+ .build()
+ .create(GoogleApiService::class.java)
+
+ override suspend fun getAasToken(oauthToken: String): String? = withContext(Dispatchers.IO) {
+ val tokenResult = service.run {
+ getAasToken(
+ oauthToken = oauthToken,
+ playServicesVersion = getPlayServicesVersion()
+ )
+ }
+ val tokenBody = when(tokenResult){
+ is Result.Success -> String(tokenResult.data.bytes())
+ is Result.Failed -> return@withContext null
+ }
+ tokenBody.decodeForm()["Token"]
+ }
+
+ override suspend fun getToken(scope: Scope): String? = withContext(Dispatchers.IO) {
+ val aasToken = encryptedSettings.aasToken
+ if(aasToken.isEmpty()) return@withContext null
+ val tokenResult = service.run {
+ getToken(
+ aasToken = aasToken,
+ playServicesVersion = getPlayServicesVersion(),
+ headerApp = scope.packageName,
+ app = scope.packageName,
+ service = scope.service,
+ clientSig = scope.clientSig,
+ callerSig = scope.clientSig,
+ callerPackage = scope.packageName
+ )
+ }
+ val tokenBody = when(tokenResult){
+ is Result.Success -> String(tokenResult.data.bytes())
+ is Result.Failed -> {
+ if(tokenResult.code == 403) {
+ //AAS Token has been revoked
+ encryptedSettings.aasToken = ""
+ }
+ return@withContext null
+ }
+ }
+ tokenBody.decodeForm()["Auth"]
+ }
+
+ override fun isSignedIn(): Boolean {
+ return runBlocking {
+ encryptedSettings.aasToken.isNotEmpty()
+ }
+ }
+
+ private fun getPlayServicesVersion(): String {
+ return try {
+ context.packageManager.getPackageInfoCompat(PACKAGE_GMS).longVersionCode.toString()
+ }catch (e: NameNotFoundException){
+ VERSION_GMS_DEFAULT
+ }
+ }
+
+ private fun String.decodeForm(): Map {
+ return lines().associate {
+ it.parseCookie()
+ }
+ }
+
+ fun String.parseCookie(): Pair {
+ val split = indexOf("=")
+ val name = substring(0, split).trim()
+ val value = substring(split + 1, length)
+ return Pair(name, value)
+ }
+
+ private fun GoogleApiService.run(block: GoogleApiService.() -> Call): Result {
+ val result = try {
+ block(this).execute()
+ }catch (e: Exception) {
+ return Result.Failed(999)
+ }
+ val body = result.body()
+ return if(result.isSuccessful && body != null) {
+ Result.Success(body)
+ }else{
+ Result.Failed(result.code())
+ }
+ }
+
+
+}
+
+
+interface GoogleApiService {
+
+ companion object {
+ private fun getLocale(): String {
+ val locale = Locale.getDefault()
+ return "${locale.language}-${locale.country}"
+ }
+
+ private fun getCountry(): String {
+ return Locale.getDefault().country.lowercase()
+ }
+ }
+
+ @FormUrlEncoded
+ @POST("https://android.clients.google.com/auth")
+ fun getAasToken(
+ @Field("Token") oauthToken: String,
+ @Field("lang") lang: String = getLocale(),
+ @Field("google_play_services_version") playServicesVersion: String,
+ @Field("sdk_version") sdkVersion: String = Build.VERSION.SDK_INT.toString(),
+ @Field("device_country") deviceCountry: String = getCountry(),
+ @Field("service") service: String = Scope.AC2DM.service,
+ @Field("get_accountid") getAccountId: String = "1",
+ @Field("ACCESS_TOKEN") systemPartition: String = "1",
+ @Field("callerPkg") callerPackage: String = Scope.AC2DM.packageName,
+ @Field("callerSig") callerSig: String = Scope.AC2DM.clientSig,
+ @Field("add_account") addAccount: String = "1"
+ ): Call
+
+ @FormUrlEncoded
+ @POST("https://android.googleapis.com/auth")
+ fun getToken(
+ @Header("app") headerApp: String,
+ @Field("lang") lang: String = getLocale(),
+ @Field("google_play_services_version") playServicesVersion: String,
+ @Field("sdk_version") sdkVersion: String = Build.VERSION.SDK_INT.toString(),
+ @Field("device_country") deviceCountry: String = getCountry(),
+ @Field("check_email") checkEmail: String = "1",
+ @Field("oauth2_foreground") oauth2Foreground: String = "1",
+ @Field("token_request_options") tokenRequestOptions: String = "CAA4AVAB",
+ @Field("app") app: String,
+ @Field("service") service: String,
+ @Field("client_sig") clientSig: String,
+ @Field("system_partition") systemPartition: String = "1",
+ @Field("callerPkg") callerPackage: String,
+ @Field("Token") aasToken: String,
+ @Field("callerSig") callerSig: String
+ ): Call
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/GoogleWalletRepository.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/GoogleWalletRepository.kt
new file mode 100644
index 0000000..28c56cf
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/GoogleWalletRepository.kt
@@ -0,0 +1,289 @@
+package com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.os.StrictMode
+import android.util.Base64
+import android.util.Log
+import androidx.core.content.res.ResourcesCompat
+import com.android.systemui.plugin.globalactions.wallet.WalletCardViewInfo
+import com.google.internal.tapandpay.v1.valuables.SyncValuablesRequestProto.SyncValuablesRequest
+import com.google.internal.tapandpay.v1.valuables.SyncValuablesRequestProto.SyncValuablesRequest.SyncValuablesRequestInner.Request
+import com.google.internal.tapandpay.v1.valuables.SyncValuablesResponseProto
+import com.google.internal.tapandpay.v1.valuables.SyncValuablesResponseProto.SyncValuablesResponse.Inner.Valuables.Valuable
+import com.google.internal.tapandpay.v1.valuables.ValuableWrapperProto
+import com.google.internal.tapandpay.v1.valuables.ValuableWrapperProto.ValuableWrapper
+import com.google.protobuf.ByteString
+import com.kieronquinn.app.classicpowermenu.R
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.GooglePayConstants
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.GoogleWalletRepository.SyncValuablesResult
+import com.kieronquinn.app.classicpowermenu.components.settings.EncryptedSettings
+import com.kieronquinn.app.classicpowermenu.components.settings.Settings
+import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.LoyaltyCard
+import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.WalletLoyaltyCardViewInfo
+import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.database.WalletValuable
+import com.kieronquinn.app.classicpowermenu.utils.extensions.CONTENT_TYPE_PROTOBUF
+import com.kieronquinn.app.classicpowermenu.utils.extensions.compress
+import com.kieronquinn.app.classicpowermenu.utils.extensions.toColor
+import com.kieronquinn.app.classicpowermenu.utils.extensions.toRequestBody
+import com.kieronquinn.app.classicpowermenu.utils.room.EncryptedValue
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+import okhttp3.RequestBody
+import okhttp3.ResponseBody
+import retrofit2.Call
+import retrofit2.Retrofit
+import retrofit2.http.Body
+import retrofit2.http.Header
+import retrofit2.http.POST
+import java.net.URL
+import java.time.ZoneId
+import java.time.format.TextStyle
+import java.util.Locale
+
+
+interface GoogleWalletRepository {
+
+ suspend fun syncValuables(): SyncValuablesResult
+
+ suspend fun getLoyaltyCards(onCardClicked: (LoyaltyCard) -> Boolean, ignoreSetting: Boolean = false): List?
+
+ /**
+ * Current valuables from the database. Call [syncValuables] to trigger a sync.
+ */
+ fun getValuables(): Flow>
+
+
+ enum class SyncValuablesResult {
+ /**
+ * Sync was successful
+ */
+ SUCCESS,
+
+ /**
+ * A non-fatal error, for example a network connection issue, occurred
+ */
+ ERROR,
+
+ /**
+ * The request returned a bad auth response, and then token refresh also failed. Likely
+ * will require a re-log.
+ */
+ FATAL_ERROR
+ }
+
+}
+
+class GoogleWalletRepositoryImpl(
+ private val context: Context,
+ private val settings: Settings,
+ private val encryptedSettings: EncryptedSettings,
+ private val googleApiRepository: GoogleApiRepository,
+ private val valuablesDatabaseRepository: ValuablesDatabaseRepository
+): GoogleWalletRepository {
+
+ companion object {
+ private const val HEADER_REQUEST = "ExoBBA8FCxQMCAcJAwYOCg0QEhYYHA=="
+ }
+
+ private val isGooglePayInstalled by lazy {
+ GooglePayConstants.isGooglePayInstalled(context.packageManager)
+ }
+
+ private val googleSansMedium by lazy {
+ ResourcesCompat.getFont(context, R.font.google_sans_text_medium)!!
+ }
+
+ init {
+ val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
+ StrictMode.setThreadPolicy(policy)
+ }
+
+ override suspend fun syncValuables() = withContext(Dispatchers.IO) {
+ syncValuablesInternal()
+ }
+
+ override suspend fun getLoyaltyCards(onCardClicked: (LoyaltyCard) -> Boolean, ignoreSetting: Boolean): List? {
+ if((!settings.quickAccessWalletShowLoyaltyCards && !ignoreSetting) || !isGooglePayInstalled) return null
+ return getValuables().first().map { WalletLoyaltyCardViewInfo(context, it, googleSansMedium, onCardClicked) }
+ }
+
+ override fun getValuables(): Flow> {
+ return valuablesDatabaseRepository.getWalletValuables().map { it.map { it.toValuable() } }
+ }
+
+ private fun WalletValuable.toValuable(): LoyaltyCard {
+ val valuable = ValuableWrapper.parseFrom(valuable.bytes)
+
+ val imageBytes = image?.bytes
+ val bitmap = if (imageBytes != null) BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) else null
+ val loyaltyCard = LoyaltyCard(
+ id,
+ bitmap,
+ valuable.loyaltyCard.issuerInfo.id,
+ valuable.loyaltyCard.issuerInfo.title,
+ valuable.loyaltyCard.issuerInfo.issuerName,
+ valuable.loyaltyCard.groupingInfo.backgroundColor.toColor(),
+ LoyaltyCard.RedemptionInfo(
+ valuable.loyaltyCard.redemptionInfo.identifier,
+ valuable.loyaltyCard.redemptionInfo.barcode.typeValue,
+ valuable.loyaltyCard.redemptionInfo.barcode.encodedValue,
+ valuable.loyaltyCard.redemptionInfo.barcode.displayText,
+ valuable.loyaltyCard.redemptionInfo.barcode.displayText))
+ return loyaltyCard
+ }
+
+ private val service = Retrofit.Builder()
+ .baseUrl(GoogleWalletService.BASE_URL)
+ .build()
+ .create(GoogleWalletService::class.java)
+
+
+ private fun createSyncValuablesRequest(currentValuables: List): SyncValuablesRequest {
+ val request = SyncValuablesRequest.SyncValuablesRequestInner.newBuilder()
+ .setRequest(buildRequest(currentValuables))
+ .build()
+ return SyncValuablesRequest.newBuilder()
+ .setRequest(request)
+ .build()
+ }
+
+ private fun buildRequest(currentValuables: List): Request {
+ val cachedValuables = currentValuables.map {
+ Request.CachedValuable.newBuilder()
+ .setId(it.id)
+ .setHash(it.hash)
+ .build()
+ }
+ return Request.newBuilder()
+ .setHeader(ByteString.copyFrom(Base64.decode(HEADER_REQUEST, Base64.DEFAULT)))
+ .setTimezone(ZoneId.systemDefault().getDisplayName(
+ TextStyle.FULL_STANDALONE, Locale.ENGLISH
+ ))
+ .addAllCachedValuable(cachedValuables)
+ .build()
+ }
+
+ private fun GoogleWalletService.syncValuables(
+ token: String,
+ currentValuables: List
+ ): SyncValuablesResponse {
+ val requestBody = createSyncValuablesRequest(currentValuables).toByteArray()
+ .toRequestBody(CONTENT_TYPE_PROTOBUF)
+ return try {
+ val response = syncValuables("Bearer $token", body = requestBody).execute()
+ when(response.code()){
+ 200 -> {
+ response.body()?.let { body ->
+ SyncValuablesResponse.Success(body)
+ } ?: SyncValuablesResponse.GenericError
+ }
+ 401 -> SyncValuablesResponse.BadAuthentication
+ else -> SyncValuablesResponse.GenericError
+ }
+ }catch (e: Exception){
+ SyncValuablesResponse.GenericError
+ }
+ }
+
+ private suspend fun syncValuablesInternal(isRetry: Boolean = false): SyncValuablesResult {
+ val token = getToken() ?: return SyncValuablesResult.FATAL_ERROR
+ val result = loadValuables(token)
+ return when {
+ result == SyncValuablesResult.FATAL_ERROR && !isRetry -> {
+ encryptedSettings.walletToken = ""
+ syncValuablesInternal(true)
+ }
+ else -> result
+ }
+ }
+
+ private suspend fun getToken(): String? {
+ val token = encryptedSettings.walletToken
+ if(token.isNotEmpty()) return token
+ return googleApiRepository.getToken(GoogleApiRepository.Scope.WALLET)?.also {
+ encryptedSettings.walletToken = it
+ }
+ }
+
+ private suspend fun getCurrentValuables(): List {
+ return valuablesDatabaseRepository.getWalletValuables().first().map {
+ Valuable.newBuilder()
+ .setId(it.id)
+ .setHash(it.hash)
+ .build()
+ }
+ }
+
+ private suspend fun loadValuables(token: String): SyncValuablesResult {
+ val valuables = service.run {
+ syncValuables(token, getCurrentValuables())
+ }
+ return when(valuables) {
+ is SyncValuablesResponse.Success -> {
+ val bytes = valuables.body.bytes()
+ val response = SyncValuablesResponseProto.SyncValuablesResponse.parseFrom(bytes)
+ commitValuables(response)
+ SyncValuablesResult.SUCCESS
+ }
+ is SyncValuablesResponse.GenericError -> SyncValuablesResult.ERROR
+ is SyncValuablesResponse.BadAuthentication -> SyncValuablesResult.FATAL_ERROR
+ }
+ }
+
+ private suspend fun commitValuables(response: SyncValuablesResponseProto.SyncValuablesResponse) {
+ val valuableList = response.inner.valuables.valuableList
+ valuableList.forEach {
+ // For now we only care about loyalty cards
+ if(it.hash != 0L && it.valuable.isValidValuable()) {
+
+ var image: Bitmap? = null
+ if (it.valuable.loyaltyCard.groupingInfo.groupingImage.uri.isNotEmpty()) {
+ val url = URL(it.valuable.loyaltyCard.groupingInfo.groupingImage.uri)
+ image = BitmapFactory.decodeStream(url.openConnection().getInputStream())
+ }
+
+ val valuable = WalletValuable(
+ it.id,
+ it.hash,
+ EncryptedValue(it.valuable.toByteArray()),
+ if (image != null) image.compress()?.let { compressed -> EncryptedValue(compressed) } else null
+ )
+ valuablesDatabaseRepository.addWalletValuable(valuable)
+ }else{
+ valuablesDatabaseRepository.deleteWalletValuable(it.id)
+ }
+ }
+ }
+
+ private fun ValuableWrapper.isValidValuable(): Boolean {
+ if(loyaltyCard == null) return false
+ if(loyaltyCard.issuerInfo.issuerName.isNullOrBlank()) return false
+ return true
+ }
+
+ sealed class SyncValuablesResponse {
+ data class Success(val body: ResponseBody): SyncValuablesResponse()
+ object BadAuthentication: SyncValuablesResponse()
+ object GenericError: SyncValuablesResponse()
+ }
+
+}
+
+interface GoogleWalletService {
+
+ companion object {
+ const val BASE_URL = "https://pay-users-pa.googleapis.com/"
+ }
+
+ @POST("g/valuables/syncvaluables")
+ fun syncValuables(
+ @Header("Authorization") authorization: String,
+ @Body body: RequestBody
+ ): Call
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/LoyaltyCardsRepository.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/LoyaltyCardsRepository.kt
deleted file mode 100644
index ca9b7c5..0000000
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/quickaccesswallet/loyaltycards/LoyaltyCardsRepository.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-package com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards
-
-import android.content.Context
-import android.database.sqlite.SQLiteDatabase
-import android.graphics.BitmapFactory
-import android.graphics.drawable.BitmapDrawable
-import android.os.ParcelFileDescriptor
-import android.util.Log
-import androidx.core.content.res.ResourcesCompat
-import com.android.systemui.plugin.globalactions.wallet.WalletCardViewInfo
-import com.kieronquinn.app.classicpowermenu.IClassicPowerMenu
-import com.kieronquinn.app.classicpowermenu.R
-import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.GooglePayConstants
-import com.kieronquinn.app.classicpowermenu.components.settings.Settings
-import com.kieronquinn.app.classicpowermenu.model.protobuf.loyaltycard.LoyaltyCardProtos
-import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.LoyaltyCard
-import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.WalletLoyaltyCardViewInfo
-import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.extract
-import com.kieronquinn.app.classicpowermenu.service.container.CPMServiceContainer
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.util.*
-import kotlin.collections.ArrayList
-import kotlin.collections.HashMap
-
-interface LoyaltyCardsRepository {
-
- suspend fun getLoyaltyCards(onCardClicked: (LoyaltyCard) -> Boolean, ignoreSetting: Boolean = false): List?
-
-}
-
-class LoyaltyCardsRepositoryImpl(private val service: CPMServiceContainer, private val context: Context, private val settings: Settings): LoyaltyCardsRepository {
-
- private val isGooglePayInstalled by lazy {
- GooglePayConstants.isGooglePayInstalled(context.packageManager)
- }
-
- private val googleSansMedium by lazy {
- ResourcesCompat.getFont(context, R.font.google_sans_text_medium)!!
- }
-
- override suspend fun getLoyaltyCards(onCardClicked: (LoyaltyCard) -> Boolean, ignoreSetting: Boolean): List? {
- if((!settings.quickAccessWalletShowLoyaltyCards && !ignoreSetting) || !isGooglePayInstalled) return null
- return service.runWithService { service ->
- withContext(Dispatchers.IO) {
- context.withTemporaryFile {
- service.googlePayDatabaseForLoyalty?.loadRemoteFile(it) ?: return@withTemporaryFile null
- val database = SQLiteDatabase.openOrCreateDatabase(it, null)
- val valuableImageFileNames = getValuableImageList(database)
- getValuables(database, valuableImageFileNames, service)
- }
- }
- }?.map { WalletLoyaltyCardViewInfo(context, it, googleSansMedium, onCardClicked) }
- }
-
- private fun getValuableImageList(database: SQLiteDatabase): HashMap {
- val valuableImageFileNames = HashMap()
- val valuableImagesCursor = database.rawQuery("select valuable_id,file_name from valuable_images", null, null)
- valuableImagesCursor.moveToFirst()
- if(valuableImagesCursor.isAfterLast) return valuableImageFileNames
- do {
- if(valuableImagesCursor.isAfterLast) break
- valuableImageFileNames[valuableImagesCursor.getString(0)] = valuableImagesCursor.getString(1)
- }while (valuableImagesCursor.moveToNext())
- valuableImagesCursor.close()
- return valuableImageFileNames
- }
-
- private fun getValuables(database: SQLiteDatabase, valuableImageFileNames: HashMap, service: IClassicPowerMenu): ArrayList {
- val loyaltyCards = ArrayList()
- //Vertical ID 1 = LOYALTY_CARD (See CommonProto.ValuableType in Google Pay)
- val cursor = database.rawQuery("select valuable_id,proto from valuables where vertical_id='1'", null, null)
- cursor.moveToFirst()
- if(cursor.isAfterLast) return loyaltyCards
- do {
- if(cursor.isAfterLast) break
- val valuableId = cursor.getString(0)
- val valuableIcon = valuableImageFileNames[valuableId]?.let { valuableImageFileName ->
- val bitmap = context.withTemporaryFile {
- service.getGooglePayLoyaltyImageForId(valuableImageFileName)?.loadRemoteFile(it) ?: return@withTemporaryFile null
- BitmapFactory.decodeFile(it.absolutePath)
- }
- bitmap
- }
- val blob = cursor.getBlob(1)
- val loyaltyCard = LoyaltyCardProtos.LoyaltyCard.parseFrom(blob)
- loyaltyCards.add(loyaltyCard.extract(valuableIcon, context))
- } while (cursor.moveToNext())
- cursor.close()
- return loyaltyCards
- }
-
- private fun ParcelFileDescriptor.loadRemoteFile(into: File){
- ParcelFileDescriptor.AutoCloseInputStream(this).buffered().use { stream ->
- stream.copyTo(into.outputStream())
- }
- }
-
- private fun Context.withTemporaryFile(block: (File) -> T): T {
- val file = File(cacheDir, UUID.randomUUID().toString())
- val result = block.invoke(file)
- file.delete()
- return result
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/settings/EncryptedSettings.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/settings/EncryptedSettings.kt
new file mode 100644
index 0000000..d93c643
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/settings/EncryptedSettings.kt
@@ -0,0 +1,114 @@
+package com.kieronquinn.app.classicpowermenu.components.settings
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.graphics.Color
+import com.kieronquinn.app.classicpowermenu.BuildConfig
+import com.kieronquinn.app.classicpowermenu.model.power.PowerMenuButtonId
+import com.kieronquinn.app.classicpowermenu.utils.extensions.createEncryptedSharedPrefDestructively
+import com.kieronquinn.app.classicpowermenu.utils.extensions.toHexString
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+abstract class EncryptedSettings {
+
+ abstract var aasToken: String
+ abstract var walletToken: String
+
+}
+
+class EncryptedSettingsImpl(context: Context): EncryptedSettings() {
+
+ companion object {
+
+ private const val KEY_AAS_TOKEN = "aas_token"
+ private const val KEY_WALLET_TOKEN = "wallet_token"
+
+ }
+
+ override var aasToken by shared(KEY_AAS_TOKEN, "")
+ override var walletToken by shared(KEY_WALLET_TOKEN, "")
+
+ private val sharedPreferences: SharedPreferences by lazy {
+ context.createEncryptedSharedPrefDestructively("encrypted_shared_prefs")
+ }
+
+ private fun shared(key: String, default: Boolean) = ReadWriteProperty({
+ sharedPreferences.getBoolean(key, default)
+ }, {
+ sharedPreferences.edit().putBoolean(key, it).commit()
+ })
+
+ private fun shared(key: String, default: String) = ReadWriteProperty({
+ sharedPreferences.getString(key, default) ?: default
+ }, {
+ sharedPreferences.edit().putString(key, it).commit()
+ })
+
+ private inline fun sharedList(key: String, default: List, crossinline transform: (List) -> String, crossinline reverseTransform: (String) -> List) = ReadWriteProperty({
+ reverseTransform(sharedPreferences.getString(key, null) ?: transform(default))
+ }, {
+ sharedPreferences.edit().putString(key, transform(it)).commit()
+ })
+
+ private fun sharedColor(key: String) = ReadWriteProperty({
+ val rawColor = sharedPreferences.getString(key, "") ?: ""
+ if(rawColor.isEmpty()) null
+ else Color.parseColor(rawColor)
+ }, {
+ sharedPreferences.edit().putString(key, it?.toHexString() ?: "").commit()
+ })
+
+ private inline fun > sharedEnum(key: String, default: Enum): ReadWriteProperty {
+ return object: ReadWriteProperty {
+
+ override operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
+ return java.lang.Enum.valueOf(T::class.java, sharedPreferences.getString(key, default.name))
+ }
+
+ override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+ sharedPreferences.edit().putString(key, value.name).commit()
+ }
+
+ }
+ }
+
+ private inline fun ReadWriteProperty(crossinline getValue: () -> T, crossinline setValue: (T) -> Unit): ReadWriteProperty {
+ return object: ReadWriteProperty {
+
+ override operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
+ return getValue.invoke()
+ }
+
+ override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+ setValue.invoke(value)
+ }
+
+ }
+ }
+
+ private fun stringListTypeConverter(list: List) : String {
+ if(list.isEmpty()) return ""
+ return list.joinToString(",")
+ }
+
+ private fun stringListTypeReverseConverter(pref: String): List {
+ if(pref.isEmpty()) return emptyList()
+ if(!pref.contains(",")) return listOf(pref.trim())
+ return pref.split(",")
+ }
+
+ private fun powerMenuButtonTypeConverter(list: List): String {
+ if(list.isEmpty()) return ""
+ return list.joinToString { it.name }
+ }
+
+ private fun powerMenuButtonTypeReverseConverter(pref: String): List {
+ if(pref.isEmpty()) return emptyList()
+ if(!pref.contains(",")) return listOf(PowerMenuButtonId.valueOf(pref.trim()))
+ return pref.split(",").map {
+ PowerMenuButtonId.valueOf(it.trim())
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/settings/RoomEncryptedSettingsRepository.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/settings/RoomEncryptedSettingsRepository.kt
new file mode 100644
index 0000000..b8cf091
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/components/settings/RoomEncryptedSettingsRepository.kt
@@ -0,0 +1,118 @@
+package com.kieronquinn.app.classicpowermenu.components.settings
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Base64
+import com.kieronquinn.app.classicpowermenu.BuildConfig
+import com.kieronquinn.app.classicpowermenu.components.settings.EncryptedSettingsImpl.Companion
+import com.kieronquinn.app.classicpowermenu.utils.extensions.createEncryptedSharedPrefDestructively
+import java.security.SecureRandom
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+/**
+ * Holds the encryption key and IV for the encrypted values in the Room database
+ */
+interface RoomEncryptedSettingsRepository {
+
+ /**
+ * The encryption key used to encrypt sensitive data stored in the Room database.
+ */
+ fun getDatabaseEncryptionKey(): SecretKey
+
+ /**
+ * The encryption IV used to encrypt sensitive data stored in the Room database.
+ */
+ fun getDatabaseEncryptionIV(): IvParameterSpec
+
+ interface RoomEncryptionFailedCallback {
+ fun onEncryptionFailed()
+ }
+
+}
+
+class RoomEncryptedSettingsRepositoryImpl(
+ context: Context,
+ //failedCallback: RoomEncryptionFailedCallback?
+): RoomEncryptedSettingsRepository {
+
+ companion object {
+
+ private const val KEY_ENCRYPTION_KEY = "encryption_key"
+ private const val KEY_ENCRYPTION_IV = "encryption_iv"
+
+ }
+
+ var encryptionKey by shared(KEY_ENCRYPTION_KEY, "")
+ var encryptionIV by shared(KEY_ENCRYPTION_IV, "")
+
+ private val sharedPreferences: SharedPreferences by lazy {
+ context.createEncryptedSharedPrefDestructively("room_encrypted_shared_prefs")
+ }
+
+ @Synchronized
+ override fun getDatabaseEncryptionKey(): SecretKey {
+ return loadEncryptionKey() ?: saveEncryptionKey()
+ }
+
+ @Synchronized
+ override fun getDatabaseEncryptionIV(): IvParameterSpec {
+ return loadEncryptionIV() ?: saveEncryptionIV()
+ }
+
+ private fun loadEncryptionKey(): SecretKey? {
+ val b64Key = encryptionKey
+ if(b64Key.isEmpty()) return null
+ val key = Base64.decode(b64Key, Base64.DEFAULT)
+ return SecretKeySpec(key, "AES")
+ }
+
+ private fun saveEncryptionKey(): SecretKey {
+ return KeyGenerator.getInstance("AES").apply {
+ init(256)
+ }.generateKey().also {
+ encryptionKey = (Base64.encodeToString(it.encoded, Base64.DEFAULT))
+ }
+ }
+
+ private fun loadEncryptionIV(): IvParameterSpec? {
+ val b64IV = encryptionIV
+ if(b64IV.isEmpty()) return null
+ val iv = Base64.decode(b64IV, Base64.DEFAULT)
+ return IvParameterSpec(iv)
+ }
+
+ private fun saveEncryptionIV(): IvParameterSpec {
+ val bytes = ByteArray(16)
+ SecureRandom().apply {
+ nextBytes(bytes)
+ }
+ return IvParameterSpec(bytes).also {
+ encryptionIV = (Base64.encodeToString(bytes, Base64.DEFAULT))
+ }
+ }
+
+ private fun shared(key: String, default: String) = ReadWriteProperty({
+ sharedPreferences.getString(key, default) ?: default
+ }, {
+ sharedPreferences.edit().putString(key, it).commit()
+ })
+
+ private inline fun ReadWriteProperty(crossinline getValue: () -> T, crossinline setValue: (T) -> Unit): ReadWriteProperty {
+ return object: ReadWriteProperty {
+
+ override operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
+ return getValue.invoke()
+ }
+
+ override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+ setValue.invoke(value)
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/LoyaltyCard.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/LoyaltyCard.kt
index 89fcb25..dab175a 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/LoyaltyCard.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/LoyaltyCard.kt
@@ -97,7 +97,7 @@ data class LoyaltyCard(
}
-fun LoyaltyCardProtos.LoyaltyCard.extract(valuableImage: Bitmap?, context: Context): LoyaltyCard {
+fun LoyaltyCardProtos.LoyaltyCard_.extract(valuableImage: Bitmap?, context: Context): LoyaltyCard {
val valuableId = this.id
val id = this.issuerInfo.id
val title = this.issuerInfo.title
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/WalletLoyaltyCardViewInfo.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/WalletLoyaltyCardViewInfo.kt
index d514b2b..48272ea 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/WalletLoyaltyCardViewInfo.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/WalletLoyaltyCardViewInfo.kt
@@ -55,9 +55,8 @@ class WalletLoyaltyCardViewInfo(private val context: Context, private val loyalt
}
override fun getPendingIntent(): PendingIntent {
- val intent = Intent().apply {
- component = GooglePayConstants.WALLET_DEEP_LINK_COMPONENT
- data = Uri.parse(String.format(GooglePayConstants.WALLET_DEEP_LINK_VALUABLE, cardId.substring(3)))
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ data = Uri.parse(String.format(GooglePayConstants.WALLET_DEEP_LINK_VALUABLE, loyaltyCard.valuableId))
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/database/WalletDatabase.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/database/WalletDatabase.kt
new file mode 100644
index 0000000..7513cae
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/database/WalletDatabase.kt
@@ -0,0 +1,29 @@
+package com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.database
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import com.kieronquinn.app.classicpowermenu.utils.room.EncryptedValueConverter
+import com.kieronquinn.app.classicpowermenu.utils.room.GsonSetConverter
+
+@Database(entities = [
+ //TargetData::class,
+ WalletValuable::class
+], version = 1, exportSchema = false)
+@TypeConverters(EncryptedValueConverter::class, GsonSetConverter::class)
+abstract class WalletDatabase: RoomDatabase() {
+
+ companion object {
+ fun getDatabase(context: Context): WalletDatabase {
+ return Room.databaseBuilder(
+ context,
+ WalletDatabase::class.java,
+ "wallet"
+ ).build()
+ }
+ }
+
+ abstract fun walletValuableDao(): WalletValuableDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/database/WalletValuable.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/database/WalletValuable.kt
new file mode 100644
index 0000000..adadbba
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/database/WalletValuable.kt
@@ -0,0 +1,19 @@
+package com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.database
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.kieronquinn.app.classicpowermenu.utils.room.EncryptedValue
+
+@Entity
+data class WalletValuable(
+ @PrimaryKey
+ @ColumnInfo(name = "id")
+ val id: String,
+ @ColumnInfo("hash")
+ val hash: Long,
+ @ColumnInfo("proto")
+ val valuable: EncryptedValue,
+ @ColumnInfo("image")
+ val image: EncryptedValue?
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/database/WalletValuableDao.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/database/WalletValuableDao.kt
new file mode 100644
index 0000000..40ef2f7
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/model/quickaccesswallet/database/WalletValuableDao.kt
@@ -0,0 +1,27 @@
+package com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.database
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface WalletValuableDao {
+
+ @Query("select * from `WalletValuable`")
+ fun getAll(): Flow>
+
+ @Query("select * from `WalletValuable` where id=:id")
+ fun getValuableById(id: String): Flow
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(valuable: WalletValuable)
+
+ @Query("delete from `WalletValuable` where id=:id")
+ fun delete(id: String)
+
+ @Query("delete from `WalletValuable`")
+ fun clear()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/main/PowerMenuFragment.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/main/PowerMenuFragment.kt
index 1e957db..5812f69 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/main/PowerMenuFragment.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/main/PowerMenuFragment.kt
@@ -6,6 +6,7 @@ import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
+import android.os.StrictMode
import android.provider.Settings
import android.service.quickaccesswallet.CPMQuickAccessWalletClientImpl
import android.util.Log
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/main/PowerMenuViewModel.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/main/PowerMenuViewModel.kt
index 845765b..36fbd44 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/main/PowerMenuViewModel.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/main/PowerMenuViewModel.kt
@@ -10,7 +10,7 @@ import com.android.systemui.plugin.globalactions.wallet.WalletCardViewInfo
import com.android.systemui.plugins.ActivityStarter
import com.kieronquinn.app.classicpowermenu.R
import com.kieronquinn.app.classicpowermenu.components.navigation.PowerMenuNavigation
-import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.LoyaltyCardsRepository
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.GoogleWalletRepository
import com.kieronquinn.app.classicpowermenu.components.settings.Settings
import com.kieronquinn.app.classicpowermenu.model.power.PowerMenuButton
import com.kieronquinn.app.classicpowermenu.model.power.PowerMenuButtonId
@@ -51,10 +51,9 @@ abstract class PowerMenuViewModel: ViewModel() {
abstract val useSolidBackground: Boolean
abstract val quickAccessWalletAutoSwitchService: Boolean
abstract val quickAccessWalletSelectedAutoSwitchService: String
-
}
-class PowerMenuViewModelImpl(context: Context, private val service: CPMServiceContainer, private val navigation: PowerMenuNavigation, private val settings: Settings, private val loyaltyCards: LoyaltyCardsRepository, private val activityStarter: ActivityStarter): PowerMenuViewModel() {
+class PowerMenuViewModelImpl(context: Context, private val service: CPMServiceContainer, private val navigation: PowerMenuNavigation, private val settings: Settings, private val googleWalletRepository: GoogleWalletRepository, private val activityStarter: ActivityStarter): PowerMenuViewModel() {
private val telecomManager by lazy {
context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
@@ -192,7 +191,7 @@ class PowerMenuViewModelImpl(context: Context, private val service: CPMServiceCo
override fun addLoyaltyCardsToWallet(list: ArrayList, callback: Runnable) {
viewModelScope.launch {
- val loyaltyCards = loyaltyCards.getLoyaltyCards(this@PowerMenuViewModelImpl::onCardClicked)?.run {
+ val loyaltyCards = googleWalletRepository.getLoyaltyCards(this@PowerMenuViewModelImpl::onCardClicked)?.run {
val hidden = settings.quickAccessWalletLoyaltyCardsHidden
val order = settings.quickAccessWalletLoyaltyCardsOrder
filterNot { hidden.contains(it.getLoyaltyIdOrNull()) }.sortedBy {
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/walletcode/WalletCodeDialogFragment.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/walletcode/WalletCodeDialogFragment.kt
index 7082b91..20ce7b1 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/walletcode/WalletCodeDialogFragment.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/powermenu/walletcode/WalletCodeDialogFragment.kt
@@ -151,14 +151,10 @@ class WalletCodeDialogFragment: BaseDialogFragment
abstract val autoSwitchServiceChanged: Flow
+ abstract val showLogoutOption: Boolean
abstract val isGooglePayInstalled: Boolean
@@ -32,10 +36,10 @@ abstract class SettingsQuickAccessWalletViewModel: ViewModel() {
abstract fun onChangeGooglePaySettingsClicked()
abstract fun onReorderLoyaltyCardsClicked()
abstract fun onAutoSwitchServiceClicked()
-
+ abstract fun onLogoutClicked()
}
-class SettingsQuickAccessWalletViewModelImpl(context: Context, private val settings: Settings, private val navigation: AppNavigation, private val containerNavigation: ContainerNavigation): SettingsQuickAccessWalletViewModel() {
+class SettingsQuickAccessWalletViewModelImpl(context: Context, private val settings: Settings, private val googleApiRepository: GoogleApiRepository, private val encryptedSettings: EncryptedSettings, private val valuablesDatabaseRepository: ValuablesDatabaseRepository, val navigation: AppNavigation, private val containerNavigation: ContainerNavigation): SettingsQuickAccessWalletViewModel() {
private val packageManager = context.packageManager
@@ -53,6 +57,10 @@ class SettingsQuickAccessWalletViewModelImpl(context: Context, private val setti
override val isGooglePayInstalled
get() = GooglePayConstants.isGooglePayInstalled(packageManager) || GooglePayConstants.isGPayInstalled(packageManager)
+ override val showLogoutOption
+ get() = googleApiRepository.isSignedIn()
+
+
override fun onSwitchClicked() {
settings.quickAccessWalletShow = !settings.quickAccessWalletShow
}
@@ -65,7 +73,7 @@ class SettingsQuickAccessWalletViewModelImpl(context: Context, private val setti
override fun onReorderLoyaltyCardsClicked() {
viewModelScope.launch {
- containerNavigation.navigate(SettingsContainerFragmentDirections.actionSettingsContainerFragmentToSettingsQuickAccessWalletRearrangeFragment())
+ containerNavigation.navigate(SettingsContainerFragmentDirections.actionSettingsContainerFragmentToSettingsQuickAccessWalletManageFragment())
}
}
@@ -75,6 +83,14 @@ class SettingsQuickAccessWalletViewModelImpl(context: Context, private val setti
}
}
+ override fun onLogoutClicked() {
+ encryptedSettings.walletToken = ""
+ encryptedSettings.aasToken = ""
+ viewModelScope.launch {
+ valuablesDatabaseRepository.deleteAllRecords()
+ }
+ }
+
private fun getGooglePayIntent(): Intent? {
return if(GooglePayConstants.isGPayInstalled(packageManager)){
packageManager.getLaunchIntentForPackage(GooglePayConstants.NBU_PAISA_PACKAGE_NAME)
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/loyaltycards/SiginInWithGoogleViewModel.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/loyaltycards/SiginInWithGoogleViewModel.kt
new file mode 100644
index 0000000..6043047
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/loyaltycards/SiginInWithGoogleViewModel.kt
@@ -0,0 +1,71 @@
+package com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.loyaltycards
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kieronquinn.app.classicpowermenu.components.navigation.ContainerNavigation
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.GoogleApiRepository
+import com.kieronquinn.app.classicpowermenu.components.settings.EncryptedSettings
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.launch
+
+abstract class SignInWithGoogleViewModel: ViewModel() {
+
+ abstract val state: StateFlow
+
+ abstract fun setCookie(cookie: String)
+ abstract fun close()
+
+ sealed class State {
+ object SignIn: State()
+ object Success: State()
+ object Error: State()
+ }
+
+}
+
+class SignInWithGoogleViewModelImpl(
+ private val navigation: ContainerNavigation,
+ googleApiRepository: GoogleApiRepository,
+ encryptedSettings: EncryptedSettings
+): SignInWithGoogleViewModel() {
+
+ private val cookie = MutableStateFlow(null)
+
+ private val _state = cookie.mapLatest {
+ if(it == null) return@mapLatest State.SignIn
+ val aasToken = googleApiRepository.getAasToken(it)
+ if(aasToken != null){
+ encryptedSettings.aasToken = aasToken
+ State.Success
+ }else{
+ State.Error
+ }
+ }
+
+ override val state = MutableStateFlow(State.SignIn)
+
+ override fun setCookie(cookie: String) {
+ viewModelScope.launch {
+ this@SignInWithGoogleViewModelImpl.cookie.emit(cookie)
+ }
+ }
+
+ override fun close() {
+ viewModelScope.launch {
+ navigation.navigateBack()
+ }
+ }
+
+ private fun setupState() = viewModelScope.launch {
+ _state.collect {
+ state.emit(it)
+ }
+ }
+
+ init {
+ setupState()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/loyaltycards/SignInWithGoogleFragment.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/loyaltycards/SignInWithGoogleFragment.kt
new file mode 100644
index 0000000..ae95b22
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/loyaltycards/SignInWithGoogleFragment.kt
@@ -0,0 +1,118 @@
+package com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.loyaltycards
+
+import android.graphics.Bitmap
+import android.os.Bundle
+import android.view.View
+import android.webkit.CookieManager
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.FrameLayout
+import android.widget.Toast
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
+import androidx.core.view.updateMargins
+import com.kieronquinn.app.classicpowermenu.databinding.FragmentSignInWithGoogleBinding
+import com.kieronquinn.app.classicpowermenu.ui.base.BackAvailable
+import com.kieronquinn.app.classicpowermenu.ui.base.BoundFragment
+import com.kieronquinn.app.classicpowermenu.utils.extensions.whenResumed
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.loyaltycards.SignInWithGoogleViewModel.State
+import com.kieronquinn.app.classicpowermenu.utils.extensions.getCookies
+import com.kieronquinn.app.classicpowermenu.utils.extensions.onApplyInsets
+
+class SignInWithGoogleFragment : BoundFragment(FragmentSignInWithGoogleBinding::inflate), BackAvailable{
+
+ companion object {
+ private const val URL_EMBEDDED_SIGN_IN = "https://accounts.google.com/EmbeddedSetup"
+ }
+
+ private val viewModel by viewModel()
+ private val cookieManager = CookieManager.getInstance()
+
+ private val webViewClient = object: WebViewClient() {
+ override fun onPageFinished(view: WebView, url: String) {
+ super.onPageFinished(view, url)
+ checkForCookie(url)
+ }
+
+ override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
+ super.onPageStarted(view, url, favicon)
+ checkForCookie(url)
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupWebView()
+ setupState()
+ setupInsets()
+ if(savedInstanceState == null){
+ cookieManager.removeAllCookies(null)
+ cookieManager.removeSessionCookies(null)
+ binding.signInWithGoogleWebview.loadUrl(URL_EMBEDDED_SIGN_IN)
+ }else{
+ binding.signInWithGoogleWebview.restoreState(savedInstanceState)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ binding.signInWithGoogleWebview.saveState(outState)
+ }
+
+ private fun setupWebView() = with(binding.signInWithGoogleWebview) {
+ cookieManager.acceptThirdPartyCookies(this)
+ settings.run {
+ javaScriptEnabled = true
+ }
+ webViewClient = this@SignInWithGoogleFragment.webViewClient
+ }
+
+ fun onBackPressed(): Boolean {
+ return if(binding.signInWithGoogleWebview.canGoBack()){
+ binding.signInWithGoogleWebview.goBack()
+ true
+ }else false
+ }
+
+ private fun checkForCookie(url: String) {
+ cookieManager.getCookies(url)["oauth_token"]?.let {
+ viewModel.setCookie(it)
+ }
+ }
+
+ private fun setupState() {
+ handleState(viewModel.state.value)
+ whenResumed {
+ viewModel.state.collect {
+ handleState(it)
+ }
+ }
+ }
+
+ private fun handleState(state: State) {
+ when(state){
+ is State.SignIn -> {
+ //No-op, don't break WebView state
+ }
+ is State.Success -> {
+ viewModel.close()
+ }
+ is State.Error -> {
+ Toast.makeText(
+ requireContext(), "R.string.sign_in_with_google_error", Toast.LENGTH_LONG
+ ).show()
+ viewModel.close()
+ }
+ }
+ }
+
+ private fun setupInsets() = with(binding.signInWithGoogleWebview) {
+ onApplyInsets { view, insets ->
+ view.updateLayoutParams {
+ updateMargins(bottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom)
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/rearrange/SettingsQuickAccessWalletRearrangeAdapter.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/manage/SettingsQuickAccessWalletManageAdapter.kt
similarity index 86%
rename from app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/rearrange/SettingsQuickAccessWalletRearrangeAdapter.kt
rename to app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/manage/SettingsQuickAccessWalletManageAdapter.kt
index ee79cda..3f52d75 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/rearrange/SettingsQuickAccessWalletRearrangeAdapter.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/manage/SettingsQuickAccessWalletManageAdapter.kt
@@ -1,4 +1,4 @@
-package com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.rearrange
+package com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.manage
import android.content.Context
import android.content.res.ColorStateList
@@ -9,7 +9,7 @@ import com.kieronquinn.app.classicpowermenu.R
import com.kieronquinn.app.classicpowermenu.databinding.ItemWalletRearrangeCardBinding
import com.kieronquinn.monetcompat.core.MonetCompat
-class SettingsQuickAccessWalletRearrangeAdapter(context: Context, var cards: List, val onCardVisibilityToggleClicked: (SettingsQuickAccessWalletRearrangeViewModel.RearrangeLoyaltyCard) -> Unit, val onHandleLongPress: (ViewHolder) -> Unit): RecyclerView.Adapter() {
+class SettingsQuickAccessWalletManageAdapter(context: Context, var cards: List, val onCardVisibilityToggleClicked: (SettingsQuickAccessWalletManageViewModel.RearrangeLoyaltyCard) -> Unit, val onHandleLongPress: (ViewHolder) -> Unit): RecyclerView.Adapter() {
init {
setHasStableIds(true)
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/manage/SettingsQuickAccessWalletManageFragment.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/manage/SettingsQuickAccessWalletManageFragment.kt
new file mode 100644
index 0000000..685350a
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/manage/SettingsQuickAccessWalletManageFragment.kt
@@ -0,0 +1,187 @@
+package com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.manage
+
+import android.content.res.ColorStateList
+import android.os.Bundle
+import android.view.View
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.isVisible
+import androidx.core.view.updatePadding
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.kieronquinn.app.classicpowermenu.R
+import com.kieronquinn.app.classicpowermenu.databinding.FragmentSettingsQuickAccessWalletManageBinding
+import com.kieronquinn.app.classicpowermenu.ui.base.BoundFragment
+import com.kieronquinn.app.classicpowermenu.ui.base.StandaloneFragment
+import com.kieronquinn.app.classicpowermenu.utils.extensions.isDarkMode
+import com.kieronquinn.app.classicpowermenu.utils.extensions.onApplyInsets
+import com.kieronquinn.app.classicpowermenu.utils.extensions.whenResumed
+import com.kieronquinn.monetcompat.extensions.views.applyMonet
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import java.util.*
+
+class SettingsQuickAccessWalletManageFragment: BoundFragment(FragmentSettingsQuickAccessWalletManageBinding::inflate), StandaloneFragment {
+
+ private val adapter by lazy {
+ SettingsQuickAccessWalletManageAdapter(requireContext(), emptyList(), viewModel::onCardVisibilityClicked){
+ itemTouchHelper.startDrag(it)
+ }
+ }
+
+ private val itemTouchHelper by lazy {
+ ItemTouchHelper(ItemTouchCallback())
+ }
+
+ private val viewModel by viewModel()
+
+ override val onBackPressed by lazy {
+ return@lazy viewModel::onBackPressed
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupToolbar()
+ setupRecyclerView()
+ setupState()
+ setupMonet()
+ setupInsets()
+ setUpClickListeners()
+ viewModel.onPageLoad()
+ }
+
+ private fun setUpClickListeners() {
+ binding.signInButton.setOnClickListener {
+ viewModel.onSignInClicked()
+ }
+
+ binding.syncButton.setOnClickListener {
+ viewModel.syncLoyaltyCards()
+ }
+ }
+
+ private fun setupMonet(){
+ binding.root.setBackgroundColor(monet.getBackgroundColor(requireContext()))
+ binding.loadingBar.applyMonet()
+ binding.syncButton.applyMonet()
+ binding.signInButton.applyMonet()
+ binding.signInButton.setTextColor(monet.getPrimaryColor(requireContext()))
+ binding.syncButton.drawable.setTint(monet.getPrimaryColor(requireContext()))
+ binding.signInInfoCard.run {
+ val background = monet.getPrimaryColor(context, !context.isDarkMode)
+ backgroundTintList = ColorStateList.valueOf(background)
+ }
+ }
+
+ private fun setupState() = whenResumed {
+ viewModel.state.collect {
+ handleState(it)
+ }
+ }
+
+ private fun handleState(state: SettingsQuickAccessWalletManageViewModel.State){
+ when(state){
+ is SettingsQuickAccessWalletManageViewModel.State.Loading -> {
+ binding.recyclerview.isVisible = false
+ binding.error.isVisible = false
+ binding.signIn.isVisible = false
+ binding.syncButtonContainer.isVisible = false
+
+ binding.loading.isVisible = true
+ }
+ is SettingsQuickAccessWalletManageViewModel.State.SignInRequired -> {
+ binding.recyclerview.isVisible = false
+ binding.error.isVisible = false
+ binding.loading.isVisible = false
+ binding.syncButtonContainer.isVisible = false
+
+ binding.signIn.isVisible = true
+ }
+ is SettingsQuickAccessWalletManageViewModel.State.Error -> {
+ binding.recyclerview.isVisible = false
+ binding.loading.isVisible = false
+ binding.signIn.isVisible = false
+
+ binding.error.isVisible = true
+ binding.errorText.setText(state.type.contentRes)
+
+ when (state.type) {
+ SettingsQuickAccessWalletManageViewModel.ErrorType.ERROR -> binding.syncButtonContainer.isVisible = false
+ SettingsQuickAccessWalletManageViewModel.ErrorType.NO_CARDS -> binding.syncButtonContainer.isVisible = true
+ }
+ }
+ is SettingsQuickAccessWalletManageViewModel.State.Loaded -> {
+ binding.error.isVisible = false
+ binding.loading.isVisible = false
+ binding.signIn.isVisible = false
+
+ binding.recyclerview.isVisible = true
+ binding.syncButtonContainer.isVisible = true
+ adapter.cards = state.cards
+
+ binding.syncButtonContainer.bringToFront()
+ }
+ }
+ }
+
+ private fun setupToolbar(){
+ with(binding.toolbar){
+ setupWithScrollableView(binding.recyclerview)
+ setNavigationOnClickListener {
+ viewModel.onBackPressed()
+ }
+ }
+ }
+
+ private fun setupRecyclerView() = with(binding.recyclerview) {
+ layoutManager = LinearLayoutManager(context)
+ adapter = this@SettingsQuickAccessWalletManageFragment.adapter
+ itemTouchHelper.attachToRecyclerView(this)
+ }
+
+ private fun setupInsets(){
+ binding.toolbar.onApplyInsets { view, insets ->
+ val topInset = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
+ view.updatePadding(top = topInset)
+ }
+ binding.signIn.onApplyInsets { view, insets ->
+ val topInset = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
+ view.updatePadding(top = topInset)
+ }
+ binding.recyclerview.onApplyInsets { view, insets ->
+ val bottomPadding = resources.getDimension(R.dimen.margin_16).toInt()
+ val bottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
+ view.updatePadding(bottom = bottomPadding + bottomInset)
+ }
+ binding.syncButtonContainer.onApplyInsets { view, insets ->
+ val bottomPadding = resources.getDimension(R.dimen.margin_16).toInt()
+ val bottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
+ view.updatePadding(bottom = bottomPadding + bottomInset)
+ }
+ }
+
+ private inner class ItemTouchCallback: ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
+
+ override fun onMove(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder
+ ): Boolean {
+ val fromPosition = viewHolder.bindingAdapterPosition
+ val toPosition = target.bindingAdapterPosition
+ Collections.swap(adapter.cards, fromPosition, toPosition)
+ adapter.notifyItemMoved(fromPosition, toPosition)
+ adapter.notifyItemChanged(fromPosition)
+ adapter.notifyItemChanged(toPosition)
+ viewModel.onCardOrderChanged(adapter.cards)
+ return true
+ }
+
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
+ //no-op
+ }
+
+ override fun isLongPressDragEnabled() = false
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/manage/SettingsQuickAccessWalletManageViewModel.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/manage/SettingsQuickAccessWalletManageViewModel.kt
new file mode 100644
index 0000000..7d3d70f
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/manage/SettingsQuickAccessWalletManageViewModel.kt
@@ -0,0 +1,112 @@
+package com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.manage
+
+import android.util.Log
+import androidx.annotation.StringRes
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kieronquinn.app.classicpowermenu.R
+import com.kieronquinn.app.classicpowermenu.components.navigation.ContainerNavigation
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.GoogleApiRepository
+import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.GoogleWalletRepository
+import com.kieronquinn.app.classicpowermenu.components.settings.Settings
+import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.WalletLoyaltyCardViewInfo
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+abstract class SettingsQuickAccessWalletManageViewModel: ViewModel() {
+
+ abstract val state: MutableStateFlow
+
+ abstract fun onCardVisibilityClicked(card: RearrangeLoyaltyCard)
+ abstract fun onCardOrderChanged(cards: List)
+ abstract fun onBackPressed()
+ abstract fun onSignInClicked()
+ abstract fun onPageLoad()
+ abstract fun syncLoyaltyCards()
+
+ sealed class State {
+ object Loading: State()
+ object SignInRequired: State()
+ data class Loaded(val cards: List): State()
+ data class Error(val type: ErrorType): State()
+ }
+
+ data class RearrangeLoyaltyCard(var visible: Boolean, val info: WalletLoyaltyCardViewInfo) {
+ val id = info.id
+ }
+
+ enum class ErrorType(@StringRes val contentRes: Int) {
+ ERROR(R.string.settings_quick_access_wallet_manage_error_generic), NO_CARDS(R.string.settings_quick_access_wallet_manage_error_no_cards)
+ }
+}
+
+class SettingsQuickAccessWalletManageViewModelImpl(private val walletRepository: GoogleWalletRepository, private val googleApiRepository: GoogleApiRepository, private val settings: Settings, private val containerNavigation: ContainerNavigation): SettingsQuickAccessWalletManageViewModel() {
+
+ override val state = MutableStateFlow(State.Loading)
+
+ override fun syncLoyaltyCards() {
+ viewModelScope.launch {
+ state.emit(State.Loading)
+ walletRepository.syncValuables()
+ updateLoyaltyCards()
+ }
+ }
+
+ override fun onPageLoad() {
+ viewModelScope.launch {
+ if (!googleApiRepository.isSignedIn()) return@launch state.emit(State.SignInRequired)
+ updateLoyaltyCards()
+ }
+ }
+
+ private suspend fun updateLoyaltyCards() {
+ val cards = walletRepository.getLoyaltyCards({ false }, true)?.map {
+ it as WalletLoyaltyCardViewInfo
+ }
+
+ if (cards == null) return state.emit(State.Error(ErrorType.ERROR))
+ if (cards.isEmpty()) return state.emit(State.Error(ErrorType.NO_CARDS))
+
+ val order = settings.quickAccessWalletLoyaltyCardsOrder
+ val hidden = settings.quickAccessWalletLoyaltyCardsHidden
+ val ordered = cards.sortedBy {
+ order.indexOf(it.id)
+ }.map {
+ RearrangeLoyaltyCard(!hidden.contains(it.id), it)
+ }
+ state.emit(State.Loaded(ordered))
+ }
+
+ override fun onCardVisibilityClicked(card: RearrangeLoyaltyCard) {
+ viewModelScope.launch {
+ if(card.visible){
+ settings.quickAccessWalletLoyaltyCardsHidden = settings.quickAccessWalletLoyaltyCardsHidden.toMutableSet().apply {
+ remove(card.id)
+ }.toList()
+ }else{
+ settings.quickAccessWalletLoyaltyCardsHidden = settings.quickAccessWalletLoyaltyCardsHidden.toMutableSet().apply {
+ add(card.id)
+ }.toList()
+ }
+ }
+ }
+
+ override fun onCardOrderChanged(cards: List) {
+ viewModelScope.launch {
+ settings.quickAccessWalletLoyaltyCardsOrder = cards.map { it.id }
+ }
+ }
+
+ override fun onSignInClicked() {
+ viewModelScope.launch {
+ containerNavigation.navigate(SettingsQuickAccessWalletManageFragmentDirections.actionSettingsQuickAccessWalletManageFragmentToSettingsQuickAccessWalletSignInToGoogle())
+ }
+ }
+
+ override fun onBackPressed() {
+ viewModelScope.launch {
+ containerNavigation.navigateBack()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/rearrange/SettingsQuickAccessWalletRearrangeFragment.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/rearrange/SettingsQuickAccessWalletRearrangeFragment.kt
deleted file mode 100644
index e9c5dc7..0000000
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/rearrange/SettingsQuickAccessWalletRearrangeFragment.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-package com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.rearrange
-
-import android.os.Bundle
-import android.view.View
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.isVisible
-import androidx.core.view.updatePadding
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.kieronquinn.app.classicpowermenu.R
-import com.kieronquinn.app.classicpowermenu.databinding.FragmentSettingsQuickAccessWalletRearrangeBinding
-import com.kieronquinn.app.classicpowermenu.ui.base.BoundFragment
-import com.kieronquinn.app.classicpowermenu.ui.base.StandaloneFragment
-import com.kieronquinn.app.classicpowermenu.utils.extensions.onApplyInsets
-import com.kieronquinn.app.classicpowermenu.utils.extensions.whenResumed
-import com.kieronquinn.monetcompat.extensions.views.applyMonet
-import org.koin.androidx.viewmodel.ext.android.viewModel
-import java.util.*
-
-class SettingsQuickAccessWalletRearrangeFragment: BoundFragment(FragmentSettingsQuickAccessWalletRearrangeBinding::inflate), StandaloneFragment {
-
- private val adapter by lazy {
- SettingsQuickAccessWalletRearrangeAdapter(requireContext(), emptyList(), viewModel::onCardVisibilityClicked){
- itemTouchHelper.startDrag(it)
- }
- }
-
- private val itemTouchHelper by lazy {
- ItemTouchHelper(ItemTouchCallback())
- }
-
- private val viewModel by viewModel()
-
- override val onBackPressed by lazy {
- return@lazy viewModel::onBackPressed
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- setupToolbar()
- setupRecyclerView()
- setupState()
- setupMonet()
- setupInsets()
- }
-
- private fun setupMonet(){
- binding.root.setBackgroundColor(monet.getBackgroundColor(requireContext()))
- binding.quickAccessWalletRearrangeLoadingBar.applyMonet()
- }
-
- private fun setupState() = whenResumed {
- viewModel.state.collect {
- handleState(it)
- }
- }
-
- private fun handleState(state: SettingsQuickAccessWalletRearrangeViewModel.State){
- when(state){
- is SettingsQuickAccessWalletRearrangeViewModel.State.Loading -> {
- binding.quickAccessWalletRearrangeRecyclerview.isVisible = false
- binding.quickAccessWalletRearrangeEmpty.isVisible = false
- binding.quickAccessWalletRearrangeLoading.isVisible = true
- }
- is SettingsQuickAccessWalletRearrangeViewModel.State.Error -> {
- binding.quickAccessWalletRearrangeRecyclerview.isVisible = false
- binding.quickAccessWalletRearrangeEmpty.isVisible = true
- binding.quickAccessWalletRearrangeEmptyText.setText(state.type.contentRes)
- binding.quickAccessWalletRearrangeLoading.isVisible = false
- }
- is SettingsQuickAccessWalletRearrangeViewModel.State.Loaded -> {
- binding.quickAccessWalletRearrangeRecyclerview.isVisible = true
- binding.quickAccessWalletRearrangeEmpty.isVisible = false
- binding.quickAccessWalletRearrangeLoading.isVisible = false
- adapter.cards = state.cards
- }
- }
- }
-
- private fun setupToolbar(){
- with(binding.toolbar){
- setupWithScrollableView(binding.quickAccessWalletRearrangeRecyclerview)
- setNavigationOnClickListener {
- viewModel.onBackPressed()
- }
- }
- }
-
- private fun setupRecyclerView() = with(binding.quickAccessWalletRearrangeRecyclerview) {
- layoutManager = LinearLayoutManager(context)
- adapter = this@SettingsQuickAccessWalletRearrangeFragment.adapter
- itemTouchHelper.attachToRecyclerView(this)
- }
-
- private fun setupInsets(){
- binding.toolbar.onApplyInsets { view, insets ->
- val topInset = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
- view.updatePadding(top = topInset)
- }
- binding.quickAccessWalletRearrangeRecyclerview.onApplyInsets { view, insets ->
- val bottomPadding = resources.getDimension(R.dimen.margin_16).toInt()
- val bottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
- view.updatePadding(bottom = bottomPadding + bottomInset)
- }
- }
-
- private inner class ItemTouchCallback: ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
-
- override fun onMove(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- target: RecyclerView.ViewHolder
- ): Boolean {
- val fromPosition = viewHolder.bindingAdapterPosition
- val toPosition = target.bindingAdapterPosition
- Collections.swap(adapter.cards, fromPosition, toPosition)
- adapter.notifyItemMoved(fromPosition, toPosition)
- viewModel.onCardOrderChanged(adapter.cards)
- return true
- }
-
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
- //no-op
- }
-
- override fun isLongPressDragEnabled() = false
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/rearrange/SettingsQuickAccessWalletRearrangeViewModel.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/rearrange/SettingsQuickAccessWalletRearrangeViewModel.kt
deleted file mode 100644
index 6b12cfd..0000000
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/ui/screens/settings/quickaccesswallet/rearrange/SettingsQuickAccessWalletRearrangeViewModel.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.rearrange
-
-import androidx.annotation.StringRes
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.kieronquinn.app.classicpowermenu.R
-import com.kieronquinn.app.classicpowermenu.components.navigation.ContainerNavigation
-import com.kieronquinn.app.classicpowermenu.components.quickaccesswallet.loyaltycards.LoyaltyCardsRepository
-import com.kieronquinn.app.classicpowermenu.components.settings.Settings
-import com.kieronquinn.app.classicpowermenu.model.quickaccesswallet.WalletLoyaltyCardViewInfo
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.launch
-
-abstract class SettingsQuickAccessWalletRearrangeViewModel: ViewModel() {
-
- abstract val state: Flow
-
- abstract fun onCardVisibilityClicked(card: RearrangeLoyaltyCard)
- abstract fun onCardOrderChanged(cards: List)
- abstract fun onBackPressed()
-
- sealed class State {
- object Loading: State()
- data class Loaded(val cards: List): State()
- data class Error(val type: ErrorType): State()
- }
-
- data class RearrangeLoyaltyCard(var visible: Boolean, val info: WalletLoyaltyCardViewInfo) {
- val id = info.id
- }
-
- enum class ErrorType(@StringRes val contentRes: Int) {
- ERROR(R.string.settings_quick_access_wallet_rearrange_error_generic), NO_CARDS(R.string.settings_quick_access_wallet_rearrange_error_no_cards)
- }
-
-}
-
-class SettingsQuickAccessWalletRearrangeViewModelImpl(private val loyaltyCardsRepository: LoyaltyCardsRepository, private val settings: Settings, private val containerNavigation: ContainerNavigation): SettingsQuickAccessWalletRearrangeViewModel() {
-
- private val loyaltyCards = flow {
- val cards = loyaltyCardsRepository.getLoyaltyCards({ false }, true)
- emit(cards?.map { it as WalletLoyaltyCardViewInfo })
- }
-
- private val _state = combine(loyaltyCards, settings.quickAccessWalletLoyaltyCardsHiddenFlow){ cards: List?, hiddenIds: List ->
- when {
- cards == null -> {
- State.Error(ErrorType.ERROR)
- }
- cards.isEmpty() -> {
- State.Error(ErrorType.NO_CARDS)
- }
- else -> {
- val order = settings.quickAccessWalletLoyaltyCardsOrder
- State.Loaded(cards.sortedBy { order.indexOf(it.id) }.map { RearrangeLoyaltyCard(!hiddenIds.contains(it.id), it) })
- }
- }
- }
-
- override val state = _state.onStart { emit(State.Loading) }
-
- override fun onCardVisibilityClicked(card: RearrangeLoyaltyCard) {
- viewModelScope.launch {
- if(card.visible){
- settings.quickAccessWalletLoyaltyCardsHidden = settings.quickAccessWalletLoyaltyCardsHidden.toMutableList().apply {
- remove(card.id)
- }
- }else{
- settings.quickAccessWalletLoyaltyCardsHidden = settings.quickAccessWalletLoyaltyCardsHidden.toMutableList().apply {
- add(card.id)
- }
- }
- }
- }
-
- override fun onCardOrderChanged(cards: List) {
- viewModelScope.launch {
- settings.quickAccessWalletLoyaltyCardsOrder = cards.map { it.id }
- }
- }
-
- override fun onBackPressed() {
- viewModelScope.launch {
- containerNavigation.navigateBack()
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/Result.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/Result.kt
new file mode 100644
index 0000000..4a71a44
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/Result.kt
@@ -0,0 +1,25 @@
+package com.kieronquinn.app.classicpowermenu.utils
+
+
+sealed class Result(val time: Long = System.currentTimeMillis()) {
+
+ data class Failed(val code: Int = 999, val cached: T? = null): Result()
+ data class Success(val data: T): Result()
+
+ fun unwrap(): T? {
+ return if(this is Success) data else null
+ }
+
+ /**
+ * Converts type of wrapped data, this is only designed to work with [Failed], but if the
+ * data is castable from [T] to [O], then [Success] will also work. Alternatively, passing
+ * a [convert] block will allow type conversion.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun mutate(convert: ((T) -> O) = { it as O }): Result {
+ return when(this){
+ is Success -> Success(convert(data))
+ is Failed -> Failed(code)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Bitmap.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Bitmap.kt
index ca1468b..210d289 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Bitmap.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Bitmap.kt
@@ -1,6 +1,37 @@
package com.kieronquinn.app.classicpowermenu.utils.extensions
import android.graphics.*
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+
+/**
+ * Compresses the bitmap to a byte array for serialization.
+ */
+fun Bitmap.compress(recycleBitmap: Boolean = false): ByteArray? {
+ val out = ByteArrayOutputStream(getExpectedBitmapSize())
+ return try {
+ compress(Bitmap.CompressFormat.PNG, 100, out)
+ out.flush()
+ out.close()
+ out.toByteArray().also {
+ if(recycleBitmap) recycle()
+ }
+ } catch (e: IOException) {
+ null
+ }
+}
+
+fun ByteArray.toBitmap(): Bitmap? {
+ return BitmapFactory.decodeByteArray(this, 0, size)
+}
+
+/**
+ * Try go guesstimate how much space the icon will take when serialized to avoid unnecessary
+ * allocations/copies during the write (4 bytes per pixel).
+ */
+private fun Bitmap.getExpectedBitmapSize(): Int {
+ return width * height * 4
+}
/**
* Convert square bitmap to circle
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Color.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Color.kt
index 45afa5f..b7c6799 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Color.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Color.kt
@@ -1,5 +1,16 @@
package com.kieronquinn.app.classicpowermenu.utils.extensions
+import android.graphics.Color
+
fun Int.toHexString(): String {
return "#" + Integer.toHexString(this)
+}
+
+
+fun com.google.type.Color.toColor(): Int {
+ return if(hasAlpha()){
+ Color.valueOf(red, green, blue, alpha.value)
+ }else{
+ Color.valueOf(red, green, blue)
+ }.toArgb()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Coroutines.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Coroutines.kt
new file mode 100644
index 0000000..5cc9f0e
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+Coroutines.kt
@@ -0,0 +1,34 @@
+package com.kieronquinn.app.classicpowermenu.utils.extensions
+
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withTimeoutOrNull
+import kotlin.coroutines.Continuation
+
+suspend inline fun suspendCoroutineWithTimeout(timeout: Long, crossinline block: (Continuation) -> Unit ) : T? {
+ var finalValue : T? = null
+ withTimeoutOrNull(timeout) {
+ finalValue = suspendCancellableCoroutine(block = block)
+ }
+ return finalValue
+}
+
+suspend inline fun suspendCancellableCoroutineWithTimeout(timeout: Long, crossinline block: (CancellableContinuation) -> Unit ) : T? {
+ var finalValue : T? = null
+ withTimeoutOrNull(timeout) {
+ finalValue = suspendCancellableCoroutine(block = block)
+ }
+ return finalValue
+}
+
+fun CoroutineScope.launch(lock: Mutex, block: suspend () -> T) {
+ launch {
+ lock.withLock {
+ block()
+ }
+ }
+}
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+EncryptedSharedPreferences.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+EncryptedSharedPreferences.kt
new file mode 100644
index 0000000..9a78d3d
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+EncryptedSharedPreferences.kt
@@ -0,0 +1,57 @@
+package com.kieronquinn.app.classicpowermenu.utils.extensions
+
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Build
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKey
+import java.security.GeneralSecurityException
+import java.security.KeyStore
+
+fun Context.createEncryptedSharedPrefDestructively(
+ fileName: String, onDelete: () -> Unit = {}
+): SharedPreferences {
+ return try {
+ createEncryptedSharedPrefs(fileName)
+ } catch (e: GeneralSecurityException) {
+ deleteMasterKeyEntry()
+ deleteExistingPref(fileName)
+ onDelete()
+ createEncryptedSharedPrefs(fileName)
+ }
+}
+
+@SuppressLint("ApplySharedPref")
+private fun Context.deleteExistingPref(fileName: String) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ deleteSharedPreferences(fileName)
+ } else {
+ getSharedPreferences(fileName, Context.MODE_PRIVATE)
+ .edit()
+ .clear()
+ .commit()
+ }
+}
+
+private fun deleteMasterKeyEntry() {
+ KeyStore.getInstance("AndroidKeyStore").apply {
+ load(null)
+ deleteEntry(MasterKey.DEFAULT_MASTER_KEY_ALIAS)
+ }
+}
+
+private fun Context.createEncryptedSharedPrefs(fileName: String): SharedPreferences {
+ val masterKey = MasterKey.Builder(this, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build()
+
+ return EncryptedSharedPreferences.create(
+ this,
+ fileName,
+ masterKey,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+PackageManager.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+PackageManager.kt
index efbe1b9..c1fe345 100644
--- a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+PackageManager.kt
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extensions+PackageManager.kt
@@ -1,6 +1,8 @@
package com.kieronquinn.app.classicpowermenu.utils.extensions
+import android.content.pm.PackageInfo
import android.content.pm.PackageManager
+import android.os.Build
fun PackageManager.isAppInstalled(packageName: String): Boolean {
return try {
@@ -9,4 +11,13 @@ fun PackageManager.isAppInstalled(packageName: String): Boolean {
}catch (e: PackageManager.NameNotFoundException){
false
}
+}
+
+@Suppress("DEPRECATION")
+fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo {
+ return if (Build.VERSION.SDK_INT >= 33) {
+ getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
+ } else {
+ getPackageInfo(packageName, flags)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extentions+Retrofit.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extentions+Retrofit.kt
new file mode 100644
index 0000000..c6f54b6
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extentions+Retrofit.kt
@@ -0,0 +1,50 @@
+package com.kieronquinn.app.classicpowermenu.utils.extensions
+
+
+import android.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import okhttp3.MediaType
+import okhttp3.RequestBody
+import okio.BufferedSink
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import kotlin.coroutines.resume
+
+const val CONTENT_TYPE_PROTOBUF = "application/x-protobuf"
+
+suspend fun Call.invoke(): T? = withContext(Dispatchers.IO) {
+ return@withContext getSuspended()
+}
+
+private suspend fun Call.getSuspended() = suspendCancellableCoroutine {
+ enqueue(object: Callback{
+ override fun onResponse(call: Call, response: Response) {
+ it.resume(response.body())
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ Log.d("RetrofitError", "Failed to ${call.request().method()} ${call.request().url()}", t)
+ it.resume(null)
+ }
+ })
+ it.invokeOnCancellation {
+ if(!this.isCanceled){
+ cancel()
+ }
+ }
+}
+
+fun ByteArray.toRequestBody(contentType: String): RequestBody {
+ return object: RequestBody() {
+ override fun contentType(): MediaType? {
+ return MediaType.parse(contentType)
+ }
+
+ override fun writeTo(sink: BufferedSink) {
+ sink.write(this@toRequestBody)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extentions+WebView.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extentions+WebView.kt
new file mode 100644
index 0000000..172a99f
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/extensions/Extentions+WebView.kt
@@ -0,0 +1,53 @@
+package com.kieronquinn.app.classicpowermenu.utils.extensions
+
+
+import android.webkit.CookieManager
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+private object WebViewClient: WebViewClient()
+
+fun CookieManager.getCookies(url: String): Map {
+ val cookieString = getCookie(url)
+ return when {
+ cookieString.isNullOrBlank() -> emptyList()
+ !cookieString.contains(";") -> listOf(cookieString.trim())
+ else -> {
+ cookieString.split(";")
+ }
+ }.associate {
+ it.parseCookie()
+ }
+}
+
+fun String.parseCookie(): Pair {
+ val split = indexOf("=")
+ val name = substring(0, split).trim()
+ val value = substring(split + 1, length)
+ return Pair(name, value)
+}
+
+suspend fun WebView.getHtml(): String = suspendCoroutine {
+ evaluateJavascript("(function(){return window.document.body.outerHTML})();") { html ->
+ it.resume(html)
+ }
+}
+
+suspend fun WebView.load(
+ url: String,
+ timeout: Long = 10_000L
+): Boolean = suspendCancellableCoroutineWithTimeout(timeout) {
+ webViewClient = object: WebViewClient() {
+ override fun onPageFinished(view: WebView, url: String) {
+ super.onPageFinished(view, url)
+ webViewClient = WebViewClient
+ it.resume(true)
+ }
+ }
+ loadUrl(url)
+ it.invokeOnCancellation {
+ stopLoading()
+ }
+} ?: false
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/room/EncryptedValue.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/room/EncryptedValue.kt
new file mode 100644
index 0000000..eb0d7ff
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/room/EncryptedValue.kt
@@ -0,0 +1,63 @@
+package com.kieronquinn.app.classicpowermenu.utils.room
+
+import android.os.Parcelable
+import com.kieronquinn.app.classicpowermenu.components.settings.RoomEncryptedSettingsRepository
+import kotlinx.parcelize.Parcelize
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import javax.crypto.Cipher
+import androidx.room.TypeConverter
+
+
+/**
+ * ByteArray which is stored in the Room database in encrypted form, and encrypted/decrypted based
+ * on a key and IV stored in the encrypted shared preferences. Automatically creates and stores
+ * a key and IV on first use.
+ */
+object EncryptedValueConverter: KoinComponent {
+
+ private const val ENCRYPTION_TRANSFORMATION = "AES/CBC/PKCS5PADDING"
+
+ private val encryptedSettings by inject()
+ private val key = encryptedSettings.getDatabaseEncryptionKey()
+ private val iv = encryptedSettings.getDatabaseEncryptionIV()
+
+ private val encryptionCipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION).apply {
+ init(Cipher.ENCRYPT_MODE, key, EncryptedValueConverter.iv)
+ }
+
+ private val decryptionCipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION).apply {
+ init(Cipher.DECRYPT_MODE, key, EncryptedValueConverter.iv)
+ }
+
+ @Synchronized
+ @TypeConverter
+ fun fromBytes(value: ByteArray?): EncryptedValue? {
+ return value?.let { EncryptedValue(decryptionCipher.doFinal(it)) }
+ }
+
+ @Synchronized
+ @TypeConverter
+ fun fromEncryptedValue(value: EncryptedValue?): ByteArray? {
+ return value?.bytes?.let { encryptionCipher.doFinal(it) }
+ }
+
+}
+
+@Parcelize
+data class EncryptedValue(val bytes: ByteArray): Parcelable {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as EncryptedValue
+
+ if (!bytes.contentEquals(other.bytes)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return bytes.contentHashCode()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/room/GsonConverter.kt b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/room/GsonConverter.kt
new file mode 100644
index 0000000..50d48b7
--- /dev/null
+++ b/app/src/main/java/com/kieronquinn/app/classicpowermenu/utils/room/GsonConverter.kt
@@ -0,0 +1,34 @@
+package com.kieronquinn.app.classicpowermenu.utils.room
+
+
+import androidx.room.TypeConverter
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+object GsonSetConverter: GsonConverter>(typeToken())
+
+abstract class GsonConverter(type: TypeToken): KoinComponent {
+
+ companion object {
+ @JvmStatic
+ protected inline fun typeToken(): TypeToken {
+ return object: TypeToken(){}
+ }
+ }
+
+ private val gson by inject()
+ private val typeToken = type.type
+
+ @TypeConverter
+ fun fromString(value: String): T {
+ return gson.fromJson(value, typeToken)
+ }
+
+ @TypeConverter
+ fun fromObject(obj: T): String {
+ return gson.toJson(obj)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/android/libraries/commerce/hce/ndef/Format.proto b/app/src/main/proto/com/google/android/libraries/commerce/hce/ndef/Format.proto
new file mode 100644
index 0000000..6fe18a5
--- /dev/null
+++ b/app/src/main/proto/com/google/android/libraries/commerce/hce/ndef/Format.proto
@@ -0,0 +1,15 @@
+syntax = "proto3";
+
+package com.google.android.libraries.commerce.hce.ndef;
+
+option java_package = "com.google.android.libraries.commerce.hce.ndef";
+option java_outer_classname = "FormatProto";
+
+enum Format {
+ FORMAT_UNSPECIFIED = 0;
+ FORMAT_ASCII = 1;
+ FORMAT_UTF_8 = 2;
+ FORMAT_UTF_16 = 3;
+ FORMAT_BINARY = 4;
+ FORMAT_BCD = 5;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/Common.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/Common.proto
new file mode 100644
index 0000000..44ea99c
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/Common.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1;
+
+import "google/protobuf/duration.proto";
+import "google/type/date.proto";
+import "google/type/timeofday.proto";
+
+option java_package = "com.google.internal.tapandpay.v1";
+option java_outer_classname = "CommonProto";
+
+message Money {
+ int64 micros = 1;
+ string currency_code = 2;
+}
+
+message DateTime {
+ .google.type.Date local_date = 1;
+ .google.type.TimeOfDay local_time = 2;
+ .google.protobuf.Duration offset_from_utc = 3;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/ListTokenizedCardsRequest.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/ListTokenizedCardsRequest.proto
new file mode 100644
index 0000000..69b9f76
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/ListTokenizedCardsRequest.proto
@@ -0,0 +1,30 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1;
+
+option java_package = "com.google.internal.tapandpay.v1";
+option java_outer_classname = "ListTokenizedCardsRequestProto";
+
+message ListTokenizedCardsRequest {
+ Inner inner = 1;
+
+ message Inner {
+ int64 header = 1;
+ PackageVersion wallet_version = 2;
+ PackageVersion gms_version = 3;
+ bytes data = 7;
+ repeated Token tokens = 9;
+
+ message PackageVersion {
+ string package_name = 1;
+ string version_code = 2;
+ string version = 3;
+ bool caller = 4;
+ }
+
+ message Token {
+ string package_name = 1;
+ string token = 2;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/notifications/ScheduledNotificationProto.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/notifications/ScheduledNotificationProto.proto
new file mode 100644
index 0000000..0b3a465
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/notifications/ScheduledNotificationProto.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.notifications;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "google/protobuf/duration.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.notifications";
+option java_outer_classname = "ScheduledNotificationProto";
+
+message ScheduledNotification {
+ string title = 2;
+ string message = 3;
+ string opt_out_label = 4;
+ string issuer_logo_url = 5;
+ DateTime date_time_to_show = 7;
+ .google.protobuf.Duration timeout_after = 11;
+ string id = 12;
+ bool display_after_time_to_show = 13;
+ int32 priority = 14;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Card.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Card.proto
new file mode 100644
index 0000000..0c7e787
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Card.proto
@@ -0,0 +1,67 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.passes.templates;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/ColorProfile.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Logo.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Reference.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/TransitMode.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.passes.templates";
+option java_outer_classname = "CardProto";
+
+message PassCardHeaderTemplateInfo {
+ PassLogo logo = 1;
+ ReferenceValue title_label = 2;
+ ReferenceValue title = 3;
+ ReferenceValue subtitle = 4;
+}
+
+message PassCardRowTemplateInfo {
+ oneof Row {
+ PassCardRowItems items = 2;
+ PassCardRowTransit transit = 3;
+ PassCardRowDivider divider = 4;
+ PassCardRowSpace space = 5;
+ }
+}
+
+message PassCardRowItems {
+ repeated PassCardRowItem item = 1;
+
+ message PassCardRowItem {
+ //This is actually a oneof but we only care for this one
+ PassCardRowItemReferenceValue reference_value = 2;
+
+ message PassCardRowItemReferenceValue {
+ ItemReference item_reference = 1;
+
+ message ItemReference {
+ ReferenceValue title = 1;
+ ReferenceValue subtitle = 2;
+ }
+ }
+ }
+}
+
+message PassCardRowTransit {
+ ReferenceValue route_name = 6;
+ TransitLocation start_location = 3;
+ TransitLocation end_location = 4;
+ TransitModeInfo tranist_mode_info = 5;
+
+ message TransitLocation {
+ ReferenceValue name = 1;
+ ReferenceValue short_name = 2;
+ }
+}
+
+message PassCardRowDivider {
+ //Stub
+}
+
+message PassCardRowSpace {
+ //Stub
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/ColorProfile.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/ColorProfile.proto
new file mode 100644
index 0000000..a13e946
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/ColorProfile.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.passes.templates;
+
+import "google/type/color.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.passes.templates";
+option java_outer_classname = "ColorProfileProto";
+
+message PassColorProfile {
+ .google.type.Color backdrop_color = 1;
+ .google.type.Color card_color = 2;
+ .google.type.Color card_icon_color = 3;
+ .google.type.Color card_label_text_color = 4;
+ .google.type.Color card_content_text_color = 5;
+ .google.type.Color card_divider_color = 6;
+ .google.type.Color list_icon_color = 7;
+ .google.type.Color list_label_text_color = 8;
+ .google.type.Color list_content_text_color = 9;
+ .google.type.Color action_primary_color = 11;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Detail.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Detail.proto
new file mode 100644
index 0000000..2022c9a
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Detail.proto
@@ -0,0 +1,16 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.passes.templates;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Card.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/ColorProfile.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.passes.templates";
+option java_outer_classname = "DetailProto";
+
+message PassDetailCardTemplateInfo {
+ PassCardHeaderTemplateInfo card_header_info = 1;
+ repeated PassCardRowTemplateInfo card_row_info = 2;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Logo.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Logo.proto
new file mode 100644
index 0000000..cd1af75
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Logo.proto
@@ -0,0 +1,12 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.passes.templates;
+
+option java_package = "com.google.internal.tapandpay.v1.passes.templates";
+option java_outer_classname = "LogoProto";
+
+message PassLogo {
+ string url = 1;
+ string fallback_display_letter = 2;
+ string image_content_description = 3;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Pass.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Pass.proto
new file mode 100644
index 0000000..deb7959
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Pass.proto
@@ -0,0 +1,17 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.passes.templates;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/ColorProfile.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Detail.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.passes.templates";
+option java_outer_classname = "PassProto";
+
+message PassTemplateInfo {
+ PassColorProfile color_profile = 1;
+ PassDetailCardTemplateInfo details_card_info = 2;
+ PassColorProfile dark_color_profile = 4;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Reference.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Reference.proto
new file mode 100644
index 0000000..2edb0b5
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/Reference.proto
@@ -0,0 +1,20 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.passes.templates;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/ColorProfile.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Logo.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.passes.templates";
+option java_outer_classname = "ReferenceProto";
+
+message ReferenceValue {
+ oneof Value {
+ string html = 1;
+ string dynamic_formatted_string = 2;
+ string safe_html = 3;
+ }
+ string raw_value = 6;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/TransitMode.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/TransitMode.proto
new file mode 100644
index 0000000..c5322a9
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/passes/templates/TransitMode.proto
@@ -0,0 +1,30 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.passes.templates;
+
+option java_package = "com.google.internal.tapandpay.v1.passes.templates";
+option java_outer_classname = "TransitModeProto";
+
+message TransitModeInfo {
+ TransitMode transit_mode = 1;
+ string image_content_description = 2;
+
+ enum TransitMode {
+ UNDEFINED = 0;
+ BOAT = 1;
+ BUS = 2;
+ CAR = 3;
+ FLIGHT = 4;
+ SUBWAY = 5;
+ TAXI = 6;
+ TRAIN = 7;
+ TRAM = 8;
+ FLIGHT_LAND = 9;
+ FLIGHT_TAKEOFF = 10;
+ WALK = 11;
+ TRANSFER = 12;
+ FORWARD = 13;
+ BACK = 14;
+ SWAP = 15;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Common.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Common.proto
new file mode 100644
index 0000000..73509c0
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Common.proto
@@ -0,0 +1,158 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/android/libraries/commerce/hce/ndef/Format.proto";
+import "google/protobuf/timestamp.proto";
+import "google/type/color.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "CommonProto";
+
+message Metadata {
+ string sort_key = 1;
+ .google.protobuf.Timestamp create_time = 2;
+ .google.protobuf.Timestamp update_time = 3;
+ bool editable = 4;
+ bool is_active = 5;
+ bool device_pinned_valuable = 8;
+ bool prevent_auto_save = 9;
+ bool has_linked_valuables = 10;
+ int32 source = 11;
+ string gmail_permalink_id = 12;
+ OnViewConfig on_view_config = 14;
+
+ message OnViewConfig {
+ bool notify_on_view = 1;
+ }
+}
+
+message IssuerInfo {
+ string id = 1;
+ string title = 2;
+ string issuer_name = 3;
+ repeated IssuerMessage message = 5;
+ repeated LinkInfo link = 6;
+ repeated MainImageInfo main_image = 7;
+ repeated ExtraText extra_text = 8;
+ Logo logo = 9;
+ repeated ExtraKeyedInfo extra_keyed_info = 10;
+ HeroImage hero_image = 11;
+ .google.type.Color background_color = 13;
+ string save_to_android_pay_preview_message = 14;
+}
+
+message IssuerMessage {
+ string header = 1;
+ string body = 2;
+}
+
+message LinkInfo {
+ string url = 1;
+ string label = 2;
+ int32 type = 3;
+}
+
+message MainImageInfo {
+ string uri = 1;
+}
+
+message ExtraText {
+ string header = 1;
+ string body = 2;
+}
+
+message Logo {
+ string url = 1;
+ .google.type.Color dominant_color = 2;
+}
+
+message ExtraKeyedInfo {
+ string label = 1;
+ string value = 2;
+}
+
+message HeroImage {
+ string url = 1;
+ .google.type.Color dominant_color = 2;
+}
+
+message RedemptionInfo {
+ string identifier = 1;
+ Barcode barcode = 2;
+ repeated SmartTapRedemptionInfo smart_tap_redemption_info = 4;
+ bool hide_barcode = 5;
+ int64 issuing_smart_tap_collector_id = 7;
+ bool use_expanded_barcode_view = 10;
+ bool smart_tap_enabled = 11;
+ ObjectId object_id = 12;
+}
+
+message Barcode {
+ BarcodeType type = 1;
+ string encoded_value = 2;
+ string display_text = 3;
+ BarcodeRenderEncoding barcode_render_encoding = 5;
+}
+
+enum BarcodeType {
+ BARCODE_TYPE_UNSPECIFIED = 0;
+ BARCODE_TYPE_UNRECOGNIZED = 1;
+ BARCODE_TYPE_AZTEC = 2;
+ BARCODE_TYPE_CODE_39 = 3;
+ BARCODE_TYPE_CODE_128 = 5;
+ BARCODE_TYPE_CODABAR = 6;
+ BARCODE_TYPE_DATA_MATRIX = 7;
+ BARCODE_TYPE_EAN_8 = 8;
+ BARCODE_TYPE_EAN_13 = 9;
+ BARCODE_TYPE_ITF_14 = 10;
+ BARCODE_TYPE_PDF_417 = 11;
+ BARCODE_TYPE_QR_CODE = 14;
+ BARCODE_TYPE_UPC_A = 15;
+ BARCODE_TYPE_UPC_E = 16;
+ BARCODE_TYPE_TEXT_ONLY = 19;
+}
+
+message SmartTapRedemptionInfo {
+ int64 collector_id = 1;
+ android.libraries.commerce.hce.ndef.Format type = 2;
+ bool encryption_required = 22;
+ bool authentication_required = 23;
+ repeated IssuerAuthenticationKey issuer_authentication_key = 24;
+ bool unlock_required = 25;
+ bool multiple_transmission_required = 29;
+}
+
+message IssuerAuthenticationKey {
+ bytes compressed_public_key = 1;
+ int32 id = 2;
+}
+
+message ObjectId {
+ int64 issuer_id = 1;
+ string issuer_object_id = 2;
+}
+
+enum BarcodeRenderEncoding {
+ BARCODE_RENDER_ENCODING_UNSPECIFIED = 0;
+ BARCODE_RENDER_ENCODING_UTF_8 = 1;
+}
+
+message GroupingInfo {
+ string grouping_id = 1;
+ GroupingImage grouping_image = 2;
+ string grouping_title = 3;
+ string grouping_subtitle = 4;
+ string grouping_hint = 5;
+ .google.type.Color background_color = 6;
+ int32 sort_index = 7;
+}
+
+message GroupingImage {
+ string uri = 1;
+}
+
+message LabeledStringValue {
+ string label = 1;
+ string value = 2;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/EventTicket.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/EventTicket.proto
new file mode 100644
index 0000000..1d49c9f
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/EventTicket.proto
@@ -0,0 +1,56 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "com/google/internal/tapandpay/v1/notifications/ScheduledNotificationProto.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Pass.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+import "google/protobuf/timestamp.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "EventTicketProto";
+
+message EventTicket {
+ string id = 1;
+ Metadata metadata = 2;
+ IssuerInfo issuer_info = 3;
+ RedemptionInfo redemption_info = 4;
+ EventVenue venue = 6;
+ EventDateTime date_time = 7;
+ EventSeat seat_info = 8;
+ EventReservationInfo reservation_info = 9;
+ string ticket_holder_name = 10;
+ string ticket_number = 11;
+ string ticket_type = 12;
+ Money face_value = 13;
+ string fine_print = 14;
+ GroupingInfo grouping_info = 15;
+ notifications.ScheduledNotification upcoming_event_notification = 16;
+ passes.templates.PassTemplateInfo template_info = 17;
+
+ message EventVenue {
+ string maps_uri = 1;
+ string name = 2;
+ string address = 3;
+ }
+
+ message EventDateTime {
+ string doors_open_label = 1;
+ DateTime doors_open = 2;
+ DateTime start = 3;
+ DateTime end = 4;
+ }
+
+ message EventSeat {
+ LabeledStringValue seat = 1;
+ LabeledStringValue row = 2;
+ LabeledStringValue section = 3;
+ LabeledStringValue gate = 4;
+ }
+
+ message EventReservationInfo {
+ LabeledStringValue confirmation_code = 1;
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Flight.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Flight.proto
new file mode 100644
index 0000000..083043a
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Flight.proto
@@ -0,0 +1,75 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "com/google/internal/tapandpay/v1/notifications/ScheduledNotificationProto.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Pass.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+import "google/type/color.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "FlightProto";
+
+message Flight {
+ string id = 1;
+ Metadata metadata = 2;
+ IssuerInfo issuer_info = 3;
+ RedemptionInfo redemption_info = 4;
+ string carrier_code_and_flight_number = 5;
+ AirportInfo origin = 6;
+ AirportInfo destination = 7;
+ FlightStatus airline_flight_status = 8;
+ string passenger_name = 9;
+ BoardingAndSeatingInfo boarding_and_seating_info = 10;
+ ReservationInfo reservation_info = 12;
+ GroupingInfo grouping_info = 13;
+ FlightCarrier flight_carrier = 14;
+ string security_program_logo_url = 15;
+ notifications.ScheduledNotification upcoming_flight_notification = 16;
+ string operating_carrier_info = 18;
+ FlightStatus google_flight_status = 20;
+ passes.templates.PassTemplateInfo template_info = 21;
+
+ message AirportInfo {
+ string serves_city = 1;
+ string airport_iata_code = 3;
+ string terminal = 4;
+ string gate = 5;
+ }
+
+ message FlightStatus {
+ string status = 1;
+ DateTime boarding = 7;
+ DateTime scheduled_departure = 8;
+ DateTime actual_departure = 9;
+ DateTime scheduled_arrival = 10;
+ DateTime actual_arrival = 11;
+ DateTime gate_closes = 12;
+ .google.type.Color status_color = 13;
+ string status_message = 14;
+ string more_details_uri = 15;
+ string more_details_message = 16;
+ }
+
+ message BoardingAndSeatingInfo {
+ LabeledStringValue boarding_group = 1;
+ LabeledStringValue seat_number = 2;
+ LabeledStringValue seat_class = 3;
+ LabeledStringValue boarding_position = 4;
+ LabeledStringValue sequence_number = 5;
+ LabeledStringValue boarding_door = 6;
+ string boarding_privilege_image_url = 7;
+ }
+
+ message ReservationInfo {
+ string confirmation_code = 1;
+ LabeledStringValue frequent_flyer_number = 2;
+ string eticket_number = 3;
+ }
+
+ message FlightCarrier {
+ string airline_alliance_logo_url = 3;
+ int32 airline_alliance_logo_location = 4;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/GenericCard.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/GenericCard.proto
new file mode 100644
index 0000000..81bfef7
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/GenericCard.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/passes/templates/Pass.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "GenericCardProto";
+
+message GenericCard {
+ string id = 1;
+ Metadata metadata = 2;
+ IssuerInfo issuer_info = 3;
+ RedemptionInfo redemption_info = 4;
+ GroupingInfo grouping_info = 5;
+ passes.templates.PassTemplateInfo template_info = 6;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/GiftCard.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/GiftCard.proto
new file mode 100644
index 0000000..fd443c4
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/GiftCard.proto
@@ -0,0 +1,37 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Pass.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+import "google/protobuf/timestamp.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "GiftCardProto";
+
+message GiftCard {
+ string id = 1;
+ int64 hash = 2;
+ Metadata metadata = 3;
+ IssuerInfo issuer_info = 4;
+ RedemptionInfo redemption_info = 5;
+ .google.protobuf.Timestamp expiration_time = 6;
+ BalanceInfo balance_info = 7;
+ string pin = 8;
+ string notes = 9;
+ string event_number = 10;
+ string pin_label = 11;
+ string default_currency_code = 12;
+ string country_display_name = 13;
+ int32 input_mode = 14;
+ string country_code = 15;
+ passes.templates.PassTemplateInfo template_info = 16;
+ GroupingInfo grouping_info = 17;
+
+ message BalanceInfo {
+ Money balance = 1;
+ int32 balance_source = 2;
+ .google.protobuf.Timestamp balance_update_time = 3;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/HealthCard.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/HealthCard.proto
new file mode 100644
index 0000000..3ec8501
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/HealthCard.proto
@@ -0,0 +1,12 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/passes/templates/Pass.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "HealthCardProto";
+
+message HealthCard {
+ //Not currently supported - no examples, no class on API - what is this valuable type?
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/LoyaltyCard.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/LoyaltyCard.proto
new file mode 100644
index 0000000..48ca348
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/LoyaltyCard.proto
@@ -0,0 +1,39 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Pass.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "LoyaltyCardProto";
+
+message LoyaltyCard {
+ string id = 1;
+ int64 hash = 2;
+ Metadata metadata = 3;
+ IssuerInfo issuer_info = 4;
+ RedemptionInfo redemption_info = 5;
+ string card_holder_name = 6;
+ RewardsInfo rewards_info = 7;
+ string card_holder_name_label = 8;
+ string membership_id_label = 9;
+ string country_display_name = 10;
+ int32 input_mode = 11;
+ string country_code = 12;
+ RewardsInfo secondary_rewards_info = 13;
+ passes.templates.PassTemplateInfo template_info = 16;
+ GroupingInfo grouping_info = 17;
+
+ message RewardsInfo {
+ oneof Points {
+ string points_string = 1;
+ int64 points_long = 2;
+ double points_double = 3;
+ }
+ string label = 5;
+ string tier = 6;
+ string tier_label = 7;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Offer.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Offer.proto
new file mode 100644
index 0000000..4e7a95c
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Offer.proto
@@ -0,0 +1,32 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "com/google/internal/tapandpay/v1/notifications/ScheduledNotificationProto.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Pass.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+import "google/protobuf/timestamp.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "OfferProto";
+
+message Offer {
+ string id = 1;
+ int64 hash = 2;
+ Metadata metadata = 3;
+ IssuerInfo issuer_info = 4;
+ RedemptionInfo redemption_info = 5;
+ .google.protobuf.Timestamp expiration_time = 6;
+ string description = 7;
+ string provider = 8;
+ string fine_print = 9;
+ repeated notifications.ScheduledNotification notifications = 10;
+ string offer_short_title = 12;
+ bool is_card_linked = 13;
+ string country_code = 14;
+ notifications.ScheduledNotification expiration_notification = 15;
+ DateTime expiration_date_time = 16;
+ passes.templates.PassTemplateInfo template_info = 17;
+ GroupingInfo grouping_info = 18;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/SyncValuablesRequest.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/SyncValuablesRequest.proto
new file mode 100644
index 0000000..ed67557
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/SyncValuablesRequest.proto
@@ -0,0 +1,30 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/valuables/ValuableWrapper.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "SyncValuablesRequestProto";
+
+message SyncValuablesRequest {
+
+ SyncValuablesRequestInner request = 2;
+
+ message SyncValuablesRequestInner {
+
+ Request request = 2;
+
+ message Request {
+ bytes header = 1;
+ string timezone = 2;
+ repeated CachedValuable cached_valuable = 3;
+
+ message CachedValuable {
+ string id = 1;
+ int64 hash = 2;
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/SyncValuablesResponse.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/SyncValuablesResponse.proto
new file mode 100644
index 0000000..a23bde1
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/SyncValuablesResponse.proto
@@ -0,0 +1,26 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/valuables/ValuableWrapper.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "SyncValuablesResponseProto";
+
+message SyncValuablesResponse {
+ Inner inner = 2;
+
+ message Inner {
+ Valuables valuables = 2;
+
+ message Valuables {
+ repeated Valuable valuable = 1;
+
+ message Valuable {
+ string id = 2;
+ int64 hash = 3;
+ ValuableWrapper valuable = 4;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Transit.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Transit.proto
new file mode 100644
index 0000000..ba05148
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/Transit.proto
@@ -0,0 +1,24 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+import "com/google/internal/tapandpay/v1/notifications/ScheduledNotificationProto.proto";
+import "com/google/internal/tapandpay/v1/passes/templates/Pass.proto";
+import "com/google/internal/tapandpay/v1/valuables/Common.proto";
+import "google/protobuf/timestamp.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "TransitProto";
+
+message TransitCard {
+ string id = 1;
+ Metadata metadata = 2;
+ IssuerInfo issuer_info = 3;
+ RedemptionInfo redemption_info = 4;
+ GroupingInfo grouping_info = 5;
+ passes.templates.PassTemplateInfo template_info = 6;
+ notifications.ScheduledNotification upcoming_transit_notification = 7;
+ DateTime departure_time = 8;
+ DateTime arrival_time = 9;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/ValuableWrapper.proto b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/ValuableWrapper.proto
new file mode 100644
index 0000000..b53c82e
--- /dev/null
+++ b/app/src/main/proto/com/google/internal/tapandpay/v1/valuables/ValuableWrapper.proto
@@ -0,0 +1,29 @@
+syntax = "proto3";
+
+package com.google.internal.tapandpay.v1.valuables;
+
+import "com/google/internal/tapandpay/v1/valuables/EventTicket.proto";
+import "com/google/internal/tapandpay/v1/valuables/Flight.proto";
+import "com/google/internal/tapandpay/v1/valuables/GenericCard.proto";
+import "com/google/internal/tapandpay/v1/valuables/GiftCard.proto";
+import "com/google/internal/tapandpay/v1/valuables/HealthCard.proto";
+import "com/google/internal/tapandpay/v1/valuables/LoyaltyCard.proto";
+import "com/google/internal/tapandpay/v1/valuables/Offer.proto";
+import "com/google/internal/tapandpay/v1/valuables/Transit.proto";
+
+option java_package = "com.google.internal.tapandpay.v1.valuables";
+option java_outer_classname = "ValuableWrapperProto";
+
+message ValuableWrapper {
+ oneof Valuable {
+ LoyaltyCard loyalty_card = 1;
+ GiftCard gift_card = 2;
+ Offer offer = 3;
+ EventTicket event_ticket = 4;
+ Flight flight = 5;
+ TransitCard transit_card = 6;
+ HealthCard health_card = 7;
+ GenericCard generic_card = 8;
+ GenericCard sensitive_generic_pass = 9;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/kieronquinn/app/smartspacer/plugin/googlewallet/model/extras/TransitCardExtras.proto b/app/src/main/proto/com/kieronquinn/app/smartspacer/plugin/googlewallet/model/extras/TransitCardExtras.proto
new file mode 100644
index 0000000..73f9dc8
--- /dev/null
+++ b/app/src/main/proto/com/kieronquinn/app/smartspacer/plugin/googlewallet/model/extras/TransitCardExtras.proto
@@ -0,0 +1,87 @@
+syntax = "proto3";
+
+package com.kieronquinn.app.smartspacer.plugin.googlewallet.model.extras;
+
+import "com/google/internal/tapandpay/v1/Common.proto";
+
+option java_package = "com.kieronquinn.app.smartspacer.plugin.googlewallet.model.extras";
+option java_outer_classname = "TransitCardExtrasProto";
+
+/**
+ * Generic Transit Card extra proto message. This is designed to encompass every type of transit
+ * extra that could be shown by Smartspacer. If your transit provider exposes more data, you may
+ * add more fields and messages, but do not change the field numbers or names of the current ones.
+ */
+message TransitCardExtras {
+ //Internal service ID for the overall service, usually constructed from the time, origin & destination
+ string service_id = 1;
+ //The legs of the journey
+ repeated TransitLeg leg = 2;
+ //If the journey is no longer possible due to delays, the index of the first impossible leg change
+ int32 impossible_leg_change = 3;
+ //Items to show in the list area of the Target, if set (up to 4)
+ repeated string items = 4;
+
+ message TransitLeg {
+ //Whether the service can be tracked using realtime data, or if data is effectively static
+ bool has_realtime = 1;
+ //The internal ID of the service, for example "Y41331"
+ string service_id = 2;
+ //The internal ID of the operator, for example "urn:trainline:atoc:carrier:NT"
+ string operator_id = 3;
+ //The user-visible "origin" of the service, this should be where they board the service at
+ string origin = 4;
+ //The user-visible destination of the service, this may not be where they alight but the eventual destination
+ string destination = 5;
+ //The internal ID of the "origin" of the service, for example "urn:trainline:atoc:loc:MAN2968"
+ string origin_id = 6;
+ //The internal ID of the destination of the service, for example "urn:trainline:atoc:loc:PRE2753"
+ string destination_id = 7;
+ //The scheduled departure time of this service from the "origin", should always be supplied
+ com.google.internal.tapandpay.v1.DateTime scheduled_departure = 8;
+ //The scheduled arrival time of this service to the destination, should always be supplied
+ com.google.internal.tapandpay.v1.DateTime scheduled_arrival = 9;
+ //The estimated or actual departure time of this service from the "origin", optional
+ com.google.internal.tapandpay.v1.DateTime actual_departure = 10;
+ //The estimated or actual arrival time of this service to the destination, optional
+ com.google.internal.tapandpay.v1.DateTime actual_arrival = 11;
+ //Whether the service has been cancelled
+ bool cancelled = 12;
+ //The mode of this leg
+ Mode mode = 13;
+ //A contextual message for this leg, optional
+ Message message = 14;
+ //The platform or gate this service is leaving from, if available
+ string platform_or_gate = 15;
+ //A user-visible service name, for example "Caledonian Sleeper", or in some cases the route ID
+ string service = 16;
+ //A user-visible operator name, for example "Northern"
+ string operator = 17;
+
+ //Pre-set messages for each leg of the journey, ordered by increasing severity
+ enum Message {
+ //Unset
+ MESSAGE_UNSPECIFIED = 0;
+ //The leg is on time
+ ON_TIME = 1;
+ //The leg is delayed, the service may define what this means
+ DELAYED = 2;
+ //The leg is partially or fully cancelled
+ CANCELLED = 4;
+ }
+
+ //Modes for tranist legs
+ enum Mode {
+ //Unknown mode, fallback
+ MODE_UNSPECIFIED = 0;
+ //Main modes
+ TRAIN = 1;
+ //May also be used for rail replacement
+ BUS = 2;
+ TRAM = 4;
+ FERRY = 5;
+ //For moving between nearby interchanges
+ WALK = 6;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/google/type/calendar_period.proto b/app/src/main/proto/google/type/calendar_period.proto
new file mode 100644
index 0000000..82f5690
--- /dev/null
+++ b/app/src/main/proto/google/type/calendar_period.proto
@@ -0,0 +1,56 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option go_package = "google.golang.org/genproto/googleapis/type/calendarperiod;calendarperiod";
+option java_multiple_files = true;
+option java_outer_classname = "CalendarPeriodProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// A `CalendarPeriod` represents the abstract concept of a time period that has
+// a canonical start. Grammatically, "the start of the current
+// `CalendarPeriod`." All calendar times begin at midnight UTC.
+enum CalendarPeriod {
+ // Undefined period, raises an error.
+ CALENDAR_PERIOD_UNSPECIFIED = 0;
+
+ // A day.
+ DAY = 1;
+
+ // A week. Weeks begin on Monday, following
+ // [ISO 8601](https://en.wikipedia.org/wiki/ISO_week_date).
+ WEEK = 2;
+
+ // A fortnight. The first calendar fortnight of the year begins at the start
+ // of week 1 according to
+ // [ISO 8601](https://en.wikipedia.org/wiki/ISO_week_date).
+ FORTNIGHT = 3;
+
+ // A month.
+ MONTH = 4;
+
+ // A quarter. Quarters start on dates 1-Jan, 1-Apr, 1-Jul, and 1-Oct of each
+ // year.
+ QUARTER = 5;
+
+ // A half-year. Half-years start on dates 1-Jan and 1-Jul.
+ HALF = 6;
+
+ // A year.
+ YEAR = 7;
+}
diff --git a/app/src/main/proto/color.proto b/app/src/main/proto/google/type/color.proto
similarity index 100%
rename from app/src/main/proto/color.proto
rename to app/src/main/proto/google/type/color.proto
diff --git a/app/src/main/proto/google/type/date.proto b/app/src/main/proto/google/type/date.proto
new file mode 100644
index 0000000..e4e730e
--- /dev/null
+++ b/app/src/main/proto/google/type/date.proto
@@ -0,0 +1,52 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/date;date";
+option java_multiple_files = true;
+option java_outer_classname = "DateProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Represents a whole or partial calendar date, such as a birthday. The time of
+// day and time zone are either specified elsewhere or are insignificant. The
+// date is relative to the Gregorian Calendar. This can represent one of the
+// following:
+//
+// * A full date, with non-zero year, month, and day values
+// * A month and day value, with a zero year, such as an anniversary
+// * A year on its own, with zero month and day values
+// * A year and month value, with a zero day, such as a credit card expiration
+// date
+//
+// Related types are [google.type.TimeOfDay][google.type.TimeOfDay] and
+// `google.protobuf.Timestamp`.
+message Date {
+ // Year of the date. Must be from 1 to 9999, or 0 to specify a date without
+ // a year.
+ int32 year = 1;
+
+ // Month of a year. Must be from 1 to 12, or 0 to specify a year without a
+ // month and day.
+ int32 month = 2;
+
+ // Day of a month. Must be from 1 to 31 and valid for the year and month, or 0
+ // to specify a year by itself or a year and month where the day isn't
+ // significant.
+ int32 day = 3;
+}
diff --git a/app/src/main/proto/google/type/datetime.proto b/app/src/main/proto/google/type/datetime.proto
new file mode 100644
index 0000000..cfed85d
--- /dev/null
+++ b/app/src/main/proto/google/type/datetime.proto
@@ -0,0 +1,104 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+import "google/protobuf/duration.proto";
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/datetime;datetime";
+option java_multiple_files = true;
+option java_outer_classname = "DateTimeProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Represents civil time (or occasionally physical time).
+//
+// This type can represent a civil time in one of a few possible ways:
+//
+// * When utc_offset is set and time_zone is unset: a civil time on a calendar
+// day with a particular offset from UTC.
+// * When time_zone is set and utc_offset is unset: a civil time on a calendar
+// day in a particular time zone.
+// * When neither time_zone nor utc_offset is set: a civil time on a calendar
+// day in local time.
+//
+// The date is relative to the Proleptic Gregorian Calendar.
+//
+// If year is 0, the DateTime is considered not to have a specific year. month
+// and day must have valid, non-zero values.
+//
+// This type may also be used to represent a physical time if all the date and
+// time fields are set and either case of the `time_offset` oneof is set.
+// Consider using `Timestamp` message for physical time instead. If your use
+// case also would like to store the user's timezone, that can be done in
+// another field.
+//
+// This type is more flexible than some applications may want. Make sure to
+// document and validate your application's limitations.
+message DateTime {
+ // Optional. Year of date. Must be from 1 to 9999, or 0 if specifying a
+ // datetime without a year.
+ int32 year = 1;
+
+ // Required. Month of year. Must be from 1 to 12.
+ int32 month = 2;
+
+ // Required. Day of month. Must be from 1 to 31 and valid for the year and
+ // month.
+ int32 day = 3;
+
+ // Required. Hours of day in 24 hour format. Should be from 0 to 23. An API
+ // may choose to allow the value "24:00:00" for scenarios like business
+ // closing time.
+ int32 hours = 4;
+
+ // Required. Minutes of hour of day. Must be from 0 to 59.
+ int32 minutes = 5;
+
+ // Required. Seconds of minutes of the time. Must normally be from 0 to 59. An
+ // API may allow the value 60 if it allows leap-seconds.
+ int32 seconds = 6;
+
+ // Required. Fractions of seconds in nanoseconds. Must be from 0 to
+ // 999,999,999.
+ int32 nanos = 7;
+
+ // Optional. Specifies either the UTC offset or the time zone of the DateTime.
+ // Choose carefully between them, considering that time zone data may change
+ // in the future (for example, a country modifies their DST start/end dates,
+ // and future DateTimes in the affected range had already been stored).
+ // If omitted, the DateTime is considered to be in local time.
+ oneof time_offset {
+ // UTC offset. Must be whole seconds, between -18 hours and +18 hours.
+ // For example, a UTC offset of -4:00 would be represented as
+ // { seconds: -14400 }.
+ google.protobuf.Duration utc_offset = 8;
+
+ // Time zone.
+ TimeZone time_zone = 9;
+ }
+}
+
+// Represents a time zone from the
+// [IANA Time Zone Database](https://www.iana.org/time-zones).
+message TimeZone {
+ // IANA Time Zone Database time zone, e.g. "America/New_York".
+ string id = 1;
+
+ // Optional. IANA Time Zone Database version number, e.g. "2019a".
+ string version = 2;
+}
diff --git a/app/src/main/proto/google/type/dayofweek.proto b/app/src/main/proto/google/type/dayofweek.proto
new file mode 100644
index 0000000..4c80c62
--- /dev/null
+++ b/app/src/main/proto/google/type/dayofweek.proto
@@ -0,0 +1,50 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option go_package = "google.golang.org/genproto/googleapis/type/dayofweek;dayofweek";
+option java_multiple_files = true;
+option java_outer_classname = "DayOfWeekProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Represents a day of the week.
+enum DayOfWeek {
+ // The day of the week is unspecified.
+ DAY_OF_WEEK_UNSPECIFIED = 0;
+
+ // Monday
+ MONDAY = 1;
+
+ // Tuesday
+ TUESDAY = 2;
+
+ // Wednesday
+ WEDNESDAY = 3;
+
+ // Thursday
+ THURSDAY = 4;
+
+ // Friday
+ FRIDAY = 5;
+
+ // Saturday
+ SATURDAY = 6;
+
+ // Sunday
+ SUNDAY = 7;
+}
diff --git a/app/src/main/proto/google/type/decimal.proto b/app/src/main/proto/google/type/decimal.proto
new file mode 100644
index 0000000..beb18a5
--- /dev/null
+++ b/app/src/main/proto/google/type/decimal.proto
@@ -0,0 +1,95 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/decimal;decimal";
+option java_multiple_files = true;
+option java_outer_classname = "DecimalProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// A representation of a decimal value, such as 2.5. Clients may convert values
+// into language-native decimal formats, such as Java's [BigDecimal][] or
+// Python's [decimal.Decimal][].
+//
+// [BigDecimal]:
+// https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/math/BigDecimal.html
+// [decimal.Decimal]: https://docs.python.org/3/library/decimal.html
+message Decimal {
+ // The decimal value, as a string.
+ //
+ // The string representation consists of an optional sign, `+` (`U+002B`)
+ // or `-` (`U+002D`), followed by a sequence of zero or more decimal digits
+ // ("the integer"), optionally followed by a fraction, optionally followed
+ // by an exponent.
+ //
+ // The fraction consists of a decimal point followed by zero or more decimal
+ // digits. The string must contain at least one digit in either the integer
+ // or the fraction. The number formed by the sign, the integer and the
+ // fraction is referred to as the significand.
+ //
+ // The exponent consists of the character `e` (`U+0065`) or `E` (`U+0045`)
+ // followed by one or more decimal digits.
+ //
+ // Services **should** normalize decimal values before storing them by:
+ //
+ // - Removing an explicitly-provided `+` sign (`+2.5` -> `2.5`).
+ // - Replacing a zero-length integer value with `0` (`.5` -> `0.5`).
+ // - Coercing the exponent character to lower-case (`2.5E8` -> `2.5e8`).
+ // - Removing an explicitly-provided zero exponent (`2.5e0` -> `2.5`).
+ //
+ // Services **may** perform additional normalization based on its own needs
+ // and the internal decimal implementation selected, such as shifting the
+ // decimal point and exponent value together (example: `2.5e-1` <-> `0.25`).
+ // Additionally, services **may** preserve trailing zeroes in the fraction
+ // to indicate increased precision, but are not required to do so.
+ //
+ // Note that only the `.` character is supported to divide the integer
+ // and the fraction; `,` **should not** be supported regardless of locale.
+ // Additionally, thousand separators **should not** be supported. If a
+ // service does support them, values **must** be normalized.
+ //
+ // The ENBF grammar is:
+ //
+ // DecimalString =
+ // [Sign] Significand [Exponent];
+ //
+ // Sign = '+' | '-';
+ //
+ // Significand =
+ // Digits ['.'] [Digits] | [Digits] '.' Digits;
+ //
+ // Exponent = ('e' | 'E') [Sign] Digits;
+ //
+ // Digits = { '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' };
+ //
+ // Services **should** clearly document the range of supported values, the
+ // maximum supported precision (total number of digits), and, if applicable,
+ // the scale (number of digits after the decimal point), as well as how it
+ // behaves when receiving out-of-bounds values.
+ //
+ // Services **may** choose to accept values passed as input even when the
+ // value has a higher precision or scale than the service supports, and
+ // **should** round the value to fit the supported scale. Alternatively, the
+ // service **may** error with `400 Bad Request` (`INVALID_ARGUMENT` in gRPC)
+ // if precision would be lost.
+ //
+ // Services **should** error with `400 Bad Request` (`INVALID_ARGUMENT` in
+ // gRPC) if the service receives a value outside of the supported range.
+ string value = 1;
+}
diff --git a/app/src/main/proto/google/type/expr.proto b/app/src/main/proto/google/type/expr.proto
new file mode 100644
index 0000000..af0778c
--- /dev/null
+++ b/app/src/main/proto/google/type/expr.proto
@@ -0,0 +1,73 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option go_package = "google.golang.org/genproto/googleapis/type/expr;expr";
+option java_multiple_files = true;
+option java_outer_classname = "ExprProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Represents a textual expression in the Common Expression Language (CEL)
+// syntax. CEL is a C-like expression language. The syntax and semantics of CEL
+// are documented at https://github.com/google/cel-spec.
+//
+// Example (Comparison):
+//
+// title: "Summary size limit"
+// description: "Determines if a summary is less than 100 chars"
+// expression: "document.summary.size() < 100"
+//
+// Example (Equality):
+//
+// title: "Requestor is owner"
+// description: "Determines if requestor is the document owner"
+// expression: "document.owner == request.auth.claims.email"
+//
+// Example (Logic):
+//
+// title: "Public documents"
+// description: "Determine whether the document should be publicly visible"
+// expression: "document.type != 'private' && document.type != 'internal'"
+//
+// Example (Data Manipulation):
+//
+// title: "Notification string"
+// description: "Create a notification string with a timestamp."
+// expression: "'New message received at ' + string(document.create_time)"
+//
+// The exact variables and functions that may be referenced within an expression
+// are determined by the service that evaluates it. See the service
+// documentation for additional information.
+message Expr {
+ // Textual representation of an expression in Common Expression Language
+ // syntax.
+ string expression = 1;
+
+ // Optional. Title for the expression, i.e. a short string describing
+ // its purpose. This can be used e.g. in UIs which allow to enter the
+ // expression.
+ string title = 2;
+
+ // Optional. Description of the expression. This is a longer text which
+ // describes the expression, e.g. when hovered over it in a UI.
+ string description = 3;
+
+ // Optional. String indicating the location of the expression for error
+ // reporting, e.g. a file name and a position in the file.
+ string location = 4;
+}
diff --git a/app/src/main/proto/google/type/fraction.proto b/app/src/main/proto/google/type/fraction.proto
new file mode 100644
index 0000000..6c5ae6e
--- /dev/null
+++ b/app/src/main/proto/google/type/fraction.proto
@@ -0,0 +1,33 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option go_package = "google.golang.org/genproto/googleapis/type/fraction;fraction";
+option java_multiple_files = true;
+option java_outer_classname = "FractionProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Represents a fraction in terms of a numerator divided by a denominator.
+message Fraction {
+ // The numerator in the fraction, e.g. 2 in 2/3.
+ int64 numerator = 1;
+
+ // The value by which the numerator is divided, e.g. 3 in 2/3. Must be
+ // positive.
+ int64 denominator = 2;
+}
diff --git a/app/src/main/proto/google/type/interval.proto b/app/src/main/proto/google/type/interval.proto
new file mode 100644
index 0000000..9702324
--- /dev/null
+++ b/app/src/main/proto/google/type/interval.proto
@@ -0,0 +1,46 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+import "google/protobuf/timestamp.proto";
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/interval;interval";
+option java_multiple_files = true;
+option java_outer_classname = "IntervalProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Represents a time interval, encoded as a Timestamp start (inclusive) and a
+// Timestamp end (exclusive).
+//
+// The start must be less than or equal to the end.
+// When the start equals the end, the interval is empty (matches no time).
+// When both start and end are unspecified, the interval matches any time.
+message Interval {
+ // Optional. Inclusive start of the interval.
+ //
+ // If specified, a Timestamp matching this interval will have to be the same
+ // or after the start.
+ google.protobuf.Timestamp start_time = 1;
+
+ // Optional. Exclusive end of the interval.
+ //
+ // If specified, a Timestamp matching this interval will have to be before the
+ // end.
+ google.protobuf.Timestamp end_time = 2;
+}
diff --git a/app/src/main/proto/google/type/latlng.proto b/app/src/main/proto/google/type/latlng.proto
new file mode 100644
index 0000000..9231456
--- /dev/null
+++ b/app/src/main/proto/google/type/latlng.proto
@@ -0,0 +1,37 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/latlng;latlng";
+option java_multiple_files = true;
+option java_outer_classname = "LatLngProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// An object that represents a latitude/longitude pair. This is expressed as a
+// pair of doubles to represent degrees latitude and degrees longitude. Unless
+// specified otherwise, this must conform to the
+// WGS84
+// standard. Values must be within normalized ranges.
+message LatLng {
+ // The latitude in degrees. It must be in the range [-90.0, +90.0].
+ double latitude = 1;
+
+ // The longitude in degrees. It must be in the range [-180.0, +180.0].
+ double longitude = 2;
+}
diff --git a/app/src/main/proto/google/type/localized_text.proto b/app/src/main/proto/google/type/localized_text.proto
new file mode 100644
index 0000000..5c6922b
--- /dev/null
+++ b/app/src/main/proto/google/type/localized_text.proto
@@ -0,0 +1,36 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/localized_text;localized_text";
+option java_multiple_files = true;
+option java_outer_classname = "LocalizedTextProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Localized variant of a text in a particular language.
+message LocalizedText {
+ // Localized string in the language corresponding to `language_code' below.
+ string text = 1;
+
+ // The text's BCP-47 language code, such as "en-US" or "sr-Latn".
+ //
+ // For more information, see
+ // http://www.unicode.org/reports/tr35/#Unicode_locale_identifier.
+ string language_code = 2;
+}
diff --git a/app/src/main/proto/google/type/money.proto b/app/src/main/proto/google/type/money.proto
new file mode 100644
index 0000000..98d6494
--- /dev/null
+++ b/app/src/main/proto/google/type/money.proto
@@ -0,0 +1,42 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/money;money";
+option java_multiple_files = true;
+option java_outer_classname = "MoneyProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Represents an amount of money with its currency type.
+message Money {
+ // The three-letter currency code defined in ISO 4217.
+ string currency_code = 1;
+
+ // The whole units of the amount.
+ // For example if `currencyCode` is `"USD"`, then 1 unit is one US dollar.
+ int64 units = 2;
+
+ // Number of nano (10^-9) units of the amount.
+ // The value must be between -999,999,999 and +999,999,999 inclusive.
+ // If `units` is positive, `nanos` must be positive or zero.
+ // If `units` is zero, `nanos` can be positive, zero, or negative.
+ // If `units` is negative, `nanos` must be negative or zero.
+ // For example $-1.75 is represented as `units`=-1 and `nanos`=-750,000,000.
+ int32 nanos = 3;
+}
diff --git a/app/src/main/proto/google/type/month.proto b/app/src/main/proto/google/type/month.proto
new file mode 100644
index 0000000..99e7551
--- /dev/null
+++ b/app/src/main/proto/google/type/month.proto
@@ -0,0 +1,65 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option go_package = "google.golang.org/genproto/googleapis/type/month;month";
+option java_multiple_files = true;
+option java_outer_classname = "MonthProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Represents a month in the Gregorian calendar.
+enum Month {
+ // The unspecified month.
+ MONTH_UNSPECIFIED = 0;
+
+ // The month of January.
+ JANUARY = 1;
+
+ // The month of February.
+ FEBRUARY = 2;
+
+ // The month of March.
+ MARCH = 3;
+
+ // The month of April.
+ APRIL = 4;
+
+ // The month of May.
+ MAY = 5;
+
+ // The month of June.
+ JUNE = 6;
+
+ // The month of July.
+ JULY = 7;
+
+ // The month of August.
+ AUGUST = 8;
+
+ // The month of September.
+ SEPTEMBER = 9;
+
+ // The month of October.
+ OCTOBER = 10;
+
+ // The month of November.
+ NOVEMBER = 11;
+
+ // The month of December.
+ DECEMBER = 12;
+}
diff --git a/app/src/main/proto/google/type/phone_number.proto b/app/src/main/proto/google/type/phone_number.proto
new file mode 100644
index 0000000..7bbb7d8
--- /dev/null
+++ b/app/src/main/proto/google/type/phone_number.proto
@@ -0,0 +1,113 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/phone_number;phone_number";
+option java_multiple_files = true;
+option java_outer_classname = "PhoneNumberProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// An object representing a phone number, suitable as an API wire format.
+//
+// This representation:
+//
+// - should not be used for locale-specific formatting of a phone number, such
+// as "+1 (650) 253-0000 ext. 123"
+//
+// - is not designed for efficient storage
+// - may not be suitable for dialing - specialized libraries (see references)
+// should be used to parse the number for that purpose
+//
+// To do something meaningful with this number, such as format it for various
+// use-cases, convert it to an `i18n.phonenumbers.PhoneNumber` object first.
+//
+// For instance, in Java this would be:
+//
+// com.google.type.PhoneNumber wireProto =
+// com.google.type.PhoneNumber.newBuilder().build();
+// com.google.i18n.phonenumbers.Phonenumber.PhoneNumber phoneNumber =
+// PhoneNumberUtil.getInstance().parse(wireProto.getE164Number(), "ZZ");
+// if (!wireProto.getExtension().isEmpty()) {
+// phoneNumber.setExtension(wireProto.getExtension());
+// }
+//
+// Reference(s):
+// - https://github.com/google/libphonenumber
+message PhoneNumber {
+ // An object representing a short code, which is a phone number that is
+ // typically much shorter than regular phone numbers and can be used to
+ // address messages in MMS and SMS systems, as well as for abbreviated dialing
+ // (e.g. "Text 611 to see how many minutes you have remaining on your plan.").
+ //
+ // Short codes are restricted to a region and are not internationally
+ // dialable, which means the same short code can exist in different regions,
+ // with different usage and pricing, even if those regions share the same
+ // country calling code (e.g. US and CA).
+ message ShortCode {
+ // Required. The BCP-47 region code of the location where calls to this
+ // short code can be made, such as "US" and "BB".
+ //
+ // Reference(s):
+ // - http://www.unicode.org/reports/tr35/#unicode_region_subtag
+ string region_code = 1;
+
+ // Required. The short code digits, without a leading plus ('+') or country
+ // calling code, e.g. "611".
+ string number = 2;
+ }
+
+ // Required. Either a regular number, or a short code. New fields may be
+ // added to the oneof below in the future, so clients should ignore phone
+ // numbers for which none of the fields they coded against are set.
+ oneof kind {
+ // The phone number, represented as a leading plus sign ('+'), followed by a
+ // phone number that uses a relaxed ITU E.164 format consisting of the
+ // country calling code (1 to 3 digits) and the subscriber number, with no
+ // additional spaces or formatting, e.g.:
+ // - correct: "+15552220123"
+ // - incorrect: "+1 (555) 222-01234 x123".
+ //
+ // The ITU E.164 format limits the latter to 12 digits, but in practice not
+ // all countries respect that, so we relax that restriction here.
+ // National-only numbers are not allowed.
+ //
+ // References:
+ // - https://www.itu.int/rec/T-REC-E.164-201011-I
+ // - https://en.wikipedia.org/wiki/E.164.
+ // - https://en.wikipedia.org/wiki/List_of_country_calling_codes
+ string e164_number = 1;
+
+ // A short code.
+ //
+ // Reference(s):
+ // - https://en.wikipedia.org/wiki/Short_code
+ ShortCode short_code = 2;
+ }
+
+ // The phone number's extension. The extension is not standardized in ITU
+ // recommendations, except for being defined as a series of numbers with a
+ // maximum length of 40 digits. Other than digits, some other dialing
+ // characters such as ',' (indicating a wait) or '#' may be stored here.
+ //
+ // Note that no regions currently use extensions with short codes, so this
+ // field is normally only set in conjunction with an E.164 number. It is held
+ // separately from the E.164 number to allow for short code extensions in the
+ // future.
+ string extension = 3;
+}
diff --git a/app/src/main/proto/google/type/postal_address.proto b/app/src/main/proto/google/type/postal_address.proto
new file mode 100644
index 0000000..c57c7c3
--- /dev/null
+++ b/app/src/main/proto/google/type/postal_address.proto
@@ -0,0 +1,134 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/postaladdress;postaladdress";
+option java_multiple_files = true;
+option java_outer_classname = "PostalAddressProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Represents a postal address, e.g. for postal delivery or payments addresses.
+// Given a postal address, a postal service can deliver items to a premise, P.O.
+// Box or similar.
+// It is not intended to model geographical locations (roads, towns,
+// mountains).
+//
+// In typical usage an address would be created via user input or from importing
+// existing data, depending on the type of process.
+//
+// Advice on address input / editing:
+// - Use an i18n-ready address widget such as
+// https://github.com/google/libaddressinput)
+// - Users should not be presented with UI elements for input or editing of
+// fields outside countries where that field is used.
+//
+// For more guidance on how to use this schema, please see:
+// https://support.google.com/business/answer/6397478
+message PostalAddress {
+ // The schema revision of the `PostalAddress`. This must be set to 0, which is
+ // the latest revision.
+ //
+ // All new revisions **must** be backward compatible with old revisions.
+ int32 revision = 1;
+
+ // Required. CLDR region code of the country/region of the address. This
+ // is never inferred and it is up to the user to ensure the value is
+ // correct. See http://cldr.unicode.org/ and
+ // http://www.unicode.org/cldr/charts/30/supplemental/territory_information.html
+ // for details. Example: "CH" for Switzerland.
+ string region_code = 2;
+
+ // Optional. BCP-47 language code of the contents of this address (if
+ // known). This is often the UI language of the input form or is expected
+ // to match one of the languages used in the address' country/region, or their
+ // transliterated equivalents.
+ // This can affect formatting in certain countries, but is not critical
+ // to the correctness of the data and will never affect any validation or
+ // other non-formatting related operations.
+ //
+ // If this value is not known, it should be omitted (rather than specifying a
+ // possibly incorrect default).
+ //
+ // Examples: "zh-Hant", "ja", "ja-Latn", "en".
+ string language_code = 3;
+
+ // Optional. Postal code of the address. Not all countries use or require
+ // postal codes to be present, but where they are used, they may trigger
+ // additional validation with other parts of the address (e.g. state/zip
+ // validation in the U.S.A.).
+ string postal_code = 4;
+
+ // Optional. Additional, country-specific, sorting code. This is not used
+ // in most regions. Where it is used, the value is either a string like
+ // "CEDEX", optionally followed by a number (e.g. "CEDEX 7"), or just a number
+ // alone, representing the "sector code" (Jamaica), "delivery area indicator"
+ // (Malawi) or "post office indicator" (e.g. Côte d'Ivoire).
+ string sorting_code = 5;
+
+ // Optional. Highest administrative subdivision which is used for postal
+ // addresses of a country or region.
+ // For example, this can be a state, a province, an oblast, or a prefecture.
+ // Specifically, for Spain this is the province and not the autonomous
+ // community (e.g. "Barcelona" and not "Catalonia").
+ // Many countries don't use an administrative area in postal addresses. E.g.
+ // in Switzerland this should be left unpopulated.
+ string administrative_area = 6;
+
+ // Optional. Generally refers to the city/town portion of the address.
+ // Examples: US city, IT comune, UK post town.
+ // In regions of the world where localities are not well defined or do not fit
+ // into this structure well, leave locality empty and use address_lines.
+ string locality = 7;
+
+ // Optional. Sublocality of the address.
+ // For example, this can be neighborhoods, boroughs, districts.
+ string sublocality = 8;
+
+ // Unstructured address lines describing the lower levels of an address.
+ //
+ // Because values in address_lines do not have type information and may
+ // sometimes contain multiple values in a single field (e.g.
+ // "Austin, TX"), it is important that the line order is clear. The order of
+ // address lines should be "envelope order" for the country/region of the
+ // address. In places where this can vary (e.g. Japan), address_language is
+ // used to make it explicit (e.g. "ja" for large-to-small ordering and
+ // "ja-Latn" or "en" for small-to-large). This way, the most specific line of
+ // an address can be selected based on the language.
+ //
+ // The minimum permitted structural representation of an address consists
+ // of a region_code with all remaining information placed in the
+ // address_lines. It would be possible to format such an address very
+ // approximately without geocoding, but no semantic reasoning could be
+ // made about any of the address components until it was at least
+ // partially resolved.
+ //
+ // Creating an address only containing a region_code and address_lines, and
+ // then geocoding is the recommended way to handle completely unstructured
+ // addresses (as opposed to guessing which parts of the address should be
+ // localities or administrative areas).
+ repeated string address_lines = 9;
+
+ // Optional. The recipient at the address.
+ // This field may, under certain circumstances, contain multiline information.
+ // For example, it might contain "care of" information.
+ repeated string recipients = 10;
+
+ // Optional. The name of the organization at the address.
+ string organization = 11;
+}
diff --git a/app/src/main/proto/google/type/quaternion.proto b/app/src/main/proto/google/type/quaternion.proto
new file mode 100644
index 0000000..dfb822d
--- /dev/null
+++ b/app/src/main/proto/google/type/quaternion.proto
@@ -0,0 +1,94 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/quaternion;quaternion";
+option java_multiple_files = true;
+option java_outer_classname = "QuaternionProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// A quaternion is defined as the quotient of two directed lines in a
+// three-dimensional space or equivalently as the quotient of two Euclidean
+// vectors (https://en.wikipedia.org/wiki/Quaternion).
+//
+// Quaternions are often used in calculations involving three-dimensional
+// rotations (https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation),
+// as they provide greater mathematical robustness by avoiding the gimbal lock
+// problems that can be encountered when using Euler angles
+// (https://en.wikipedia.org/wiki/Gimbal_lock).
+//
+// Quaternions are generally represented in this form:
+//
+// w + xi + yj + zk
+//
+// where x, y, z, and w are real numbers, and i, j, and k are three imaginary
+// numbers.
+//
+// Our naming choice `(x, y, z, w)` comes from the desire to avoid confusion for
+// those interested in the geometric properties of the quaternion in the 3D
+// Cartesian space. Other texts often use alternative names or subscripts, such
+// as `(a, b, c, d)`, `(1, i, j, k)`, or `(0, 1, 2, 3)`, which are perhaps
+// better suited for mathematical interpretations.
+//
+// To avoid any confusion, as well as to maintain compatibility with a large
+// number of software libraries, the quaternions represented using the protocol
+// buffer below *must* follow the Hamilton convention, which defines `ij = k`
+// (i.e. a right-handed algebra), and therefore:
+//
+// i^2 = j^2 = k^2 = ijk = −1
+// ij = −ji = k
+// jk = −kj = i
+// ki = −ik = j
+//
+// Please DO NOT use this to represent quaternions that follow the JPL
+// convention, or any of the other quaternion flavors out there.
+//
+// Definitions:
+//
+// - Quaternion norm (or magnitude): `sqrt(x^2 + y^2 + z^2 + w^2)`.
+// - Unit (or normalized) quaternion: a quaternion whose norm is 1.
+// - Pure quaternion: a quaternion whose scalar component (`w`) is 0.
+// - Rotation quaternion: a unit quaternion used to represent rotation.
+// - Orientation quaternion: a unit quaternion used to represent orientation.
+//
+// A quaternion can be normalized by dividing it by its norm. The resulting
+// quaternion maintains the same direction, but has a norm of 1, i.e. it moves
+// on the unit sphere. This is generally necessary for rotation and orientation
+// quaternions, to avoid rounding errors:
+// https://en.wikipedia.org/wiki/Rotation_formalisms_in_three_dimensions
+//
+// Note that `(x, y, z, w)` and `(-x, -y, -z, -w)` represent the same rotation,
+// but normalization would be even more useful, e.g. for comparison purposes, if
+// it would produce a unique representation. It is thus recommended that `w` be
+// kept positive, which can be achieved by changing all the signs when `w` is
+// negative.
+//
+message Quaternion {
+ // The x component.
+ double x = 1;
+
+ // The y component.
+ double y = 2;
+
+ // The z component.
+ double z = 3;
+
+ // The scalar component.
+ double w = 4;
+}
diff --git a/app/src/main/proto/google/type/timeofday.proto b/app/src/main/proto/google/type/timeofday.proto
new file mode 100644
index 0000000..5cb48aa
--- /dev/null
+++ b/app/src/main/proto/google/type/timeofday.proto
@@ -0,0 +1,44 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.type;
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/type/timeofday;timeofday";
+option java_multiple_files = true;
+option java_outer_classname = "TimeOfDayProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+// Represents a time of day. The date and time zone are either not significant
+// or are specified elsewhere. An API may choose to allow leap seconds. Related
+// types are [google.type.Date][google.type.Date] and
+// `google.protobuf.Timestamp`.
+message TimeOfDay {
+ // Hours of day in 24 hour format. Should be from 0 to 23. An API may choose
+ // to allow the value "24:00:00" for scenarios like business closing time.
+ int32 hours = 1;
+
+ // Minutes of hour of day. Must be from 0 to 59.
+ int32 minutes = 2;
+
+ // Seconds of minutes of the time. Must normally be from 0 to 59. An API may
+ // allow the value 60 if it allows leap-seconds.
+ int32 seconds = 3;
+
+ // Fractions of seconds in nanoseconds. Must be from 0 to 999,999,999.
+ int32 nanos = 4;
+}
diff --git a/app/src/main/proto/loyaltycard.proto b/app/src/main/proto/loyaltycard.proto
index cae61e5..17f6a8c 100644
--- a/app/src/main/proto/loyaltycard.proto
+++ b/app/src/main/proto/loyaltycard.proto
@@ -12,7 +12,7 @@ package com.kieronquinn.app.classicpowermenu.model.protobuf.loyaltycard;
option java_package = "com.kieronquinn.app.classicpowermenu.model.protobuf.loyaltycard";
option java_outer_classname = "LoyaltyCardProtos";
-import "color.proto";
+import "google/type/color.proto";
message LoyaltyCard_Metadata {
string sortKey = 1;
@@ -51,7 +51,7 @@ message LoyaltyCard_RedemptionInfo {
LoyaltyCard_RedemptionInfo_Barcode barcode = 2;
}
-message LoyaltyCard {
+message LoyaltyCard_ {
string id = 1;
int64 hash = 2;
LoyaltyCard_Metadata metadata = 3;
diff --git a/app/src/main/res/drawable/ic_quick_access_wallet_manage.xml b/app/src/main/res/drawable/ic_quick_access_wallet_manage.xml
new file mode 100644
index 0000000..9b3bbe0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_quick_access_wallet_manage.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_quick_access_wallet_remove_account.xml b/app/src/main/res/drawable/ic_quick_access_wallet_remove_account.xml
new file mode 100644
index 0000000..23a4d30
--- /dev/null
+++ b/app/src/main/res/drawable/ic_quick_access_wallet_remove_account.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_search_google.xml b/app/src/main/res/drawable/ic_search_google.xml
new file mode 100644
index 0000000..ccef885
--- /dev/null
+++ b/app/src/main/res/drawable/ic_search_google.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_settings_quick_access_wallet_manage.xml b/app/src/main/res/layout/fragment_settings_quick_access_wallet_manage.xml
new file mode 100644
index 0000000..a07dadb
--- /dev/null
+++ b/app/src/main/res/layout/fragment_settings_quick_access_wallet_manage.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_settings_quick_access_wallet_rearrange.xml b/app/src/main/res/layout/fragment_settings_quick_access_wallet_rearrange.xml
deleted file mode 100644
index c95febe..0000000
--- a/app/src/main/res/layout/fragment_settings_quick_access_wallet_rearrange.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_sign_in_with_google.xml b/app/src/main/res/layout/fragment_sign_in_with_google.xml
new file mode 100644
index 0000000..8b7ecf8
--- /dev/null
+++ b/app/src/main/res/layout/fragment_sign_in_with_google.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_graph_settings_container.xml b/app/src/main/res/navigation/nav_graph_settings_container.xml
index e069a11..5d5ff40 100644
--- a/app/src/main/res/navigation/nav_graph_settings_container.xml
+++ b/app/src/main/res/navigation/nav_graph_settings_container.xml
@@ -14,11 +14,14 @@
android:id="@+id/action_settingsContainerFragment_to_settingsPowerOptionsRearrangeFragment"
app:destination="@id/settingsPowerOptionsRearrangeFragment" />
+ android:id="@+id/action_settingsContainerFragment_to_settingsQuickAccessWalletManageFragment"
+ app:destination="@id/settingsQuickAccessWalletManageFragment" />
+
+ android:id="@+id/settingsQuickAccessWalletManageFragment"
+ android:name="com.kieronquinn.app.classicpowermenu.ui.screens.settings.quickaccesswallet.manage.SettingsQuickAccessWalletManageFragment"
+ android:label="SettingsQuickAccessWalletManageFragment"
+ tools:layout="@layout/fragment_settings_quick_access_wallet_manage" >
+
+
+
No NFC emulation services found
إظهار بطاقات الولاء
تحميل وعرض بطاقات الولاء الخاصة بك من محفظة جوجل في محفظة الوصول السريع
- إعادة ترتيب & إخفاء بطاقات الولاء
- إعادة ترتيب وإظهار/إخفاء بطاقات الولاء في محفظة الوصول السريع
إظهار معاينة بطاقة الولاء
إظهار معاينة لبطاقة الولاء التي سيتم فحصها أو عرضها في المتجر دون فتح محفظة جوجل
السماح بدخول بطاقة الولاء أثناء القفل
@@ -95,8 +93,8 @@
السماح بالتفاعل مع عناصر التحكم في الجهاز أثناء تأمينه. هذا لا يسمح بإضافة أو تعديل عناصر التحكم أثناء قفل
اختيار لون الخلفية غير متوفر لخلفية الحائط المحددة حاليا
تحميل بطاقات الولاء…
- لا توجد بطاقات الولاء
- خطأ في تحميل بطاقات الولاء
+ لا توجد بطاقات الولاء
+ خطأ في تحميل بطاقات الولاء
وضع منشئ المحتوى
تمكين هذا لطمس دائما أرقام بطاقة الدفع الخاصة بك، وعرض الكود الدموي لبطاقات الولاء في المعاينة (تمكين ذلك بشكل منفصل إذا كنت تريد ذلك دائما). هذا مفيد لالتقاط لقطات شاشة أو مقاطع فيديو دون الكشف عن معلومات بطاقتك عن طريق الخطأ.\nتحذير: لا تحاول استخدام بطاقة الولاء مع تمكين هذا الوضع، قد يسبب أخطاء في أنظمة المتجر.
خدمة إمكانية الوصول غير مفعلة
diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml
index 0815e3d..37c8e40 100644
--- a/app/src/main/res/values-el-rGR/strings.xml
+++ b/app/src/main/res/values-el-rGR/strings.xml
@@ -82,8 +82,6 @@
No NFC emulation services found
Εμφάνιση Καρτών Αφοσίωσης
Φορτώστε και δείξτε τις Κάρτες Αφοσίωσης από το Πορτοφόλι Google στο Πορτοφόλι Γρήγορης Πρόσβασης
- Αναδιάταξη & Απόκρυψη Καρτών Αφοσίωσης
- Αναδιάταξη και εμφάνιση/απόκρυψη καρτών αφοσίωσης στο Πορτοφόλι Γρήγορης Πρόσβασης
Εμφάνιση Προεπισκόπησης Κάρτας Αφοσίωσης
Εμφάνιση μιας προεπισκόπησης της Κάρτας Αφοσίωσης που θα σαρωθεί ή θα προβληθεί στο κατάστημα χωρίς άνοιγμα του Πορτοφολιού Google
Επιτρέψτε την πρόσβαση σε Κάρτα Αφοσίωσης απ\' την οθόνη κλειδώματος
@@ -95,8 +93,8 @@
Να επιτρέπεται η αλληλεπίδραση με τα Στοιχεία Ελέγχου Συσκευής ενώ είναι κλειδωμένη η συσκευή. Αυτό δεν επιτρέπει την προσθήκη ή επεξεργασία στοιχείων ενώ είναι κλειδωμένη
Επιλογέας Χρώματος Ταπετσαρίας μη διαθέσιμος για την τρέχουσα επιλεγμένη ταπετσαρία
Φόρτωση Καρτών Αφοσίωσης…
- Δεν βρέθηκαν Κάρτες Αφοσίωσης
- Σφάλμα κατά τη φόρτωση Καρτών Αφοσίωσης
+ Δεν βρέθηκαν Κάρτες Αφοσίωσης
+ Σφάλμα κατά τη φόρτωση Καρτών Αφοσίωσης
Λειτουργία Δημιουργού Περιεχομένου
Ενεργοποιήστε αυτή την επιλογή για να θολώνονται πάντα οι αριθμοί της Κάρτας Πληρωμής και να εμφανίζονται εικονικοί κωδικοί για τις Κάρτες Αφοσίωσης στην προεπισκόπηση (ενεργοποιήστε το ξεχωριστά αν θέλετε πάντα ενεργοποιημένο). Αυτό είναι χρήσιμο για τη λήψη στιγμιότυπων οθόνης ή βίντεο χωρίς να εκθέτετε κατά λάθος τις πληροφορίες της κάρτας σας.\nΠροειδοποίηση: Μην προσπαθήσετε να χρησιμοποιήσετε μια Κάρτα Αφοσίωσης με ενεργοποιημένη αυτή τη λειτουργία, μπορεί να προκαλέσει σφάλματα στα συστήματα των καταστημάτων.
Η Υπηρεσία Προσβασιμότητας Δεν Είναι Ενεργοποιημένη
diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml
index 728a05e..4c8072a 100644
--- a/app/src/main/res/values-fr-rFR/strings.xml
+++ b/app/src/main/res/values-fr-rFR/strings.xml
@@ -82,8 +82,6 @@
No NFC emulation services found
Afficher les cartes de fidélité
Chargez et montrez vos cartes de fidélité depuis Google Wallet dans l\'accès rapide à Wallet
- Réorganiser & Cacher les cartes de fidélité
- Réorganiser et afficher/masquer les cartes de fidélité dans le portefeuille d\'accès rapide
Afficher l\'aperçu de la carte de fidélité
Afficher un aperçu de la carte de fidélité à scanner ou à consulter en magasin sans ouvrir Google Wallet
Permettre l\'accès à la carte de fidélité lorsque le téléphone est verrouillé
@@ -95,8 +93,8 @@
Allow interaction with Device Controls while locked. This does not allow adding or editing controls while locked
Wallpaper Color Picker unavailable for the currently selected wallpaper
Loading Loyalty Cards…
- No Loyalty Cards found
- Error loading Loyalty Cards
+ No Loyalty Cards found
+ Error loading Loyalty Cards
Content Creator Mode
Enable this to always blur your Payment Card numbers, and show dummy codes for Loyalty Cards in the preview (enable that separately if you want it always on). This is useful for taking screenshots or videos without accidentally exposing your card info.\nWarning: Don\'t try to use a Loyalty Card with this mode enabled, it may cause errors on stores\' systems.
Accessibility Service Not Enabled
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index cd6ee52..6b4a93b 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -82,8 +82,12 @@
No NFC emulation services found
Hűségkártyák megjelenítése
Hűségkártyák betöltése és megjelenítése a Google Wallet-ből a Gyorstárcában
- Hűségkártyák átrendezése és elrejtése
- A Gyorstárca hűségkártyáinak átrendezése és elrejtése vagy megjelenítése
+ Fiók és hűségkártyák kezelése
+ A Gyorstárca hűségkártyáinak bejelentkezés, átrendezése és elrejtése vagy megjelenítése
+ Kijelentkezés
+ Törölje a tárolt hűségkártya adatait és fiókot
+ Bejelentkezés szükséges
+ A hűségkártyák használatához be kell jelentkeznie Google Fiókjába a szokásos Google bejelentkezéssel. Erre azért van szükség, mert az API nem érhető el a szokásos android fiókbejelentkezéssel.\n\nAz Ön Wallet-adatai titkosítva vannak az eszközön, soha nincsenek továbbítva, és nem szerepelnek a biztonsági másolatokban, az Ön adatainak védelme és biztonsága érdekében.
Hűségkártya előnézetének megjelenítése
Mutassa a hűségkártya előnézetét, hogy leolvasható vagy megtekinthető legyen a Google Wallet megnyitása nélkül
Hűségkártyákhoz való hozzáférés engedélyezése lezárt állapotban
@@ -95,8 +99,8 @@
Az Eszközvezérlők használatának engedélyezése lezárt állapotban. Ez nem teszi lehetővé a vezérlőelemek hozzáadását vagy szerkesztését zárolt állapotban
A háttérszínválasztó nem érhető el a jelenleg kiválasztott háttérhez
Hűségkártyák betöltése…
- Nincs talált hűségkártya
- A hűségkártyák betöltése sikertelen
+ Nincs talált hűségkártya
+ A hűségkártyák betöltése sikertelen
Tartalomkészítő mód
Engedélyezze ezt a funkciót, hogy mindig kitakarja a fizető kártyák számait, és az előnézetben a hűségkártyák hamis kódokkal jelenjenek meg (engedélyezze ezt külön, hogy mindig be legyen kapcsolva). Ez hasznos képernyőképek vagy videók készítéséhez anélkül, hogy véletlenül felfedné a kártya adatait.\nFigyelmeztetés: Ne próbáljon meg hűségkártyát használni ezzel a móddal, mert hibát okozhat az üzletek rendszereiben.
Kisegítő lehetőségek szolgáltatás nincs engedélyezve
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 9d3a754..997f429 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -83,8 +83,6 @@
No NFC emulation services found
Visualizza Carte Fedeltà
Carica e mostra le tue carte fedeltà da Google Wallet nel Quick Access Wallet
- Riorganizza & Nascondi Carte Fedeltà
- Riordina e mostra/nascondi le carte fedeltà nel Quick Access Wallet
Mostra Anteprima Carta Fedeltà
Mostra un\'anteprima della Carta fedeltà da scansionare o visualizza in negozio senza aprire Google Wallet
Consenti l\'accesso alla Carta fedeltà con dispositivo bloccato
@@ -96,8 +94,8 @@
Consenti l\'interazione con i controlli smart con dispositivo bloccato. Ciò non consente di aggiungere o modificare i controlli con dispositivo bloccato
Selettore colore sfondo non disponibile per lo sfondo selezionato
Caricamento delle Carte Fedeltà…
- Nessuna Carta Fedeltà trovata
- Errore durante il caricamento delle Carte Fedeltà
+ Nessuna Carta Fedeltà trovata
+ Errore durante il caricamento delle Carte Fedeltà
Modalità Creatore di contenuti
Abilita questa opzione per offuscare sempre i numeri delle tue carte di pagamento e mostrare codici fittizi per le carte fedeltà nell\'anteprima (attivalo separatamente se lo desideri sempre attivo). Ciò è utile per acquisire schermate o video senza accidentalmente esporre le informazioni della tua carta.
Attenzione: non tentare di utilizzare una Carta Fedeltà con questa modalità abilitata, potrebbe causare errori sui sistemi dei negozi.
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 709be85..5bc0e7d 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -82,8 +82,6 @@
No NFC emulation services found
ポイントカードを表示
Google Walletのポイントカードをクイック アクセス ウォレットに読み込んで表示することができます
- ポイントカードの並べ替えと非表示
- クイック アクセス ウォレットでのポイントカードの並び替えや表示・非表示の設定
ポイントカードのプレビューを表示
Google Walletを開かずに、店舗でスキャンまたは表示するポイントカードのプレビューを表示することができます
ロック中にポイントカードにアクセスできるようにする
@@ -95,8 +93,8 @@
ロック中のデバイスコントロールとのインタラクションを許可します。ロック中はコントロールの追加や編集はできません。
現在選択されている壁紙では、壁紙のカラーピッカーは使用できません
ポイントカードの読み込み中...
- ポイントカードが見つかりません
- ポイントカードの読み込みエラー
+ ポイントカードが見つかりません
+ ポイントカードの読み込みエラー
コンテンツクリエイターモード
これを有効にすると、支払いカードの番号が常にぼかされ、ポイントカードのダミーコードがプレビューに表示されます(常に有効にしたい場合は、別途有効にしてください)。
これは、スクリーンショットや動画を撮影する際に、カード情報を誤って公開しないようにするために有効です。\n警告:このモードを有効にした状態でポイントカードを使用しようとすると、お店側のシステムでエラーを引き起こします。絶対に無効にした状態で使用してください。
diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml
index 80b0ef0..aed672a 100644
--- a/app/src/main/res/values-pl-rPL/strings.xml
+++ b/app/src/main/res/values-pl-rPL/strings.xml
@@ -83,8 +83,6 @@
Pokaż karty lojalnościowe
Załaduj i pokaż karty lojalnościowe z portfela Google w
łatwym dostępie do portfela
- Zmień kolejność i ukryj karty lojalnościowe
- Zmień kolejność i pokaż/ukryj karty lojalnościowe w łatwym dostępie do portfela
Pokaż podgląd kart lojalnościowych
Pokaż podgląd karty lojalnościowej do zeskanowania lub okazania w sklepie bez otwierania portfela Google
Zezwalaj na dostęp do kart lojalnościowych po zablokowaniu ekranu
@@ -96,8 +94,8 @@
Zezwalaj na interakcję ze sterowaniem urządzeniami po zablokowaniu. Nie pozwala na dodawanie lub edytowanie ustawień po zablokowaniu
Wybór kolorów tapety jest niedostępny dla aktualnie wybranej tapety
Ładowanie kart lojalnościowych…
- Nie znaleziono kart lojalnościowych
- Podczas ładowania kart lojalnościowych wystąpił błąd
+ Nie znaleziono kart lojalnościowych
+ Podczas ładowania kart lojalnościowych wystąpił błąd
Tryb tworzenia treści
Włącz, aby zawsze rozmywać numery kart płatniczych, i pokaż kody pozorowane dla kart lojalnościowych w podglądzie (jeśli chcesz, by były rozmyte zawsze, jest od tego oddzielna opcja). Jest to przydatne przy wykonywaniu zrzutów ekranu lub filmów bez przypadkowego ujawnienia informacji o karcie.\nOstrzeżenie: Nie próbuj używać karty lojalnościowej z włączonym trybem, może to powodować błędy w systemach sklepu.
Usługa dostępności nie jest aktywna
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index c639c7b..f7d9b31 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -85,8 +85,6 @@
Скрывать последние цифры Карты Оплаты при заблокированном экране
Показывать Карты лояльности
Загружать ваши Карты лояльности из Google Кошелька и показывать их в Быстром доступе
- Переместить и Скрыть Карты лояльности
- Переместить и показать/скрыть карты лояльности в быстром доступе Кошелька
Показывать предпросмотр Карт лояльности
Использовать предпросмотр Карт лояльности для сканирования или предоставления в магазине без открытия Google Кошелька
Разрешить доступ к Картам лояльности с заблокированным экраном
@@ -98,8 +96,8 @@
Позволяет использовать Управление Устрояствами с заблокированным экраном. Эта настройка не позволяет добавлять или редактировать устройства с заблокированным экраном
Выбор цвета не доступен для текущих обоев
Загрузка Карт Лояльности…
- Карты Лояльности не найдены
- Ошибка во время загрузки Карт Лояльности
+ Карты Лояльности не найдены
+ Ошибка во время загрузки Карт Лояльности
Режим Блогера
Включите, чтобы размыть последние цифры Карты Оплаты и показывать коды-заглушки для Карт Лояльности во время предпросмотра. Используйте, чтобы не раскрывать данные карты на снимках экрана или видео.\nОсторожно: Не пытайтесь использвать Карты Лояльности с этой настройкой, это может привести к сбоям систем магазина.
Специальные Возможности отключены
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 73f8c06..af93a9f 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -82,8 +82,6 @@
No NFC emulation services found
显示积分卡
在快捷支付中显示您的 Google Wallet 积分卡
- 重新排序 & 显示积分卡
- 在快捷支付中 重新排列 & 显示/隐藏积分卡
显示积分卡预览
在不打开 Google Wallet 的情况下显示要在商店中扫描或查看的积分卡的预览
允许在锁定状态下访问积分卡
@@ -95,8 +93,8 @@
允许在锁定状态下使用电源菜单控件,但不能添加或编辑控件
壁纸颜色选择工具无法选取当前壁纸的颜色
正在加载积分卡…
- 未找到积分卡
- 加载积分卡时出错
+ 未找到积分卡
+ 加载积分卡时出错
内容创作者模式
启用此功能可始终模糊您的支付卡号码,并在预览中显示积分卡的虚拟卡号(如果您希望它始终开启,请单独启用)。本功能将在截图或录制视频时保护您的卡号不被泄露\n警告:请勿尝试在启用此模式的情况下使用会员卡,这可能会导致商店系统出错。
无障碍服务没有启用
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index a617198..0dfd879 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -24,4 +24,9 @@
@color/icon_foreground
@color/white
+ #4285f4
+ #ea4335
+ #34a853
+ #fbbc05
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 427ef59..d3216f3 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -1,5 +1,7 @@
+ 64dp
+ 32dp
16dp
8dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index dcc6870..0e93ace 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -90,8 +90,14 @@
No NFC emulation services found
Show Loyalty Cards
Load and show your Loyalty Cards from Google Wallet in the Quick Access Wallet
- Rearrange & Hide Loyalty Cards
- Re-order and show/hide loyalty cards in the Quick Access Wallet
+ Manage Account & Loyalty Cards
+ Login, re-order and show/hide Loyalty Cards in the Quick Access Wallet
+
+ Logout of account
+ Delete stored Loyalty Cards data and current account
+ Sign in required
+ To use Loyalty Cards you need to log into your Google Account using the regular account flow to use Google Wallet integration. This is required as the API is not accessible using the regular android account sign in.\n\nYour Wallet data is kept encrypted on your device and never transmitted, nor is it included in backups, for your privacy and security.
+
Show Loyalty Card Preview
Show a preview of the Loyalty Card to be scanned or viewed in store without opening Google Wallet
Allow Loyalty Card access while locked
@@ -103,8 +109,8 @@
Allow interaction with Device Controls while locked. This does not allow adding or editing controls while locked
Wallpaper Color Picker unavailable for the currently selected wallpaper
Loading Loyalty Cards…
- No Loyalty Cards found
- Error loading Loyalty Cards
+ No Loyalty Cards loaded. \n Press the sync button to load Loyalty Cards from Google Wallet!
+ Error loading Loyalty Cards
Content Creator Mode
Enable this to always blur your Payment Card numbers, and show dummy codes for Loyalty Cards in the preview (enable that separately if you want it always on). This is useful for taking screenshots or videos without accidentally exposing your card info.\nWarning: Don\'t try to use a Loyalty Card with this mode enabled, it may cause errors on stores\' systems.
Accessibility Service Not Enabled
@@ -121,6 +127,7 @@
GitHub
XDA Thread
+ Sign in with Google
Open source libraries
Re-run setup
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..553d973
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..cd9d804
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 494009f..da95bf4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,6 +3,7 @@ buildscript {
ext.nav_version = "2.7.7"
ext.koin_version = "3.5.3"
ext.protobuf_version = '0.9.1'
+ ext.room_version = "2.6.1"
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
classpath "com.google.android.gms:oss-licenses-plugin:0.10.6"
@@ -14,6 +15,7 @@ plugins {
id 'com.android.application' version '8.5.2' apply false
id 'com.android.library' version '8.5.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
+ id 'com.google.devtools.ksp' version '1.9.10-1.0.13' apply false
}
task clean(type: Delete) {