- Add support for importing accounts from backup

- Switch export format to JSONL
- Update ktlint to 14.0.1 and apply formatting changes
This commit is contained in:
greenart7c3
2025-11-26 11:09:35 -03:00
parent 8dd4c65cee
commit fd2e1615ea
40 changed files with 882 additions and 795 deletions
@@ -59,7 +59,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class Amber : Application(), LifecycleObserver {
class Amber :
Application(),
LifecycleObserver {
private var mainActivityRef: WeakReference<MainActivity?>? = null
val crashReportCache: CrashReportCache by lazy { CrashReportCache(this.applicationContext) }
@@ -68,9 +70,7 @@ class Amber : Application(), LifecycleObserver {
mainActivityRef = WeakReference<MainActivity?>(activity)
}
fun getMainActivity(): MainActivity? {
return if (mainActivityRef != null) mainActivityRef!!.get() else null
}
fun getMainActivity(): MainActivity? = if (mainActivityRef != null) mainActivityRef!!.get() else null
// Exists to avoid exceptions stopping the coroutine
val exceptionHandler =
@@ -437,27 +437,25 @@ class Amber : Application(), LifecycleObserver {
}
}
fun isPrivateIp(url: String): Boolean {
return url.contains("127.0.0.1") ||
url.contains("localhost") ||
url.contains("192.168.") ||
url.contains("172.16.") ||
url.contains("172.17.") ||
url.contains("172.18.") ||
url.contains("172.19.") ||
url.contains("172.20.") ||
url.contains("172.21.") ||
url.contains("172.22.") ||
url.contains("172.23.") ||
url.contains("172.24.") ||
url.contains("172.25.") ||
url.contains("172.26.") ||
url.contains("172.27.") ||
url.contains("172.28.") ||
url.contains("172.29.") ||
url.contains("172.30.") ||
url.contains("172.31.")
}
fun isPrivateIp(url: String): Boolean = url.contains("127.0.0.1") ||
url.contains("localhost") ||
url.contains("192.168.") ||
url.contains("172.16.") ||
url.contains("172.17.") ||
url.contains("172.18.") ||
url.contains("172.19.") ||
url.contains("172.20.") ||
url.contains("172.21.") ||
url.contains("172.22.") ||
url.contains("172.23.") ||
url.contains("172.24.") ||
url.contains("172.25.") ||
url.contains("172.26.") ||
url.contains("172.27.") ||
url.contains("172.28.") ||
url.contains("172.29.") ||
url.contains("172.30.") ||
url.contains("172.31.")
override fun onTerminate() {
super.onTerminate()
@@ -17,13 +17,11 @@ import kotlinx.coroutines.flow.first
object DataStoreAccess {
private val storeCache = ConcurrentHashMap<String, DataStore<Preferences>>()
private fun getDataStore(context: Context, npub: String): DataStore<Preferences> {
return storeCache.computeIfAbsent(npub) {
Log.d(Amber.TAG, "Creating new DataStore for $npub")
PreferenceDataStoreFactory.create(
produceFile = { context.applicationContext.preferencesDataStoreFile("secure_datastore_$npub") },
)
}
private fun getDataStore(context: Context, npub: String): DataStore<Preferences> = storeCache.computeIfAbsent(npub) {
Log.d(Amber.TAG, "Creating new DataStore for $npub")
PreferenceDataStoreFactory.create(
produceFile = { context.applicationContext.preferencesDataStoreFile("secure_datastore_$npub") },
)
}
val NOSTR_PRIVKEY = stringPreferencesKey("nostr_privkey")
@@ -9,9 +9,7 @@ import androidx.security.crypto.MasterKey
object EncryptedStorage {
private const val PREFERENCES_NAME = "secret_keeper"
private fun prefsFileName(npub: String? = null): String {
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
}
private fun prefsFileName(npub: String? = null): String = if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
fun preferences(npub: String? = null, context: Context): EncryptedSharedPreferences {
val masterKey: MasterKey =
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.Immutable
import androidx.core.content.edit
import com.greenart7c3.nostrsigner.models.Account
@@ -18,6 +17,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.utils.cache.LargeCache
import java.io.File
import kotlin.collections.component1
import kotlin.collections.component2
@@ -67,25 +67,21 @@ data class AccountInfo(
object LocalPreferences {
private const val COMMA = ","
private var currentAccount: String? = null
private var accountCache = LruCache<String, Account>(10)
private var accountCache = LargeCache<String, Account>()
fun allLegacySavedAccounts(context: Context): List<AccountInfo> {
return legacySavedAccounts(context).map { npub ->
AccountInfo(
npub,
true,
)
}.toSet().toList()
}
fun allLegacySavedAccounts(context: Context): List<AccountInfo> = legacySavedAccounts(context).map { npub ->
AccountInfo(
npub,
true,
)
}.toSet().toList()
fun allSavedAccounts(context: Context): List<AccountInfo> {
return savedAccounts(context).map { npub ->
AccountInfo(
npub,
true,
)
}.toSet().toList()
}
fun allSavedAccounts(context: Context): List<AccountInfo> = savedAccounts(context).map { npub ->
AccountInfo(
npub,
true,
)
}.toSet().toList()
fun currentAccount(context: Context): String? {
if (currentAccount == null) {
@@ -144,9 +140,7 @@ object LocalPreferences {
}
}
fun getBatteryOptimization(context: Context): Boolean {
return sharedPrefs(context).getBoolean(SettingsKeys.BATERRY_OPTIMIZATION.key, false)
}
fun getBatteryOptimization(context: Context): Boolean = sharedPrefs(context).getBoolean(SettingsKeys.BATERRY_OPTIMIZATION.key, false)
fun updateBatteryOptimization(context: Context, value: Boolean) {
sharedPrefs(context).edit {
@@ -194,7 +188,7 @@ object LocalPreferences {
val context = Amber.instance
currentAccount = null
savedAccounts = null
accountCache.evictAll()
accountCache.clear()
allSavedAccounts(context).forEach {
loadFromEncryptedStorage(context, it.npub)
}
@@ -229,10 +223,8 @@ object LocalPreferences {
private var savedAccounts: List<String>? = null
@Suppress("DEPRECATION")
private fun legacySavedAccounts(context: Context): List<String> {
return encryptedPreferences(context)
.getString(SettingsKeys.SAVED_ACCOUNTS.key, null)?.split(COMMA) ?: listOf()
}
private fun legacySavedAccounts(context: Context): List<String> = encryptedPreferences(context)
.getString(SettingsKeys.SAVED_ACCOUNTS.key, null)?.split(COMMA) ?: listOf()
private fun savedAccounts(context: Context): List<String> {
if (savedAccounts == null) {
@@ -254,9 +246,7 @@ object LocalPreferences {
}
}
private fun getDirPath(context: Context): String {
return "${context.filesDir.parent}/shared_prefs/"
}
private fun getDirPath(context: Context): String = "${context.filesDir.parent}/shared_prefs/"
private fun addAccount(context: Context, npub: String) {
val accounts = savedAccounts(context).toMutableList()
@@ -275,9 +265,7 @@ object LocalPreferences {
updateCurrentAccount(context, npub)
}
fun containsAccount(context: Context, npub: String): Boolean {
return savedAccounts(context).contains(npub)
}
fun containsAccount(context: Context, npub: String): Boolean = savedAccounts(context).contains(npub)
/**
* Removes the account from the app level shared preferences
@@ -349,9 +337,7 @@ object LocalPreferences {
private fun encryptedPreferences(
context: Context,
npub: String? = null,
): SharedPreferences {
return EncryptedStorage.preferences(npub, context)
}
): SharedPreferences = EncryptedStorage.preferences(npub, context)
private fun sharedPrefs(
context: Context,
@@ -451,9 +437,9 @@ object LocalPreferences {
}
if (npub == null) {
val currentAccount = currentAccount(context) ?: return null
return accountCache[currentAccount]
return accountCache.get(currentAccount)
}
return accountCache[npub]
return accountCache.get(npub)
}
fun setAccountName(
@@ -52,8 +52,7 @@ import java.time.Instant
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
fun Intent.isLaunchFromHistory(): Boolean =
this.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
fun Intent.isLaunchFromHistory(): Boolean = this.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
class MainActivity : AppCompatActivity() {
private lateinit var mainViewModel: MainViewModel
@@ -84,6 +84,5 @@ object SecureCryptoHelper {
}
}
fun Context.hasStrongBox(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
fun Context.hasStrongBox(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
@@ -30,24 +30,16 @@ class SignerProvider : ContentProvider() {
uri: Uri,
selection: String?,
selectionArgs: Array<String>?,
): Int {
return 0
}
): Int = 0
override fun getType(uri: Uri): String? {
return null
}
override fun getType(uri: Uri): String? = null
override fun insert(
uri: Uri,
values: ContentValues?,
): Uri? {
return null
}
): Uri? = null
override fun onCreate(): Boolean {
return true
}
override fun onCreate(): Boolean = true
override fun query(
uri: Uri,
@@ -251,8 +243,7 @@ class SignerProvider : ContentProvider() {
MatrixCursor(arrayOf("signature", "event", "result")).also {
val signature =
if (event.kind == LnZapRequestEvent.KIND &&
event.tags.any {
tag ->
event.tags.any { tag ->
tag.any { t -> t == "anon" }
}
) {
@@ -553,7 +544,5 @@ class SignerProvider : ContentProvider() {
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?,
): Int {
return 0
}
): Int = 0
}
@@ -163,39 +163,37 @@ abstract class AppDatabase : RoomDatabase() {
fun getDatabase(
context: Context,
npub: String,
): AppDatabase {
return synchronized(this) {
val executor = Executors.newCachedThreadPool()
val transactionExecutor = Executors.newCachedThreadPool()
): AppDatabase = synchronized(this) {
val executor = Executors.newCachedThreadPool()
val transactionExecutor = Executors.newCachedThreadPool()
val instance =
Room.databaseBuilder(
context,
AppDatabase::class.java,
"amber_db_$npub",
)
.setQueryExecutor(executor)
.setTransactionExecutor(transactionExecutor)
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4)
.addMigrations(MIGRATION_4_5)
.addMigrations(MIGRATION_5_6)
.addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.addMigrations(MIGRATION_8_9)
.addMigrations(MIGRATION_9_10)
.addMigrations(MIGRATION_10_11)
.addMigrations(MIGRATION_11_12)
.addMigrations(MIGRATION_12_13)
.addMigrations(MIGRATION_13_14)
.addMigrations(MIGRATION_14_15)
.addMigrations(MIGRATION_15_16)
.build()
instance.openHelper.writableDatabase.execSQL("VACUUM")
val instance =
Room.databaseBuilder(
context,
AppDatabase::class.java,
"amber_db_$npub",
)
.setQueryExecutor(executor)
.setTransactionExecutor(transactionExecutor)
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4)
.addMigrations(MIGRATION_4_5)
.addMigrations(MIGRATION_5_6)
.addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.addMigrations(MIGRATION_8_9)
.addMigrations(MIGRATION_9_10)
.addMigrations(MIGRATION_10_11)
.addMigrations(MIGRATION_11_12)
.addMigrations(MIGRATION_12_13)
.addMigrations(MIGRATION_13_14)
.addMigrations(MIGRATION_14_15)
.addMigrations(MIGRATION_15_16)
.build()
instance.openHelper.writableDatabase.execSQL("VACUUM")
instance
}
instance
}
}
}
@@ -61,9 +61,7 @@ data class ApplicationEntity(
)
}
fun shouldShowRelays(): Boolean {
return (secret.isNotEmpty() || relays.isNotEmpty()) && !isConnected
}
fun shouldShowRelays(): Boolean = (secret.isNotEmpty() || relays.isNotEmpty()) && !isConnected
}
data class ApplicationWithPermissions(
@@ -87,9 +85,7 @@ class Converters {
}
@TypeConverter
fun toString(relays: List<NormalizedRelayUrl>): String {
return relays.joinToString(separator = ",") {
it.url
}
fun toString(relays: List<NormalizedRelayUrl>): String = relays.joinToString(separator = ",") {
it.url
}
}
@@ -21,24 +21,22 @@ abstract class HistoryDatabase : RoomDatabase() {
fun getDatabase(
context: Context,
npub: String,
): HistoryDatabase {
return synchronized(this) {
val executor = Executors.newCachedThreadPool()
val transactionExecutor = Executors.newCachedThreadPool()
): HistoryDatabase = synchronized(this) {
val executor = Executors.newCachedThreadPool()
val transactionExecutor = Executors.newCachedThreadPool()
val instance =
Room.databaseBuilder(
context,
HistoryDatabase::class.java,
"history_db_$npub",
)
.setQueryExecutor(executor)
.setTransactionExecutor(transactionExecutor)
.build()
instance.openHelper.writableDatabase.execSQL("VACUUM")
val instance =
Room.databaseBuilder(
context,
HistoryDatabase::class.java,
"history_db_$npub",
)
.setQueryExecutor(executor)
.setTransactionExecutor(transactionExecutor)
.build()
instance.openHelper.writableDatabase.execSQL("VACUUM")
instance
}
instance
}
}
}
@@ -21,24 +21,22 @@ abstract class LogDatabase : RoomDatabase() {
fun getDatabase(
context: Context,
npub: String,
): LogDatabase {
return synchronized(this) {
val executor = Executors.newCachedThreadPool()
val transactionExecutor = Executors.newCachedThreadPool()
): LogDatabase = synchronized(this) {
val executor = Executors.newCachedThreadPool()
val transactionExecutor = Executors.newCachedThreadPool()
val instance =
Room.databaseBuilder(
context,
LogDatabase::class.java,
"log_db_$npub",
)
.setQueryExecutor(executor)
.setTransactionExecutor(transactionExecutor)
.build()
instance.openHelper.writableDatabase.execSQL("VACUUM")
val instance =
Room.databaseBuilder(
context,
LogDatabase::class.java,
"log_db_$npub",
)
.setQueryExecutor(executor)
.setTransactionExecutor(transactionExecutor)
.build()
instance.openHelper.writableDatabase.execSQL("VACUUM")
instance
}
instance
}
}
}
@@ -27,9 +27,7 @@ class Account(
suspend fun createAuthEvent(
relayUrl: NormalizedRelayUrl,
challenge: String,
): RelayAuthEvent {
return RelayAuthEvent.create(relayUrl, challenge, signer)
}
): RelayAuthEvent = RelayAuthEvent.create(relayUrl, challenge, signer)
}
class AccountLiveData(account: Account) : LiveData<AccountState>(AccountState(account))
@@ -2,6 +2,7 @@ package com.greenart7c3.nostrsigner.models
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
@@ -10,12 +11,16 @@ import com.fasterxml.jackson.module.kotlin.readValue
*/
@Immutable
data class AccountExportData(
@param:JsonProperty("version")
val version: String = "1.0",
@param:JsonProperty("exportDate")
val exportDate: Long = System.currentTimeMillis() / 1000,
@param:JsonProperty("npub")
val npub: String,
@param:JsonProperty("name")
val name: String,
@param:JsonProperty("nsec")
val nsec: String?,
val nsec: String,
@param:JsonProperty("seedWords")
val seedWords: String,
@param:JsonProperty("signPolicy")
@@ -24,31 +29,13 @@ data class AccountExportData(
val picture: String?,
@param:JsonProperty("didBackup")
val didBackup: Boolean,
)
/**
* Container for bulk account export with versioning for future compatibility
*/
@Immutable
data class BulkAccountExport(
@param:JsonProperty("version")
val version: String = "1.0",
@param:JsonProperty("exportDate")
val exportDate: Long = System.currentTimeMillis() / 1000,
@param:JsonProperty("accountCount")
val accountCount: Int,
@param:JsonProperty("accounts")
val accounts: List<AccountExportData>,
) {
companion object {
private val mapper = jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
fun toJson(export: BulkAccountExport): String {
return mapper.writeValueAsString(export)
}
fun toJson(export: AccountExportData): String = mapper.writeValueAsString(export)
fun fromJson(json: String): BulkAccountExport {
return mapper.readValue(json)
}
fun fromJson(json: String): AccountExportData = mapper.readValue(json)
}
}
@@ -24,22 +24,18 @@ data class BunkerMetadata(
.addDeserializer(BunkerMetadata::class.java, BunkerMetadataDeserializer()),
)
fun fromJson(jsonObject: JsonNode): BunkerMetadata {
return BunkerMetadata(
name = jsonObject.get("name")?.asText()?.intern() ?: "",
url = jsonObject.get("url")?.asText()?.intern() ?: "",
description = jsonObject.get("description")?.asText()?.intern() ?: "",
perms = jsonObject.get("perms")?.asText()?.intern() ?: "",
)
}
fun fromJson(jsonObject: JsonNode): BunkerMetadata = BunkerMetadata(
name = jsonObject.get("name")?.asText()?.intern() ?: "",
url = jsonObject.get("url")?.asText()?.intern() ?: "",
description = jsonObject.get("description")?.asText()?.intern() ?: "",
perms = jsonObject.get("perms")?.asText()?.intern() ?: "",
)
private class BunkerMetadataDeserializer : StdDeserializer<BunkerMetadata>(BunkerMetadata::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): BunkerMetadata {
return fromJson(jp.codec.readTree(jp))
}
): BunkerMetadata = fromJson(jp.codec.readTree(jp))
}
}
}
@@ -45,11 +45,10 @@ data class Permission(
private class PermissionManualDeserializer {
companion object {
fun fromJson(jsonObject: JsonNode): Permission =
Permission(
type = jsonObject.get("type").asText().intern(),
kind = jsonObject.get("kind").asText()?.toIntOrNull(),
)
fun fromJson(jsonObject: JsonNode): Permission = Permission(
type = jsonObject.get("type").asText().intern(),
kind = jsonObject.get("kind").asText()?.toIntOrNull(),
)
}
}
@@ -67,256 +66,252 @@ data class Permission(
}
// LAST_COMMIT_CHECKED = "14ec14dac92d891715239bfb83ed1c9bdf2ac382"
fun toLocalizedString(context: Context, shouldTranslateConnect: Boolean = false): String {
return when (type.trim()) {
"sign_message" -> context.getString(R.string.sign_message)
"get_public_key" -> context.getString(R.string.read_your_public_key)
"nip04_encrypt" -> {
context.getString(R.string.encrypt_data_using_nip_4)
}
"nip04_decrypt" -> {
context.getString(R.string.decrypt_data_using_nip_4)
}
"nip44_decrypt" -> {
context.getString(R.string.decrypt_data_using_nip_44)
}
"nip44_encrypt" -> {
context.getString(R.string.encrypt_data_using_nip_44)
}
"decrypt_zap_event" -> {
context.getString(R.string.decrypt_private_zaps)
}
"sign_event" -> {
when (kind) {
0 -> context.getString(R.string.event_kind_0)
1 -> context.getString(R.string.event_kind_1)
3 -> context.getString(R.string.event_kind_3)
4 -> context.getString(R.string.event_kind_4)
5 -> context.getString(R.string.event_kind_5)
6 -> context.getString(R.string.event_kind_6)
7 -> context.getString(R.string.event_kind_7)
8 -> context.getString(R.string.event_kind_8)
9 -> context.getString(R.string.event_kind_9)
10 -> context.getString(R.string.event_kind_10)
11 -> context.getString(R.string.event_kind_11)
12 -> context.getString(R.string.event_kind_12)
13 -> context.getString(R.string.event_kind_13)
14 -> context.getString(R.string.event_kind_14)
16 -> context.getString(R.string.event_kind_16)
17 -> context.getString(R.string.event_kind_17)
20 -> context.getString(R.string.event_kind_20)
40 -> context.getString(R.string.event_kind_40)
41 -> context.getString(R.string.event_kind_41)
42 -> context.getString(R.string.event_kind_42)
43 -> context.getString(R.string.event_kind_43)
44 -> context.getString(R.string.event_kind_44)
64 -> context.getString(R.string.event_kind_64)
443 -> context.getString(R.string.event_kind_443)
444 -> context.getString(R.string.event_kind_444)
445 -> context.getString(R.string.event_kind_445)
818 -> context.getString(R.string.event_kind_818)
1018 -> context.getString(R.string.event_kind_1018)
1021 -> context.getString(R.string.event_kind_1021)
1022 -> context.getString(R.string.event_kind_1022)
1040 -> context.getString(R.string.event_kind_1040)
1059 -> context.getString(R.string.event_kind_1059)
1063 -> context.getString(R.string.event_kind_1063)
1068 -> context.getString(R.string.event_kind_1068)
1111 -> context.getString(R.string.event_kind_1111)
1311 -> context.getString(R.string.event_kind_1311)
1617 -> context.getString(R.string.event_kind_1617)
1621 -> context.getString(R.string.event_kind_1621)
1622 -> context.getString(R.string.event_kind_1622)
in 1630..1633 -> context.getString(R.string.event_kind_1630_1633)
1971 -> context.getString(R.string.event_kind_1971)
1984 -> context.getString(R.string.event_kind_1984)
1985 -> context.getString(R.string.event_kind_1985)
1986 -> context.getString(R.string.event_kind_1986)
1987 -> context.getString(R.string.event_kind_1987)
2003 -> context.getString(R.string.event_kind_2003)
2004 -> context.getString(R.string.event_kind_2004)
2022 -> context.getString(R.string.event_kind_2022)
4550 -> context.getString(R.string.event_kind_4550)
in 5000..5999 -> context.getString(R.string.event_kind_5000_5999)
in 6000..6999 -> context.getString(R.string.event_kind_6000_6999)
7000 -> context.getString(R.string.event_kind_7000)
7374 -> context.getString(R.string.event_kind_7374)
7375 -> context.getString(R.string.event_kind_7375)
7376 -> context.getString(R.string.event_kind_7376)
9000 -> context.getString(R.string.event_kind_9000)
9001 -> context.getString(R.string.event_kind_9001)
9002 -> context.getString(R.string.event_kind_9002)
9003 -> context.getString(R.string.event_kind_9003)
9004 -> context.getString(R.string.event_kind_9004)
9005 -> context.getString(R.string.event_kind_9005)
9006 -> context.getString(R.string.event_kind_9006)
9007 -> context.getString(R.string.event_kind_9007)
9021 -> context.getString(R.string.event_kind_9021)
in 9000..9030 -> context.getString(R.string.event_kind_9000_9030)
9041 -> context.getString(R.string.event_kind_9041)
9321 -> context.getString(R.string.event_kind_9321)
9467 -> context.getString(R.string.event_kind_9467)
9734 -> context.getString(R.string.event_kind_9734)
9735 -> context.getString(R.string.event_kind_9735)
9802 -> context.getString(R.string.event_kind_9802)
10000 -> context.getString(R.string.event_kind_10000)
10001 -> context.getString(R.string.event_kind_10001)
10002 -> context.getString(R.string.event_kind_10002)
10003 -> context.getString(R.string.event_kind_10003)
10004 -> context.getString(R.string.event_kind_10004)
10005 -> context.getString(R.string.event_kind_10005)
10006 -> context.getString(R.string.event_kind_10006)
10007 -> context.getString(R.string.event_kind_10007)
10009 -> context.getString(R.string.event_kind_10009)
10013 -> context.getString(R.string.event_kind_10013)
10015 -> context.getString(R.string.event_kind_10015)
10020 -> context.getString(R.string.event_kind_10020)
10030 -> context.getString(R.string.event_kind_10030)
10050 -> context.getString(R.string.event_kind_10050)
10051 -> context.getString(R.string.event_kind_10051)
10063 -> context.getString(R.string.event_kind_10063)
10096 -> context.getString(R.string.event_kind_10096)
13194 -> context.getString(R.string.event_kind_13194)
21000 -> context.getString(R.string.event_kind_21000)
22242 -> context.getString(R.string.event_kind_22242)
23194 -> context.getString(R.string.event_kind_23194)
23195 -> context.getString(R.string.event_kind_23195)
24133 -> context.getString(R.string.event_kind_24133)
24242 -> context.getString(R.string.event_kind_24242)
27235 -> context.getString(R.string.event_kind_27235)
22456 -> context.getString(R.string.event_kind_22456)
30000 -> context.getString(R.string.event_kind_30000)
30001 -> context.getString(R.string.event_kind_30001)
30002 -> context.getString(R.string.event_kind_30002)
30003 -> context.getString(R.string.event_kind_30003)
30004 -> context.getString(R.string.event_kind_30004)
30008 -> context.getString(R.string.event_kind_30008)
30009 -> context.getString(R.string.event_kind_30009)
30015 -> context.getString(R.string.event_kind_30015)
30017 -> context.getString(R.string.event_kind_30017)
30018 -> context.getString(R.string.event_kind_30018)
30019 -> context.getString(R.string.event_kind_30019)
30020 -> context.getString(R.string.event_kind_30020)
30023 -> context.getString(R.string.event_kind_30023)
30024 -> context.getString(R.string.event_kind_30024)
30030 -> context.getString(R.string.event_kind_30030)
30040 -> context.getString(R.string.event_kind_30040)
30041 -> context.getString(R.string.event_kind_30041)
30063 -> context.getString(R.string.event_kind_30063)
30078 -> context.getString(R.string.event_kind_30078)
30311 -> context.getString(R.string.event_kind_30311)
30315 -> context.getString(R.string.event_kind_30315)
30402 -> context.getString(R.string.event_kind_30402)
30403 -> context.getString(R.string.event_kind_30403)
31234 -> context.getString(R.string.event_kind_31234)
31922 -> context.getString(R.string.event_kind_31922)
31923 -> context.getString(R.string.event_kind_31923)
31924 -> context.getString(R.string.event_kind_31924)
31925 -> context.getString(R.string.event_kind_31925)
31989 -> context.getString(R.string.event_kind_31989)
31990 -> context.getString(R.string.event_kind_31990)
34235 -> context.getString(R.string.event_kind_34235)
34236 -> context.getString(R.string.event_kind_34236)
34550 -> context.getString(R.string.event_kind_34550)
37375 -> context.getString(R.string.event_kind_37375)
38383 -> context.getString(R.string.event_kind_38383)
39000 -> context.getString(R.string.event_kind_39000)
39001 -> context.getString(R.string.event_kind_39001)
39002 -> context.getString(R.string.event_kind_39002)
in 39003..39009 -> context.getString(R.string.event_kind_39000_39009)
39089 -> context.getString(R.string.event_kind_39089)
39092 -> context.getString(R.string.event_kind_39092)
10000300 -> context.getString(R.string.event_kind_10000300)
else -> {
val nipDescription = nipToLocalizedString(context, this, true)
nipDescription.ifEmpty {
context.getString(R.string.event_kind, kind.toString())
}
fun toLocalizedString(context: Context, shouldTranslateConnect: Boolean = false): String = when (type.trim()) {
"sign_message" -> context.getString(R.string.sign_message)
"get_public_key" -> context.getString(R.string.read_your_public_key)
"nip04_encrypt" -> {
context.getString(R.string.encrypt_data_using_nip_4)
}
"nip04_decrypt" -> {
context.getString(R.string.decrypt_data_using_nip_4)
}
"nip44_decrypt" -> {
context.getString(R.string.decrypt_data_using_nip_44)
}
"nip44_encrypt" -> {
context.getString(R.string.encrypt_data_using_nip_44)
}
"decrypt_zap_event" -> {
context.getString(R.string.decrypt_private_zaps)
}
"sign_event" -> {
when (kind) {
0 -> context.getString(R.string.event_kind_0)
1 -> context.getString(R.string.event_kind_1)
3 -> context.getString(R.string.event_kind_3)
4 -> context.getString(R.string.event_kind_4)
5 -> context.getString(R.string.event_kind_5)
6 -> context.getString(R.string.event_kind_6)
7 -> context.getString(R.string.event_kind_7)
8 -> context.getString(R.string.event_kind_8)
9 -> context.getString(R.string.event_kind_9)
10 -> context.getString(R.string.event_kind_10)
11 -> context.getString(R.string.event_kind_11)
12 -> context.getString(R.string.event_kind_12)
13 -> context.getString(R.string.event_kind_13)
14 -> context.getString(R.string.event_kind_14)
16 -> context.getString(R.string.event_kind_16)
17 -> context.getString(R.string.event_kind_17)
20 -> context.getString(R.string.event_kind_20)
40 -> context.getString(R.string.event_kind_40)
41 -> context.getString(R.string.event_kind_41)
42 -> context.getString(R.string.event_kind_42)
43 -> context.getString(R.string.event_kind_43)
44 -> context.getString(R.string.event_kind_44)
64 -> context.getString(R.string.event_kind_64)
443 -> context.getString(R.string.event_kind_443)
444 -> context.getString(R.string.event_kind_444)
445 -> context.getString(R.string.event_kind_445)
818 -> context.getString(R.string.event_kind_818)
1018 -> context.getString(R.string.event_kind_1018)
1021 -> context.getString(R.string.event_kind_1021)
1022 -> context.getString(R.string.event_kind_1022)
1040 -> context.getString(R.string.event_kind_1040)
1059 -> context.getString(R.string.event_kind_1059)
1063 -> context.getString(R.string.event_kind_1063)
1068 -> context.getString(R.string.event_kind_1068)
1111 -> context.getString(R.string.event_kind_1111)
1311 -> context.getString(R.string.event_kind_1311)
1617 -> context.getString(R.string.event_kind_1617)
1621 -> context.getString(R.string.event_kind_1621)
1622 -> context.getString(R.string.event_kind_1622)
in 1630..1633 -> context.getString(R.string.event_kind_1630_1633)
1971 -> context.getString(R.string.event_kind_1971)
1984 -> context.getString(R.string.event_kind_1984)
1985 -> context.getString(R.string.event_kind_1985)
1986 -> context.getString(R.string.event_kind_1986)
1987 -> context.getString(R.string.event_kind_1987)
2003 -> context.getString(R.string.event_kind_2003)
2004 -> context.getString(R.string.event_kind_2004)
2022 -> context.getString(R.string.event_kind_2022)
4550 -> context.getString(R.string.event_kind_4550)
in 5000..5999 -> context.getString(R.string.event_kind_5000_5999)
in 6000..6999 -> context.getString(R.string.event_kind_6000_6999)
7000 -> context.getString(R.string.event_kind_7000)
7374 -> context.getString(R.string.event_kind_7374)
7375 -> context.getString(R.string.event_kind_7375)
7376 -> context.getString(R.string.event_kind_7376)
9000 -> context.getString(R.string.event_kind_9000)
9001 -> context.getString(R.string.event_kind_9001)
9002 -> context.getString(R.string.event_kind_9002)
9003 -> context.getString(R.string.event_kind_9003)
9004 -> context.getString(R.string.event_kind_9004)
9005 -> context.getString(R.string.event_kind_9005)
9006 -> context.getString(R.string.event_kind_9006)
9007 -> context.getString(R.string.event_kind_9007)
9021 -> context.getString(R.string.event_kind_9021)
in 9000..9030 -> context.getString(R.string.event_kind_9000_9030)
9041 -> context.getString(R.string.event_kind_9041)
9321 -> context.getString(R.string.event_kind_9321)
9467 -> context.getString(R.string.event_kind_9467)
9734 -> context.getString(R.string.event_kind_9734)
9735 -> context.getString(R.string.event_kind_9735)
9802 -> context.getString(R.string.event_kind_9802)
10000 -> context.getString(R.string.event_kind_10000)
10001 -> context.getString(R.string.event_kind_10001)
10002 -> context.getString(R.string.event_kind_10002)
10003 -> context.getString(R.string.event_kind_10003)
10004 -> context.getString(R.string.event_kind_10004)
10005 -> context.getString(R.string.event_kind_10005)
10006 -> context.getString(R.string.event_kind_10006)
10007 -> context.getString(R.string.event_kind_10007)
10009 -> context.getString(R.string.event_kind_10009)
10013 -> context.getString(R.string.event_kind_10013)
10015 -> context.getString(R.string.event_kind_10015)
10020 -> context.getString(R.string.event_kind_10020)
10030 -> context.getString(R.string.event_kind_10030)
10050 -> context.getString(R.string.event_kind_10050)
10051 -> context.getString(R.string.event_kind_10051)
10063 -> context.getString(R.string.event_kind_10063)
10096 -> context.getString(R.string.event_kind_10096)
13194 -> context.getString(R.string.event_kind_13194)
21000 -> context.getString(R.string.event_kind_21000)
22242 -> context.getString(R.string.event_kind_22242)
23194 -> context.getString(R.string.event_kind_23194)
23195 -> context.getString(R.string.event_kind_23195)
24133 -> context.getString(R.string.event_kind_24133)
24242 -> context.getString(R.string.event_kind_24242)
27235 -> context.getString(R.string.event_kind_27235)
22456 -> context.getString(R.string.event_kind_22456)
30000 -> context.getString(R.string.event_kind_30000)
30001 -> context.getString(R.string.event_kind_30001)
30002 -> context.getString(R.string.event_kind_30002)
30003 -> context.getString(R.string.event_kind_30003)
30004 -> context.getString(R.string.event_kind_30004)
30008 -> context.getString(R.string.event_kind_30008)
30009 -> context.getString(R.string.event_kind_30009)
30015 -> context.getString(R.string.event_kind_30015)
30017 -> context.getString(R.string.event_kind_30017)
30018 -> context.getString(R.string.event_kind_30018)
30019 -> context.getString(R.string.event_kind_30019)
30020 -> context.getString(R.string.event_kind_30020)
30023 -> context.getString(R.string.event_kind_30023)
30024 -> context.getString(R.string.event_kind_30024)
30030 -> context.getString(R.string.event_kind_30030)
30040 -> context.getString(R.string.event_kind_30040)
30041 -> context.getString(R.string.event_kind_30041)
30063 -> context.getString(R.string.event_kind_30063)
30078 -> context.getString(R.string.event_kind_30078)
30311 -> context.getString(R.string.event_kind_30311)
30315 -> context.getString(R.string.event_kind_30315)
30402 -> context.getString(R.string.event_kind_30402)
30403 -> context.getString(R.string.event_kind_30403)
31234 -> context.getString(R.string.event_kind_31234)
31922 -> context.getString(R.string.event_kind_31922)
31923 -> context.getString(R.string.event_kind_31923)
31924 -> context.getString(R.string.event_kind_31924)
31925 -> context.getString(R.string.event_kind_31925)
31989 -> context.getString(R.string.event_kind_31989)
31990 -> context.getString(R.string.event_kind_31990)
34235 -> context.getString(R.string.event_kind_34235)
34236 -> context.getString(R.string.event_kind_34236)
34550 -> context.getString(R.string.event_kind_34550)
37375 -> context.getString(R.string.event_kind_37375)
38383 -> context.getString(R.string.event_kind_38383)
39000 -> context.getString(R.string.event_kind_39000)
39001 -> context.getString(R.string.event_kind_39001)
39002 -> context.getString(R.string.event_kind_39002)
in 39003..39009 -> context.getString(R.string.event_kind_39000_39009)
39089 -> context.getString(R.string.event_kind_39089)
39092 -> context.getString(R.string.event_kind_39092)
10000300 -> context.getString(R.string.event_kind_10000300)
else -> {
val nipDescription = nipToLocalizedString(context, this, true)
nipDescription.ifEmpty {
context.getString(R.string.event_kind, kind.toString())
}
}
}
"connect" -> if (shouldTranslateConnect) context.getString(R.string.connect) else context.getString(R.string.would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf)
"nip" -> {
nipToLocalizedString(context, this)
}
else -> type
}
"connect" -> if (shouldTranslateConnect) context.getString(R.string.connect) else context.getString(R.string.would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf)
"nip" -> {
nipToLocalizedString(context, this)
}
else -> type
}
}
fun nipToLocalizedString(context: Context, permission: Permission, shouldReturnEmpty: Boolean = false): String {
return when (permission.kind) {
1 -> context.getString(R.string.nip_01)
2 -> context.getString(R.string.nip_02)
3 -> context.getString(R.string.nip_03)
4 -> context.getString(R.string.nip_04)
5 -> context.getString(R.string.nip_05)
6 -> context.getString(R.string.nip_06)
7 -> context.getString(R.string.nip_07)
8 -> context.getString(R.string.nip_08)
9 -> context.getString(R.string.nip_09)
10 -> context.getString(R.string.nip_10)
11 -> context.getString(R.string.nip_11)
13 -> context.getString(R.string.nip_13)
14 -> context.getString(R.string.nip_14)
15 -> context.getString(R.string.nip_15)
17 -> context.getString(R.string.nip_17)
18 -> context.getString(R.string.nip_18)
19 -> context.getString(R.string.nip_19)
21 -> context.getString(R.string.nip_21)
23 -> context.getString(R.string.nip_23)
24 -> context.getString(R.string.nip_24)
25 -> context.getString(R.string.nip_25)
26 -> context.getString(R.string.nip_26)
27 -> context.getString(R.string.nip_27)
28 -> context.getString(R.string.nip_28)
29 -> context.getString(R.string.nip_29)
30 -> context.getString(R.string.nip_30)
31 -> context.getString(R.string.nip_31)
32 -> context.getString(R.string.nip_32)
34 -> context.getString(R.string.nip_34)
35 -> context.getString(R.string.nip_35)
36 -> context.getString(R.string.nip_36)
38 -> context.getString(R.string.nip_38)
39 -> context.getString(R.string.nip_39)
40 -> context.getString(R.string.nip_40)
42 -> context.getString(R.string.nip_42)
44 -> context.getString(R.string.nip_44)
45 -> context.getString(R.string.nip_45)
46 -> context.getString(R.string.nip_46)
47 -> context.getString(R.string.nip_47)
48 -> context.getString(R.string.nip_48)
49 -> context.getString(R.string.nip_49)
50 -> context.getString(R.string.nip_50)
51 -> context.getString(R.string.nip_51)
52 -> context.getString(R.string.nip_52)
53 -> context.getString(R.string.nip_53)
54 -> context.getString(R.string.nip_54)
55 -> context.getString(R.string.nip_55)
56 -> context.getString(R.string.nip_56)
57 -> context.getString(R.string.nip_57)
58 -> context.getString(R.string.nip_58)
59 -> context.getString(R.string.nip_59)
65 -> context.getString(R.string.nip_65)
70 -> context.getString(R.string.nip_70)
71 -> context.getString(R.string.nip_71)
72 -> context.getString(R.string.nip_72)
75 -> context.getString(R.string.nip_75)
78 -> context.getString(R.string.nip_78)
84 -> context.getString(R.string.nip_84)
89 -> context.getString(R.string.nip_89)
90 -> context.getString(R.string.nip_90)
92 -> context.getString(R.string.nip_92)
94 -> context.getString(R.string.nip_94)
96 -> context.getString(R.string.nip_96)
98 -> context.getString(R.string.nip_98)
99 -> context.getString(R.string.nip_99)
else -> {
if (shouldReturnEmpty) {
""
} else {
context.getString(R.string.nip_kind, permission.kind.toString())
}
fun nipToLocalizedString(context: Context, permission: Permission, shouldReturnEmpty: Boolean = false): String = when (permission.kind) {
1 -> context.getString(R.string.nip_01)
2 -> context.getString(R.string.nip_02)
3 -> context.getString(R.string.nip_03)
4 -> context.getString(R.string.nip_04)
5 -> context.getString(R.string.nip_05)
6 -> context.getString(R.string.nip_06)
7 -> context.getString(R.string.nip_07)
8 -> context.getString(R.string.nip_08)
9 -> context.getString(R.string.nip_09)
10 -> context.getString(R.string.nip_10)
11 -> context.getString(R.string.nip_11)
13 -> context.getString(R.string.nip_13)
14 -> context.getString(R.string.nip_14)
15 -> context.getString(R.string.nip_15)
17 -> context.getString(R.string.nip_17)
18 -> context.getString(R.string.nip_18)
19 -> context.getString(R.string.nip_19)
21 -> context.getString(R.string.nip_21)
23 -> context.getString(R.string.nip_23)
24 -> context.getString(R.string.nip_24)
25 -> context.getString(R.string.nip_25)
26 -> context.getString(R.string.nip_26)
27 -> context.getString(R.string.nip_27)
28 -> context.getString(R.string.nip_28)
29 -> context.getString(R.string.nip_29)
30 -> context.getString(R.string.nip_30)
31 -> context.getString(R.string.nip_31)
32 -> context.getString(R.string.nip_32)
34 -> context.getString(R.string.nip_34)
35 -> context.getString(R.string.nip_35)
36 -> context.getString(R.string.nip_36)
38 -> context.getString(R.string.nip_38)
39 -> context.getString(R.string.nip_39)
40 -> context.getString(R.string.nip_40)
42 -> context.getString(R.string.nip_42)
44 -> context.getString(R.string.nip_44)
45 -> context.getString(R.string.nip_45)
46 -> context.getString(R.string.nip_46)
47 -> context.getString(R.string.nip_47)
48 -> context.getString(R.string.nip_48)
49 -> context.getString(R.string.nip_49)
50 -> context.getString(R.string.nip_50)
51 -> context.getString(R.string.nip_51)
52 -> context.getString(R.string.nip_52)
53 -> context.getString(R.string.nip_53)
54 -> context.getString(R.string.nip_54)
55 -> context.getString(R.string.nip_55)
56 -> context.getString(R.string.nip_56)
57 -> context.getString(R.string.nip_57)
58 -> context.getString(R.string.nip_58)
59 -> context.getString(R.string.nip_59)
65 -> context.getString(R.string.nip_65)
70 -> context.getString(R.string.nip_70)
71 -> context.getString(R.string.nip_71)
72 -> context.getString(R.string.nip_72)
75 -> context.getString(R.string.nip_75)
78 -> context.getString(R.string.nip_78)
84 -> context.getString(R.string.nip_84)
89 -> context.getString(R.string.nip_89)
90 -> context.getString(R.string.nip_90)
92 -> context.getString(R.string.nip_92)
94 -> context.getString(R.string.nip_94)
96 -> context.getString(R.string.nip_96)
98 -> context.getString(R.string.nip_98)
99 -> context.getString(R.string.nip_99)
else -> {
if (shouldReturnEmpty) {
""
} else {
context.getString(R.string.nip_kind, permission.kind.toString())
}
}
}
@@ -37,13 +37,12 @@ class EncryptedBlobInterceptor(
return newBuilder().body(newBody).build()
}
private fun Response.decryptOrNull(cipher: NostrCipher): Response? =
try {
decrypt(cipher)
} catch (e: Exception) {
Log.w("EncryptedBlobInterceptor", "Failed to decrypt", e)
null
}
private fun Response.decryptOrNull(cipher: NostrCipher): Response? = try {
decrypt(cipher)
} catch (e: Exception) {
Log.w("EncryptedBlobInterceptor", "Failed to decrypt", e)
null
}
private fun Response.decryptOrNullWithErrorCorrection(cipher: NostrCipher): Response? {
return decryptOrNull(cipher) ?: return if (cipher is AESGCM) {
@@ -95,18 +95,17 @@ object HttpClientManager {
.build()
}
fun getHttpClient(useProxy: Boolean): OkHttpClient =
if (useProxy) {
if (defaultHttpClient == null) {
defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
}
defaultHttpClient!!
} else {
if (defaultHttpClientWithoutProxy == null) {
defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
}
defaultHttpClientWithoutProxy!!
fun getHttpClient(useProxy: Boolean): OkHttpClient = if (useProxy) {
if (defaultHttpClient == null) {
defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
}
defaultHttpClient!!
} else {
if (defaultHttpClientWithoutProxy == null) {
defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
}
defaultHttpClientWithoutProxy!!
}
fun setDefaultProxyOnPort(port: Int) {
setDefaultProxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port)))
@@ -1,17 +1,81 @@
package com.greenart7c3.nostrsigner.service
import android.content.Context
import android.net.Uri
import android.util.Log
import android.widget.Toast
import com.anggrayudi.storage.extension.toDocumentFile
import com.anggrayudi.storage.file.openInputStream
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.AccountExportData
import com.greenart7c3.nostrsigner.models.BulkAccountExport
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip19Bech32.toNsec
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49
import com.vitorpamplona.quartz.utils.Hex
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
object AccountExportService {
suspend fun importAccounts(
uri: Uri,
password: String,
onLoading: (isLoading: Boolean) -> Unit,
onText: (text: String) -> Unit,
onFinish: () -> Unit,
) {
onLoading(true)
try {
val file = uri.toDocumentFile(Amber.instance) ?: return
val name = file.name
if (name?.endsWith(".jsonl") == false) {
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
Toast.makeText(Amber.instance, "File is not a Amber backup", Toast.LENGTH_LONG).show()
}
return
}
if (file.isFile) {
file.openInputStream(Amber.instance).use { oi ->
oi?.bufferedReader().use { reader ->
reader?.readLines()?.forEachIndexed { index, line ->
try {
onText("Importing account ${index + 1}")
val accountData = AccountExportData.fromJson(line)
val nsec = Nip49().decrypt(accountData.nsec, password)
val signer = NostrSignerInternal(KeyPair(privKey = Hex.decode(nsec)))
val account = Account(
signer = signer,
name = MutableStateFlow(accountData.name),
picture = MutableStateFlow(accountData.picture ?: ""),
signPolicy = accountData.signPolicy,
seedWords = signer.signerSync.decrypt(accountData.seedWords, signer.keyPair.pubKey.toHexKey()).split(" ").toSet(),
didBackup = accountData.didBackup,
)
LocalPreferences.switchToAccount(Amber.instance, accountData.npub)
LocalPreferences.updatePrefsForLogin(Amber.instance, account)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("Amber", "Error importing account", e)
}
}
onFinish()
}
}
}
} finally {
onText("")
onLoading(false)
}
}
/**
* Export all accounts as an encrypted JSON file
*
@@ -33,27 +97,19 @@ object AccountExportService {
return@withContext Result.failure(Exception("No accounts to export"))
}
val exportedAccounts = mutableListOf<AccountExportData>()
var data = ""
accountInfos.forEachIndexed { index, accountInfo ->
onProgress(index + 1, totalAccounts)
val account = LocalPreferences.loadFromEncryptedStorage(context, accountInfo.npub)
account?.let {
val exportData = accountToExportData(it, password)
exportedAccounts.add(exportData)
data += "${AccountExportData.toJson(exportData)}\n"
}
}
val bulkExport = BulkAccountExport(
accountCount = exportedAccounts.size,
accounts = exportedAccounts,
)
val json = BulkAccountExport.toJson(bulkExport)
// Each private key is already encrypted with NIP-49 inside the JSON
Result.success(json)
Result.success(data)
} catch (e: Exception) {
Result.failure(e)
}
@@ -66,16 +122,7 @@ object AccountExportService {
val privKey = account.signer.keyPair.privKey
val encrypedSeedWords = if (account.seedWords.isNotEmpty()) account.signer.signerSync.nip44Encrypt(account.seedWords.joinToString(" "), account.hexKey) else ""
// Encrypt private key with password (NIP-49) or use nsec if no password
val encryptedNsec = if (privKey != null) {
if (password.isNotBlank()) {
Nip49().encrypt(privKey.toHexKey(), password)
} else {
privKey.toNsec()
}
} else {
null
}
val encryptedNsec = Nip49().encrypt(privKey!!.toHexKey(), password)
return AccountExportData(
npub = account.npub,
@@ -31,39 +31,37 @@ object AmberUtils {
type: SignerType,
account: Account,
pubKey: HexKey,
): String? {
return when (type) {
SignerType.DECRYPT_ZAP_EVENT -> {
decryptZapEvent(data, account)
}
SignerType.NIP04_DECRYPT -> {
Nip04.decrypt(
data,
account.signer.keyPair.privKey!!,
Hex.decode(pubKey),
)
}
SignerType.NIP04_ENCRYPT -> {
Nip04.encrypt(
data,
account.signer.keyPair.privKey!!,
Hex.decode(pubKey),
)
}
SignerType.NIP44_ENCRYPT -> {
Nip44.encrypt(
data,
account.signer.keyPair.privKey!!,
pubKey.hexToByteArray(),
).encodePayload()
}
else -> {
Nip44.decrypt(
data,
account.signer.keyPair.privKey!!,
pubKey.hexToByteArray(),
)
}
): String? = when (type) {
SignerType.DECRYPT_ZAP_EVENT -> {
decryptZapEvent(data, account)
}
SignerType.NIP04_DECRYPT -> {
Nip04.decrypt(
data,
account.signer.keyPair.privKey!!,
Hex.decode(pubKey),
)
}
SignerType.NIP04_ENCRYPT -> {
Nip04.encrypt(
data,
account.signer.keyPair.privKey!!,
Hex.decode(pubKey),
)
}
SignerType.NIP44_ENCRYPT -> {
Nip44.encrypt(
data,
account.signer.keyPair.privKey!!,
pubKey.hexToByteArray(),
).encodePayload()
}
else -> {
Nip44.decrypt(
data,
account.signer.keyPair.privKey!!,
pubKey.hexToByteArray(),
)
}
}
@@ -252,6 +250,4 @@ fun String.toShortenHex(): String {
fun isPrivateEvent(
kind: Int,
tags: Array<Array<String>>,
): Boolean {
return kind == LnZapRequestEvent.KIND && tags.any { t -> t.size > 1 && t[0] == "anon" }
}
): Boolean = kind == LnZapRequestEvent.KIND && tags.any { t -> t.size > 1 && t[0] == "anon" }
@@ -49,9 +49,7 @@ object BunkerRequestUtils {
state.tryEmit(state.value.filter { it.request.id != id })
}
fun getBunkerRequests(): List<AmberBunkerRequest> {
return state.value
}
fun getBunkerRequests(): List<AmberBunkerRequest> = state.value
suspend fun sendBunkerResponse(
context: Context,
@@ -192,34 +190,30 @@ object BunkerRequestUtils {
onLoading(false)
}
fun getTypeFromBunker(bunkerRequest: BunkerRequest): SignerType {
return when (bunkerRequest.method) {
"sign_message" -> SignerType.SIGN_MESSAGE
"connect" -> SignerType.CONNECT
"sign_event" -> SignerType.SIGN_EVENT
"get_public_key" -> SignerType.GET_PUBLIC_KEY
"nip04_encrypt" -> SignerType.NIP04_ENCRYPT
"nip04_decrypt" -> SignerType.NIP04_DECRYPT
"nip44_encrypt" -> SignerType.NIP44_ENCRYPT
"nip44_decrypt" -> SignerType.NIP44_DECRYPT
"decrypt_zap_event" -> SignerType.DECRYPT_ZAP_EVENT
"ping" -> SignerType.PING
else -> SignerType.INVALID
}
fun getTypeFromBunker(bunkerRequest: BunkerRequest): SignerType = when (bunkerRequest.method) {
"sign_message" -> SignerType.SIGN_MESSAGE
"connect" -> SignerType.CONNECT
"sign_event" -> SignerType.SIGN_EVENT
"get_public_key" -> SignerType.GET_PUBLIC_KEY
"nip04_encrypt" -> SignerType.NIP04_ENCRYPT
"nip04_decrypt" -> SignerType.NIP04_DECRYPT
"nip44_encrypt" -> SignerType.NIP44_ENCRYPT
"nip44_decrypt" -> SignerType.NIP44_DECRYPT
"decrypt_zap_event" -> SignerType.DECRYPT_ZAP_EVENT
"ping" -> SignerType.PING
else -> SignerType.INVALID
}
fun getDataFromBunker(bunkerRequest: BunkerRequest): String {
return when (bunkerRequest.method) {
"sign_message" -> bunkerRequest.params.first()
"connect" -> "ack"
"sign_event" -> {
val amberEvent = AmberEvent.fromJson(bunkerRequest.params.first())
AmberEvent.toEvent(amberEvent).toJson()
}
"nip04_encrypt", "nip04_decrypt", "nip44_encrypt", "nip44_decrypt", "decrypt_zap_event" -> bunkerRequest.params.getOrElse(1) { "" }
"ping" -> "pong"
else -> ""
fun getDataFromBunker(bunkerRequest: BunkerRequest): String = when (bunkerRequest.method) {
"sign_message" -> bunkerRequest.params.first()
"connect" -> "ack"
"sign_event" -> {
val amberEvent = AmberEvent.fromJson(bunkerRequest.params.first())
AmberEvent.toEvent(amberEvent).toJson()
}
"nip04_encrypt", "nip04_decrypt", "nip44_encrypt", "nip44_decrypt", "decrypt_zap_event" -> bunkerRequest.params.getOrElse(1) { "" }
"ping" -> "pong"
else -> ""
}
fun sendResult(
@@ -4,8 +4,7 @@ import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
class BunkerResponseWorker(appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
class BunkerResponseWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
TODO("Not yet implemented")
@@ -10,8 +10,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.TimeUtils.ONE_WEEK
import kotlin.coroutines.cancellation.CancellationException
class ClearLogsWorker(appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
class ClearLogsWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
LocalPreferences.allSavedAccounts(Amber.instance).forEach {
@@ -83,9 +83,7 @@ class ConnectivityService : Service() {
}
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onBind(intent: Intent): IBinder? = null
override fun onCreate() {
Log.d(Amber.TAG, "onCreate ConnectivityService isStarted: $isStarted")
@@ -513,7 +513,5 @@ class EventNotificationConsumer(private val applicationContext: Context) {
}
}
fun notificationManager(): NotificationManager {
return ContextCompat.getSystemService(applicationContext, NotificationManager::class.java) as NotificationManager
}
fun notificationManager(): NotificationManager = ContextCompat.getSystemService(applicationContext, NotificationManager::class.java) as NotificationManager
}
@@ -16,9 +16,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
import kotlinx.coroutines.launch
object NostrConnectUtils {
private fun metaDataFromJson(json: String): BunkerMetadata {
return BunkerMetadata.mapper.readValue(json, BunkerMetadata::class.java)
}
private fun metaDataFromJson(json: String): BunkerMetadata = BunkerMetadata.mapper.readValue(json, BunkerMetadata::class.java)
fun getIntentFromNostrConnect(
intent: Intent,
@@ -36,12 +36,11 @@ class CrashReportCache(
private fun deleteReport() = appContext.deleteFile(STACK_TRACE_FILENAME)
private fun inputStreamOrNull(): FileInputStream? =
try {
appContext.openFileInput(STACK_TRACE_FILENAME)
} catch (_: FileNotFoundException) {
null
}
private fun inputStreamOrNull(): FileInputStream? = try {
appContext.openFileInput(STACK_TRACE_FILENAME)
} catch (_: FileNotFoundException) {
null
}
fun writeReport(report: String) {
val trace = outputStream()
@@ -49,15 +48,14 @@ class CrashReportCache(
trace.close()
}
suspend fun loadAndDelete(): String? =
withContext(Dispatchers.IO) {
val stack =
inputStreamOrNull()?.use { inStream ->
InputStreamReader(inStream).use { reader ->
reader.readText()
}
suspend fun loadAndDelete(): String? = withContext(Dispatchers.IO) {
val stack =
inputStreamOrNull()?.use { inStream ->
InputStreamReader(inStream).use { reader ->
reader.readText()
}
deleteReport()
stack
}
}
deleteReport()
stack
}
}
@@ -24,70 +24,69 @@ import android.os.Build
import com.greenart7c3.nostrsigner.BuildConfig
class ReportAssembler {
fun buildReport(e: Throwable): String =
buildString {
append(e.javaClass.simpleName)
append(": ")
appendLine(BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR.uppercase())
appendLine()
fun buildReport(e: Throwable): String = buildString {
append(e.javaClass.simpleName)
append(": ")
appendLine(BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR.uppercase())
appendLine()
// Device and Product Information
appendLine("| Prop | Value |")
appendLine("|------|-------|")
append("| Manuf |")
append(Build.MANUFACTURER)
appendLine(" |")
append("| Model |")
append(Build.MODEL)
appendLine(" |")
append("| Prod |")
append(Build.PRODUCT)
appendLine(" |")
// Device and Product Information
appendLine("| Prop | Value |")
appendLine("|------|-------|")
append("| Manuf |")
append(Build.MANUFACTURER)
appendLine(" |")
append("| Model |")
append(Build.MODEL)
appendLine(" |")
append("| Prod |")
append(Build.PRODUCT)
appendLine(" |")
// OS Information
append("| Android |")
append(Build.VERSION.RELEASE)
appendLine(" |")
append("| SDK Int |")
append(Build.VERSION.SDK_INT.toString())
appendLine(" |")
// OS Information
append("| Android |")
append(Build.VERSION.RELEASE)
appendLine(" |")
append("| SDK Int |")
append(Build.VERSION.SDK_INT.toString())
appendLine(" |")
// Hardware Information
append("| Brand |")
append(Build.BRAND)
appendLine(" |")
append("| Hardware |")
append(Build.HARDWARE)
appendLine(" |")
// Hardware Information
append("| Brand |")
append(Build.BRAND)
appendLine(" |")
append("| Hardware |")
append(Build.HARDWARE)
appendLine(" |")
// Other Useful Information
append("| Device | ")
append(Build.DEVICE)
appendLine(" |")
append("| Host | ")
append(Build.HOST)
appendLine(" |")
append("| User | ")
append(Build.USER)
appendLine(" |")
appendLine()
// Other Useful Information
append("| Device | ")
append(Build.DEVICE)
appendLine(" |")
append("| Host | ")
append(Build.HOST)
appendLine(" |")
append("| User | ")
append(Build.USER)
appendLine(" |")
appendLine()
appendLine("```")
appendLine(e.toString())
e.stackTrace.forEach {
append(" ")
appendLine("```")
appendLine(e.toString())
e.stackTrace.forEach {
append(" ")
appendLine(it.toString())
}
val cause = e.cause
if (cause != null) {
appendLine("\n\nCause:")
append(" ")
appendLine(cause.toString())
cause.stackTrace.forEach {
append(" ")
appendLine(it.toString())
}
val cause = e.cause
if (cause != null) {
appendLine("\n\nCause:")
append(" ")
appendLine(cause.toString())
cause.stackTrace.forEach {
append(" ")
appendLine(it.toString())
}
}
appendLine("```")
}
appendLine("```")
}
}
@@ -59,24 +59,21 @@ open class AmberEvent(
private class EventManualDeserializer {
companion object {
fun fromJson(jsonObject: JsonNode): AmberEvent =
AmberEvent(
id = jsonObject.get("id")?.asText() ?: "",
pubKey = jsonObject.get("pubkey")?.asText()?.intern() ?: "",
createdAt = jsonObject.get("created_at")?.asLong() ?: TimeUtils.now(),
kind = jsonObject.get("kind").asInt(),
tags = jsonObject.get("tags")?.toTypedArray {
it.toTypedArray { s -> if (s.isNull) "" else s.asText().intern() }
} ?: emptyArray(),
content = jsonObject.get("content").asText(),
sig = jsonObject.get("sig")?.asText() ?: "",
)
fun fromJson(jsonObject: JsonNode): AmberEvent = AmberEvent(
id = jsonObject.get("id")?.asText() ?: "",
pubKey = jsonObject.get("pubkey")?.asText()?.intern() ?: "",
createdAt = jsonObject.get("created_at")?.asLong() ?: TimeUtils.now(),
kind = jsonObject.get("kind").asInt(),
tags = jsonObject.get("tags")?.toTypedArray {
it.toTypedArray { s -> if (s.isNull) "" else s.asText().intern() }
} ?: emptyArray(),
content = jsonObject.get("content").asText(),
sig = jsonObject.get("sig")?.asText() ?: "",
)
}
}
fun relay(): String {
return tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.first()
}
fun relay(): String = tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.first()
fun verifiedFollowKeySet(): Set<HexKey> = tags.mapNotNullTo(HashSet(), ContactTag::parseValidKey)
@@ -94,20 +91,16 @@ open class AmberEvent(
fun fromJson(json: String): AmberEvent = mapper.readValue(json, AmberEvent::class.java)
fun toEvent(amberEvent: AmberEvent): Event {
return Event(
amberEvent.id,
amberEvent.pubKey,
amberEvent.createdAt,
amberEvent.kind,
amberEvent.tags,
amberEvent.content,
amberEvent.sig,
)
}
fun toEvent(amberEvent: AmberEvent): Event = Event(
amberEvent.id,
amberEvent.pubKey,
amberEvent.createdAt,
amberEvent.kind,
amberEvent.tags,
amberEvent.content,
amberEvent.sig,
)
fun relay(event: Event): String {
return event.tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.first()
}
fun relay(event: Event): String = event.tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.first()
}
}
@@ -242,7 +242,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() {
startUI(account, null)
}
private fun startUI(
fun startUI(
account: Account,
route: String?,
) {
@@ -199,11 +199,9 @@ fun ApplicationsScreen(
}
}
fun Context.isZapstoreInstalled(): Boolean {
return try {
packageManager.getPackageInfo("dev.zapstore.app", 0)
true
} catch (_: Exception) {
false
}
fun Context.isZapstoreInstalled(): Boolean = try {
packageManager.getPackageInfo("dev.zapstore.app", 0)
true
} catch (_: Exception) {
false
}
@@ -2,6 +2,8 @@ package com.greenart7c3.nostrsigner.ui
import android.annotation.SuppressLint
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -34,13 +36,17 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -58,6 +64,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -83,6 +90,7 @@ import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.text.toUpperCase
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
@@ -92,10 +100,12 @@ import androidx.navigation.compose.composable
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.service.AccountExportService
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.greenart7c3.nostrsigner.ui.components.AmberElevatedButton
import com.greenart7c3.nostrsigner.ui.components.IconRow
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.greenart7c3.nostrsigner.ui.theme.RichTextDefaults
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.commonmark.MarkdownParseOptions
@@ -110,17 +120,131 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("ConfigurationScreenWidthHeight")
@Composable
fun MainPage(
scope: CoroutineScope,
navController: NavController,
accountViewModel: AccountStateViewModel,
) {
var isLoading by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("") }
var password by remember { mutableStateOf(TextFieldValue()) }
val configuration = LocalConfiguration.current
val screenWidthDp = configuration.screenWidthDp.dp
val percentage = (screenWidthDp * 0.93f)
val verticalPadding = (screenWidthDp - percentage)
val context = LocalContext.current
var shouldShowBottomSheet by remember { mutableStateOf(false) }
val selectFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
shouldShowBottomSheet = false
Amber.instance.applicationIOScope.launch {
uri?.let {
val hasAccounts = LocalPreferences.allAccounts(Amber.instance).isNotEmpty()
AccountExportService.importAccounts(
uri = uri,
password = password.text,
onText = {
text = it
},
onLoading = {
isLoading = it
},
onFinish = {
Amber.instance.applicationIOScope.launch {
if (hasAccounts) {
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
navController.navigate(Route.Applications.route) {
popUpTo(0)
}
}
} else {
val accounts = LocalPreferences.allAccounts(Amber.instance)
if (accounts.isNotEmpty()) {
val account = LocalPreferences.loadFromEncryptedStorage(Amber.instance, accounts.first().npub)
account?.let {
accountViewModel.startUI(account, null)
}
}
}
Amber.instance.notificationSubscription.updateFilter()
Amber.instance.profileSubscription.updateFilter()
}
},
)
}
}
}
val sheetState =
rememberModalBottomSheetState(
confirmValueChange = { it != SheetValue.PartiallyExpanded },
skipPartiallyExpanded = true,
)
if (shouldShowBottomSheet) {
var showCharsPassword by remember { mutableStateOf(false) }
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = {
shouldShowBottomSheet = false
},
) {
CompositionLocalProvider(
LocalDensity provides Density(
LocalDensity.current.density,
1f,
),
) {
Column(
Modifier.padding(8.dp),
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.semantics {
contentType = ContentType.Password
},
value = password,
onValueChange = {
password = it
},
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next,
),
label = {
Text(text = stringResource(R.string.encryption_password))
},
placeholder = {
Text(text = stringResource(R.string.enter_strong_password))
},
trailingIcon = {
IconButton(onClick = { showCharsPassword = !showCharsPassword }) {
Icon(
imageVector = if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
contentDescription = if (showCharsPassword) {
stringResource(R.string.hide_password)
} else {
stringResource(R.string.show_password)
},
)
}
},
visualTransformation = if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
)
AmberButton(
onClick = {
selectFileLauncher.launch("*/*")
},
text = stringResource(R.string.recover_from_backup),
)
}
}
}
}
Scaffold {
Column(
@@ -130,79 +254,93 @@ fun MainPage(
.padding(horizontal = verticalPadding)
.padding(top = verticalPadding * 1.5f),
) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(R.string.app_name_release).toUpperCase(Locale.current),
fontSize = 36.sp,
if (isLoading) {
CenterCircularProgressIndicator(
modifier = Modifier.fillMaxSize(),
text = text,
)
Text(
text = stringResource(R.string.a_nostr_secure_signer),
fontSize = 16.sp,
color = Color(0xFFC98500),
)
Image(
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp),
imageVector = ImageVector.vectorResource(R.drawable.frame),
contentDescription = "Logo",
)
} else {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AmberElevatedButton(
contentColor = Color(0xFF4C4C4C),
textColor = MaterialTheme.colorScheme.primary,
onClick = {
scope.launch {
navController.navigate("loginPage")
}
},
text = stringResource(R.string.add_a_key),
Text(
text = stringResource(R.string.app_name_release).toUpperCase(Locale.current),
fontSize = 36.sp,
)
Text(
text = stringResource(R.string.a_nostr_secure_signer),
fontSize = 16.sp,
color = Color(0xFFC98500),
)
AmberButton(
onClick = {
scope.launch {
navController.navigate("create")
}
},
text = stringResource(R.string.generate_a_new_key),
Image(
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp),
imageVector = ImageVector.vectorResource(R.drawable.frame),
contentDescription = "Logo",
)
}
Spacer(Modifier.weight(1f))
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AmberElevatedButton(
contentColor = Color(0xFF4C4C4C),
textColor = MaterialTheme.colorScheme.primary,
onClick = {
scope.launch {
navController.navigate("loginPage")
}
},
text = stringResource(R.string.add_a_key),
)
Text(
modifier = Modifier.padding(bottom = 20.dp),
color = Color(0xFF8C8C8C),
text = buildAnnotatedString {
withStyle(
style = ParagraphStyle(
textAlign = TextAlign.Center,
),
) {
append("Amber is a free and open source project\n")
withLink(
LinkAnnotation.Url(
context.getString(R.string.amber_github_uri),
styles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
),
),
AmberButton(
onClick = {
scope.launch {
navController.navigate("create")
}
},
text = stringResource(R.string.generate_a_new_key),
)
AmberButton(
onClick = {
shouldShowBottomSheet = true
},
text = stringResource(R.string.recover_from_backup),
)
}
Spacer(Modifier.weight(1f))
Text(
modifier = Modifier.padding(bottom = 20.dp),
color = Color(0xFF8C8C8C),
text = buildAnnotatedString {
withStyle(
style = ParagraphStyle(
textAlign = TextAlign.Center,
),
) {
append(context.getString(R.string.check_the_code))
append("Amber is a free and open source project\n")
withLink(
LinkAnnotation.Url(
context.getString(R.string.amber_github_uri),
styles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
),
),
),
) {
append(context.getString(R.string.check_the_code))
}
}
}
},
)
},
)
}
}
}
}
@@ -225,6 +363,7 @@ fun MainLoginPage(
MainPage(
scope = scope,
navController = navController,
accountViewModel = accountViewModel,
)
},
)
@@ -298,6 +298,7 @@ fun MainScreen(
MainPage(
scope = scope,
navController = navController,
accountViewModel = accountStateViewModel,
)
},
)
@@ -119,11 +119,10 @@ private fun createQrCode(contents: String): QRCode {
)
}
fun newPath(withPath: Path.() -> Unit) =
Path().apply {
fillType = PathFillType.EvenOdd
withPath(this)
}
fun newPath(withPath: Path.() -> Unit) = Path().apply {
fillType = PathFillType.EvenOdd
withPath(this)
}
fun DrawScope.drawAllQrCodeDataBits(
bytes: ByteMatrix,
@@ -14,35 +14,33 @@ fun Modifier.verticalScrollbar(
state: ScrollState,
scrollbarWidth: Dp = 6.dp,
color: Color = Color.LightGray,
): Modifier {
return this.then(
Modifier.drawWithContent {
drawContent()
): Modifier = this.then(
Modifier.drawWithContent {
drawContent()
if (state.maxValue > 0) {
val width = size.width
val height = size.height
if (state.maxValue > 0) {
val width = size.width
val height = size.height
val scrollFraction = state.value.toFloat() / state.maxValue.toFloat()
val scrollbarHeight = (height * height / (state.maxValue + height))
.coerceIn(10.dp.toPx()..height)
val scrollFraction = state.value.toFloat() / state.maxValue.toFloat()
val scrollbarHeight = (height * height / (state.maxValue + height))
.coerceIn(10.dp.toPx()..height)
val scrollableHeight = height - scrollbarHeight
val scrollbarY = scrollableHeight * scrollFraction
val scrollableHeight = height - scrollbarHeight
val scrollbarY = scrollableHeight * scrollFraction
drawRoundRect(
color = color,
topLeft = Offset(
x = width - scrollbarWidth.toPx(),
y = scrollbarY,
),
size = Size(
width = scrollbarWidth.toPx(),
height = scrollbarHeight,
),
cornerRadius = CornerRadius(scrollbarWidth.toPx() / 2),
)
}
},
)
}
drawRoundRect(
color = color,
topLeft = Offset(
x = width - scrollbarWidth.toPx(),
y = scrollbarY,
),
size = Size(
width = scrollbarWidth.toPx(),
height = scrollbarHeight,
),
cornerRadius = CornerRadius(scrollbarWidth.toPx() / 2),
)
}
},
)
@@ -465,15 +465,13 @@ enum class DeleteAfterType(val screenCode: Int, val resourceId: Int) {
ONE_WEEK(5, R.string.one_week),
}
fun parseDeleteAfterType(screenCode: Int): DeleteAfterType {
return when (screenCode) {
DeleteAfterType.FIVE_MINUTES.screenCode -> DeleteAfterType.FIVE_MINUTES
DeleteAfterType.TEN_MINUTES.screenCode -> DeleteAfterType.TEN_MINUTES
DeleteAfterType.ONE_HOUR.screenCode -> DeleteAfterType.ONE_HOUR
DeleteAfterType.ONE_DAY.screenCode -> DeleteAfterType.ONE_DAY
DeleteAfterType.ONE_WEEK.screenCode -> DeleteAfterType.ONE_WEEK
else -> DeleteAfterType.NEVER
}
fun parseDeleteAfterType(screenCode: Int): DeleteAfterType = when (screenCode) {
DeleteAfterType.FIVE_MINUTES.screenCode -> DeleteAfterType.FIVE_MINUTES
DeleteAfterType.TEN_MINUTES.screenCode -> DeleteAfterType.TEN_MINUTES
DeleteAfterType.ONE_HOUR.screenCode -> DeleteAfterType.ONE_HOUR
DeleteAfterType.ONE_DAY.screenCode -> DeleteAfterType.ONE_DAY
DeleteAfterType.ONE_WEEK.screenCode -> DeleteAfterType.ONE_WEEK
else -> DeleteAfterType.NEVER
}
fun deleteAfterToSeconds(deleteAfterType: DeleteAfterType): Long {
@@ -495,13 +493,11 @@ enum class BiometricsTimeType(val screenCode: Int, val resourceId: Int) {
TEN_MINUTES(3, R.string.ten_minutes),
}
fun parseBiometricsTimeType(screenCode: Int): BiometricsTimeType {
return when (screenCode) {
BiometricsTimeType.ONE_MINUTE.screenCode -> BiometricsTimeType.ONE_MINUTE
BiometricsTimeType.FIVE_MINUTES.screenCode -> BiometricsTimeType.FIVE_MINUTES
BiometricsTimeType.TEN_MINUTES.screenCode -> BiometricsTimeType.TEN_MINUTES
else -> {
BiometricsTimeType.EVERY_TIME
}
fun parseBiometricsTimeType(screenCode: Int): BiometricsTimeType = when (screenCode) {
BiometricsTimeType.ONE_MINUTE.screenCode -> BiometricsTimeType.ONE_MINUTE
BiometricsTimeType.FIVE_MINUTES.screenCode -> BiometricsTimeType.FIVE_MINUTES
BiometricsTimeType.TEN_MINUTES.screenCode -> BiometricsTimeType.TEN_MINUTES
else -> {
BiometricsTimeType.EVERY_TIME
}
}
@@ -369,7 +369,7 @@ fun ExportAllAccountsScreen(
onClick = {
showConfirmationDialog = false
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
exportLauncher.launch("amber_backup_$timestamp.dat")
exportLauncher.launch("amber_backup_$timestamp.jsonl")
},
) {
Text(text = stringResource(R.string.export))
@@ -95,7 +95,13 @@ fun AmberNavigationBar(
Icon(
painterResource(it.icon),
it.route,
tint = if (selected) Color.Black else if (isSystemInDarkTheme()) Color.White else Color.Black,
tint = if (selected) {
Color.Black
} else if (isSystemInDarkTheme()) {
Color.White
} else {
Color.Black
},
)
}
},
@@ -91,16 +91,14 @@ fun SeedWordsPage(
val size = seedWords.size
val count = size / 2
val firstColumnWords = seedWords.mapIndexedNotNull {
index, word ->
val firstColumnWords = seedWords.mapIndexedNotNull { index, word ->
if (index < count) {
word
} else {
null
}
}
val secondColumnWords = seedWords.mapIndexedNotNull {
index, word ->
val secondColumnWords = seedWords.mapIndexedNotNull { index, word ->
if (index >= count) {
word
} else {
+2
View File
@@ -601,4 +601,6 @@
<string name="read_this_text_from_encrypted_content">Read this text from %1$s encrypted content</string>
<string name="ping">Ping</string>
<string name="response">Response</string>
<string name="recover_from_bac">Recover from backup</string>
<string name="recover_from_backup">Recover from backup</string>
</resources>
+1 -1
View File
@@ -22,7 +22,7 @@ okhttp = "5.3.1"
kotlin = "2.2.21"
workRuntimeKtx = "2.11.0"
agp = "8.13.1"
ktlint = "12.3.0"
ktlint = "14.0.1"
ksp = "2.3.2"
coil = "3.3.0"
storage = "2.2.0"