diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8..b589d56 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 7b46144..fcadee0 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,16 +4,27 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 39193b7..e837843 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,14 +1,7 @@ - + - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index f489b77..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,107 +0,0 @@ -plugins { - id 'com.android.application' - id 'kotlin-android' - id 'org.jetbrains.kotlin.android' -} - -android { - compileSdkVersion 33 - - defaultConfig { - applicationId "cloud.keyspace.android" - minSdkVersion 27 - targetSdkVersion 33 - versionCode 142 - versionName "1.4.2" - multiDexEnabled true - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - buildFeatures { - viewBinding true - } - - splits { - abi { - enable true - reset() - include 'x86', 'armeabi-v7a' - universalApk true - } - } - namespace 'cloud.keyspace.android' - -} - -dependencies { - implementation 'com.android.support:multidex:2.0.1' // noinspection GradleDependency - implementation 'androidx.core:core-ktx:1.9.0' // Default Android stuff - implementation 'androidx.appcompat:appcompat:1.6.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.gridlayout:gridlayout:1.0.0' - implementation 'androidx.drawerlayout:drawerlayout:1.1.1' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' - implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' - implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' - implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.preference:preference-ktx:1.2.0' - implementation 'androidx.test:core-ktx:1.5.0' - implementation 'androidx.autofill:autofill:1.1.0' - implementation 'androidx.core:core-ktx:1.9.0' - testImplementation 'junit:junit:4.13.2' // Testing - implementation 'androidx.biometric:biometric:1.2.0-alpha05' // Biometrics - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - implementation 'com.google.android.material:material:1.7.0' // Material design - implementation 'androidx.fragment:fragment-ktx:1.5.5' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' - implementation 'com.android.volley:volley:1.2.1' // Library to make POST and GET requests - implementation 'com.neovisionaries:nv-websocket-client:2.14' // WebSockets - implementation 'com.budiyev.android:code-scanner:2.1.0' // QR Code scanner - implementation 'com.github.SumiMakito:AwesomeQRCode:1.2.0' - implementation 'com.google.zxing:core:3.5.1' - implementation 'com.google.code.gson:gson:2.10' // JSON parsing - implementation 'dev.turingcomplete:kotlin-onetimepassword:2.4.0' // 2FA Tokens - implementation "cash.z.ecc.android:kotlin-bip39:1.0.2" // bip39 mnemonics - implementation "androidx.security:security-crypto:1.1.0-alpha04" // Android cryptography - implementation "com.goterl:lazysodium-android:5.0.2@aar" // for Argon2i and ed25519 keypair - implementation 'net.java.dev.jna:jna:5.10.0@aar' // LibSodium JNA libraries - implementation "androidx.security:security-app-authenticator:1.0.0-alpha02" // for biometrics + keystore - androidTestImplementation "androidx.security:security-app-authenticator:1.0.0-alpha02" // For App Authentication API testing - implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.1' - implementation 'com.github.tsuryo:Swipeable-RecyclerView:1.1' // For swipeable recyclerview - implementation "io.github.yahiaangelo.markdownedittext:markdownedittext:1.1.3" // For Notes markdown - implementation 'com.yydcdut:markdown-processor:0.1.3' - implementation 'com.yydcdut:rxmarkdown-wrapper:0.1.3' - implementation 'io.reactivex:rxandroid:1.2.1' - implementation 'io.reactivex:rxjava:1.3.8' - implementation 'com.pixplicity.sharp:library:+@aar' - implementation 'com.guolindev.permissionx:permissionx:1.6.4' // For permissions - implementation 'com.github.Dhaval2404:ColorPicker:2.3' - implementation 'com.scottyab:rootbeer-lib:0.1.0' // To check if device is rooted - implementation "androidx.core:core-splashscreen:1.0.0" // Splash screen library - implementation 'com.nulab-inc:zxcvbn:1.7.0' // password strength - - // ACRA for crash logging - implementation 'ch.acra:acra-mail:5.9.7' // mail component - implementation 'ch.acra:acra-dialog:5.9.7' // dialog component - implementation "com.anggrayudi:storage:1.5.4" // access storage -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..a201881 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,161 @@ +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.kotlinAndroid) +} + +android { + namespace = AppBuildConfig.namespace + compileSdk = AppBuildConfig.compileSdk + + defaultConfig { + applicationId = AppBuildConfig.appId + minSdk = AppBuildConfig.minSdk + targetSdk = AppBuildConfig.targetSdk + versionCode = AppBuildConfig.versionCode + versionName = AppBuildConfig.versionName + multiDexEnabled = true + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.get()) + } + + kotlinOptions { + jvmTarget = libs.versions.java.get() + } + + buildFeatures { + buildConfig = true + viewBinding = true + } + + splits { + abi { + isEnable = true + reset() + include("x86", "x86_64", "armeabi-v7a", "armeabi-v8a") + isUniversalApk = true + } + } + + packaging { + resources { + excludes.addAll( + listOf( + "META-INF/DEPENDENCIES", + "META-INF/LICENSE", + "META-INF/LICENSE.txt", + "META-INF/license.txt", + "META-INF/NOTICE", + "META-INF/NOTICE.txt", + "META-INF/notice.txt", + "META-INF/ASL2.0", + "META-INF/AL2.0", + "META-INF/LGPL2.1", + "META-INF/*.kotlin_module" + ) + ) + } + } +} + +dependencies { + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.core) + + implementation(libs.android.multidex) + implementation(libs.android.volley) // Library to make POST and GET requests + + implementation(libs.androidx.appcompat) + implementation(libs.androidx.autofill) + implementation(libs.androidx.biometric) + implementation(libs.androidx.cardview) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.drawerlayout) + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.gridlayout) + implementation(libs.androidx.legacy.support.v4) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.preference.ktx) + implementation(libs.androidx.security.app.authenticator) // for biometrics + keystore + implementation(libs.androidx.security.crypto) + + implementation(libs.google.gson) // JSON parsing + implementation(libs.google.material) + implementation(libs.google.zxing) + + implementation(libs.awesomeqrcode) + implementation(libs.code.scanner) // QR Code scanner + implementation(libs.colorpicker) + implementation(libs.jackson.module.kotlin) + implementation(libs.kotlin.bip39) // bip39 mnemonics + implementation(libs.kotlin.onetimepassword) // 2FA Tokens + implementation(libs.markdown.processor) + implementation(libs.markdownedittext) // For Notes markdown + implementation(libs.nv.websocket.client) + implementation(libs.permissionx) // For permissions + implementation(libs.rootbeer.lib) // To check if device is rooted + implementation(libs.rxandroid) + implementation(libs.rxjava) + implementation(libs.rxmarkdown.wrapper) + implementation(libs.swipeable.recyclerview) // For swipeable recyclerview + implementation(libs.zxcvbn) // password strength + + //region TODO: Using artifact.type = "aar" does not seem to work. + // The version catalog will still use .jar, which will result + // in duplicate classes. + //noinspection UseTomlInstead + implementation("net.java.dev.jna:jna:5.10.0@aar") +// implementation(libs.jna) { // LibSodium JNA libraries +// artifact { +// type = "aar" +// } +// } + + //noinspection UseTomlInstead + implementation("com.pixplicity.sharp:library:1.1.2@aar") +// implementation(libs.pixplicity.library) { +// artifact { +// type = "aar" +// } +// } + + //noinspection UseTomlInstead + implementation("com.goterl:lazysodium-android:5.0.2@aar") +// implementation(libs.lazysodium.android) { // for Argon2i and ed25519 keypair +// artifact { +// type = "aar" +// } +// } + //endregion + + // ACRA for crash logging + implementation(libs.acra.mail) // mail component + implementation(libs.acra.dialog) // dialog component + implementation(libs.anggrayudi.storage) // access storage + + // Testing + implementation(libs.androidx.test.core.ktx) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.security.app.authenticator) // For App Authentication API testing +} diff --git a/app/src/main/kotlin/cloud/keyspace/android/AddCard.kt b/app/src/main/kotlin/cloud/keyspace/android/AddCard.kt index f49cafd..11fa9ba 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/AddCard.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/AddCard.kt @@ -3,17 +3,13 @@ package cloud.keyspace.android import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences -import android.content.res.ColorStateList -import android.graphics.Color import android.os.Bundle import android.os.Handler import android.os.Looper import android.text.Editable -import android.text.SpannableStringBuilder import android.text.TextUtils import android.text.TextWatcher import android.text.method.PasswordTransformationMethod -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -23,7 +19,6 @@ import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import androidx.core.view.ViewCompat import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.widget.doOnTextChanged import androidx.recyclerview.widget.LinearLayoutManager @@ -31,15 +26,12 @@ import androidx.recyclerview.widget.RecyclerView import com.github.dhaval2404.colorpicker.MaterialColorPickerDialog import com.github.dhaval2404.colorpicker.listener.ColorListener import com.google.android.material.button.MaterialButton -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import com.keyspace.keyspacemobile.* import java.text.SimpleDateFormat -import java.time.Instant import java.util.* import kotlin.concurrent.thread @@ -109,20 +101,28 @@ class AddCard : AppCompatActivity() { lateinit var configData: SharedPreferences + private lateinit var itemPersistence: ItemPersistence + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.edit_card) - configData = getSharedPreferences(applicationContext.packageName + "_configuration_data", MODE_PRIVATE) + configData = getSharedPreferences( + applicationContext.packageName + "_configuration_data", + MODE_PRIVATE + ) val allowScreenshots = configData.getBoolean("allowScreenshots", false) - if (!allowScreenshots) window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + if (!allowScreenshots) window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) - utils = MiscUtilities (applicationContext) + utils = MiscUtilities(applicationContext) crypto = CryptoUtilities(applicationContext, this) misc = MiscUtilities(applicationContext) - val intentData = crypto.receiveKeyringFromSecureIntent ( + val intentData = crypto.receiveKeyringFromSecureIntent( currentActivityClassNameAsString = getString(R.string.title_activity_add_card), intent = intent ) @@ -138,14 +138,20 @@ class AddCard : AppCompatActivity() { vault = io.getVault() if (itemId != null) { card = io.decryptCard(io.getCard(itemId!!, vault)!!) - loadCard (card) + loadCard(card) } + itemPersistence = ItemPersistence( + applicationContext = applicationContext, + appCompatActivity = this, + keyring = keyring, + itemId = itemId + ) } - private fun initializeUI (): Boolean { + private fun initializeUI(): Boolean { - doneButton = findViewById (R.id.done) + doneButton = findViewById(R.id.done) doneButton.setOnClickListener { saveItem() } @@ -155,19 +161,22 @@ class AddCard : AppCompatActivity() { onBackPressed() } - deleteButton = findViewById (R.id.delete) + deleteButton = findViewById(R.id.delete) if (itemId != null) { deleteButton.setOnClickListener { val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() alertDialog.setTitle(getString(R.string.delete_title)) alertDialog.setMessage("${getString(R.string.delete_subtitle)} \"${card.name}\"") - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.delete_title)) { _, _ -> + alertDialog.setButton( + AlertDialog.BUTTON_POSITIVE, + getString(R.string.delete_title) + ) { _, _ -> vault.card!!.remove(io.getCard(itemId!!, vault)) io.writeVault(vault) - network.writeQueueTask (itemId!!, mode = network.MODE_DELETE) - crypto.secureStartActivity ( + network.writeQueueTask(itemId!!, mode = network.MODE_DELETE) + crypto.secureStartActivity( nextActivity = Dashboard(), nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), keyring = keyring, @@ -175,7 +184,10 @@ class AddCard : AppCompatActivity() { ) } - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.go_back_button)) { dialog, _ -> dialog.dismiss() } + alertDialog.setButton( + AlertDialog.BUTTON_NEGATIVE, + getString(R.string.go_back_button) + ) { dialog, _ -> dialog.dismiss() } alertDialog.show() } @@ -183,8 +195,8 @@ class AddCard : AppCompatActivity() { deleteButton.visibility = View.GONE } - tagButton = findViewById (R.id.tag) - tagPicker = AddTag (tagId, applicationContext, this@AddCard, keyring) + tagButton = findViewById(R.id.tag) + tagPicker = AddTag(tagId, applicationContext, this@AddCard, keyring) tagButton.setOnClickListener { tagPicker.showPicker(tagId) tagPicker.showPicker(tagId) @@ -197,14 +209,34 @@ class AddCard : AppCompatActivity() { } favoriteButton = findViewById(R.id.favoriteButton) - favoriteButton.setImageDrawable(ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_border_24)) + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + applicationContext, + R.drawable.ic_baseline_star_border_24 + ) + ) favoriteButton.setOnClickListener { favorite = if (!favorite) { - favoriteButton.setImageDrawable (ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_24)) - favoriteButton.startAnimation(AnimationUtils.loadAnimation(applicationContext, R.anim.heartbeat)) + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + applicationContext, + R.drawable.ic_baseline_star_24 + ) + ) + favoriteButton.startAnimation( + AnimationUtils.loadAnimation( + applicationContext, + R.anim.heartbeat + ) + ) true } else { - favoriteButton.setImageDrawable (ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_border_24)) + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + applicationContext, + R.drawable.ic_baseline_star_border_24 + ) + ) false } } @@ -225,24 +257,36 @@ class AddCard : AppCompatActivity() { .show() } - nameInput = findViewById (R.id.nameInput) + nameInput = findViewById(R.id.nameInput) nameInput.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - nameInputLayout = findViewById (R.id.nameInputLayout) + nameInputLayout = findViewById(R.id.nameInputLayout) - nameInputIcon = findViewById (R.id.nameInputIcon) + nameInputIcon = findViewById(R.id.nameInputIcon) nameInputIcon.setOnClickListener { iconFilePicker() } - nameIconPicker = findViewById (R.id.pickIcon) + nameIconPicker = findViewById(R.id.pickIcon) nameIconPicker.setOnClickListener { iconFilePicker() } nameInput.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable) { } - override fun beforeTextChanged(bankName: CharSequence, start: Int, count: Int, after: Int) { } - override fun onTextChanged(bankName: CharSequence, start: Int, before: Int, count: Int) { + override fun afterTextChanged(s: Editable) {} + override fun beforeTextChanged( + bankName: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + bankName: CharSequence, + start: Int, + before: Int, + count: Int + ) { thread { val bankLogo = misc.getSiteIcon(bankName.toString(), nameInput.currentTextColor) if (bankLogo != null/* && iconFileName == null*/) { @@ -255,7 +299,7 @@ class AddCard : AppCompatActivity() { } }) - cardNumberInput = findViewById (R.id.CardNumberInput) + cardNumberInput = findViewById(R.id.CardNumberInput) cardNumberInput.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING cardNumberInput.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING @@ -267,18 +311,24 @@ class AddCard : AppCompatActivity() { if (s.isNotEmpty() && s.length % 5 == 0) { val c = s[s.length - 1] if (space == c) s.delete(s.length - 1, s.length) - if (Character.isDigit(c) && TextUtils.split(s.toString(), space.toString()).size <= 3) s.insert(s.length - 1, space.toString()) + if (Character.isDigit(c) && TextUtils.split( + s.toString(), + space.toString() + ).size <= 3 + ) s.insert(s.length - 1, space.toString()) } if (s.toString().replace(" ", "").length in 0..16) { cardNumberInput.removeTextChangedListener(this) - cardNumberInput.setText(s.toString().replace(" ", "").replace("....".toRegex(), "$0 ")?.trim()) + cardNumberInput.setText( + s.toString().replace(" ", "").replace("....".toRegex(), "$0 ")?.trim() + ) cardNumberInput.addTextChangedListener(this) cardNumberInput.setSelection(cardNumberInput.text.toString().length) } if (s.toString().replace(" ", "").length in 17..18) { for (c in s) { if (c == ' ') { - s.delete(s.indexOf(c), s.indexOf(c)+1) + s.delete(s.indexOf(c), s.indexOf(c) + 1) } } } @@ -286,8 +336,21 @@ class AddCard : AppCompatActivity() { s.delete(s.length - 1, s.length) } } - override fun beforeTextChanged(cardNumber: CharSequence, start: Int, count: Int, after: Int) { } - override fun onTextChanged(cardNumber: CharSequence, start: Int, before: Int, count: Int) { + + override fun beforeTextChanged( + cardNumber: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + cardNumber: CharSequence, + start: Int, + before: Int, + count: Int + ) { val paymentGateway = misc.getPaymentGateway(cardNumber.toString()) if (paymentGateway != null) { val gatewayLogo = misc.getSiteIcon(paymentGateway, nameInput.currentTextColor) @@ -299,9 +362,9 @@ class AddCard : AppCompatActivity() { } }) - atmPinLayout = findViewById (R.id.AtmPinLayout) + atmPinLayout = findViewById(R.id.AtmPinLayout) atmPinLayout.visibility = View.GONE - isAtmCard = findViewById (R.id.isAtmCard) + isAtmCard = findViewById(R.id.isAtmCard) isAtmCard.isChecked = false isAtmCard.setOnCheckedChangeListener { _, isChecked -> @@ -309,13 +372,13 @@ class AddCard : AppCompatActivity() { else atmPinLayout.visibility = View.VISIBLE } - hasRfidChip = findViewById (R.id.hasRfidChip) + hasRfidChip = findViewById(R.id.hasRfidChip) - atmPinInput = findViewById (R.id.AtmPinInput) + atmPinInput = findViewById(R.id.AtmPinInput) atmPinInput.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING atmPinInput.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - toDate = findViewById (R.id.ToDateInput) + toDate = findViewById(R.id.ToDateInput) toDate.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING toDate.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable) { @@ -324,16 +387,20 @@ class AddCard : AppCompatActivity() { val monthFormat = SimpleDateFormat("M", Locale.US) val year = yearFormat.format(Date()).toString().toInt() val month = monthFormat.format(Date()).toString().toInt() - if (toDate.text?.takeLast(2).toString().toInt() > year+5) { + if (toDate.text?.takeLast(2).toString().toInt() > year + 5) { toDate.text!!.clear() toDate.error = getString(R.string.invalid_card_year_blurb) - } else if ( (toDate.text?.take(2).toString().toInt() <= month && toDate.text?.takeLast(2).toString().toInt() == year) || toDate.text?.takeLast(2).toString().toInt() < year) { + } else if ((toDate.text?.take(2).toString() + .toInt() <= month && toDate.text?.takeLast(2).toString() + .toInt() == year) || toDate.text?.takeLast(2).toString().toInt() < year + ) { toDate.text!!.clear() toDate.error = getString(R.string.expired_card_blurb) } } } - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { if (toDate.text?.length == 2) { try { @@ -349,11 +416,11 @@ class AddCard : AppCompatActivity() { } }) - securityCode = findViewById (R.id.CVVInput) - cardholderNameInput = findViewById (R.id.CardholderInput) + securityCode = findViewById(R.id.CVVInput) + cardholderNameInput = findViewById(R.id.CardholderInput) cardholderNameInput.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - notesInput = findViewById (R.id.notesInput) + notesInput = findViewById(R.id.notesInput) notesInput.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING @@ -361,7 +428,7 @@ class AddCard : AppCompatActivity() { customFieldsView.layoutManager = LinearLayoutManager(this) customFieldsData = mutableListOf() - customFieldsAdapter = CustomFieldsAdapter (customFieldsData) + customFieldsAdapter = CustomFieldsAdapter(customFieldsData) customFieldsView.adapter = customFieldsAdapter addCustomFieldButton = findViewById(R.id.addCustomFieldButton) @@ -377,83 +444,132 @@ class AddCard : AppCompatActivity() { return true } - private fun saveItem () { - var dateCreated = Instant.now().epochSecond - - if (itemId != null) { - dateCreated = card.dateCreated!! - vault.card?.remove(io.getCard(itemId!!, vault)) - } - - if (cardNumberInput.text.toString().replace(" ", "").length < 16) cardNumberInput.error = "Enter a valid 16 digit card number" - else if (cardNumberInput.text.toString().replace(" ", "").length in 17..18 - || cardNumberInput.text.toString().replace(" ", "").length > 19) cardNumberInput.error = "Enter a valid 19 digit card number" - else if (securityCode.text.toString().length !in 3..4) securityCode.error = "Enter a valid security code" - else if (toDate.text.toString().isEmpty()) toDate.error = "Enter an expiry date" - else if (cardholderNameInput.text.toString().isEmpty()) cardholderNameInput.error = "Enter card holder's name" - else if (nameInput.text.toString().isEmpty()) nameInput.error = "Enter a name. This can be your bank's name." - else if (isAtmCard.isChecked && atmPinInput.text.toString().length < 4) atmPinInput.error = "Enter a valid Personal Identification Number" - - else { - - val data = IOUtilities.Card( - id = itemId ?: UUID.randomUUID().toString(), - organizationId = null, - type = io.TYPE_CARD, - name = nameInput.text.toString(), - color = cardColor, - favorite = favorite, - tagId = tagPicker.getSelectedTagId() ?: tagId, - dateCreated = dateCreated, - dateModified = Instant.now().epochSecond, - frequencyAccessed = frequencyAccessed + 1, - cardNumber = cardNumberInput.text.toString().filter { !it.isWhitespace() }, - cardholderName = cardholderNameInput.text.toString(), - expiry = toDate.text.toString(), - notes = notesInput.text.toString(), - pin = if (atmPinInput.text.toString().length == 4 && isAtmCard.isChecked) atmPinInput.text.toString() else "", - securityCode = securityCode.text.toString(), - customFields = customFieldsData, - rfid = hasRfidChip.isChecked, - iconFile = iconFileName - ) - - val encryptedCard = io.encryptCard(data) - - vault.card?.add (encryptedCard) - io.writeVault(vault) - - if (itemId != null) network.writeQueueTask (encryptedCard, mode = network.MODE_PUT) - else network.writeQueueTask (encryptedCard, mode = network.MODE_POST) - - crypto.secureStartActivity ( - nextActivity = Dashboard(), - nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), - keyring = keyring, - itemId = null - ) - + private fun saveItem() { + itemPersistence.saveCard( + cardName = nameInput.text.toString(), + cardNumber = cardNumberInput.text.toString(), + cardholderName = cardholderNameInput.text.toString(), + toDate = toDate.text.toString(), + securityCode = securityCode.text.toString(), + atmPin = atmPinInput.text.toString(), + isAtmCard = isAtmCard.isChecked, + hasRfidChip = hasRfidChip.isChecked, + iconFileName = iconFileName, + cardColor = cardColor, + isFavorite = favorite, + tagId = tagPicker.getSelectedTagId() ?: tagId, + notes = notesInput.text.toString(), + customFieldsData = customFieldsData, + frequencyAccessed = frequencyAccessed + ) { error -> + cardNumberInput.error = error.cardNumberError + toDate.error = error.toDateError + securityCode.error = error.securityCodeError + cardholderNameInput.error = error.cardholderNameError + nameInput.error = error.nameError + atmPinInput.error = error.atmPinError } - } + //region Original saveItem() +// private fun saveItem() { +// var dateCreated = Instant.now().epochSecond +// +// if (itemId != null) { +// dateCreated = card.dateCreated!! +// vault.card?.remove(io.getCard(itemId!!, vault)) +// } +// +// if (cardNumberInput.text.toString().replace(" ", "").length < 16) cardNumberInput.error = +// "Enter a valid 16 digit card number" +// else if (cardNumberInput.text.toString().replace(" ", "").length in 17..18 +// || cardNumberInput.text.toString().replace(" ", "").length > 19 +// ) cardNumberInput.error = "Enter a valid 19 digit card number" +// else if (securityCode.text.toString().length !in 3..4) securityCode.error = +// "Enter a valid security code" +// else if (toDate.text.toString().isEmpty()) toDate.error = "Enter an expiry date" +// else if (cardholderNameInput.text.toString().isEmpty()) cardholderNameInput.error = +// "Enter card holder's name" +// else if (nameInput.text.toString().isEmpty()) nameInput.error = +// "Enter a name. This can be your bank's name." +// else if (isAtmCard.isChecked && atmPinInput.text.toString().length < 4) atmPinInput.error = +// "Enter a valid Personal Identification Number" +// else { +// +// val data = IOUtilities.Card( +// id = itemId ?: UUID.randomUUID().toString(), +// organizationId = null, +// type = io.TYPE_CARD, +// name = nameInput.text.toString(), +// color = cardColor, +// favorite = favorite, +// tagId = tagPicker.getSelectedTagId() ?: tagId, +// dateCreated = dateCreated, +// dateModified = Instant.now().epochSecond, +// frequencyAccessed = frequencyAccessed + 1, +// cardNumber = cardNumberInput.text.toString().filter { !it.isWhitespace() }, +// cardholderName = cardholderNameInput.text.toString(), +// expiry = toDate.text.toString(), +// notes = notesInput.text.toString(), +// pin = if (atmPinInput.text.toString().length == 4 && isAtmCard.isChecked) atmPinInput.text.toString() else "", +// securityCode = securityCode.text.toString(), +// customFields = customFieldsData, +// rfid = hasRfidChip.isChecked, +// iconFile = iconFileName +// ) +// +// val encryptedCard = io.encryptCard(data) +// +// vault.card?.add(encryptedCard) +// io.writeVault(vault) +// +// if (itemId != null) network.writeQueueTask(encryptedCard, mode = network.MODE_PUT) +// else network.writeQueueTask(encryptedCard, mode = network.MODE_POST) +// +// crypto.secureStartActivity( +// nextActivity = Dashboard(), +// nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), +// keyring = keyring, +// itemId = null +// ) +// +// } +// +// } + //endregion Original saveItem() + @SuppressLint("NotifyDataSetChanged") private fun loadCard(card: IOUtilities.Card): Boolean { favorite = if (card.favorite) { - favoriteButton.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_baseline_star_24)); true + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + this, + R.drawable.ic_baseline_star_24 + ) + ); true } else { - favoriteButton.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_baseline_star_border_24)); false + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + this, + R.drawable.ic_baseline_star_border_24 + ) + ); false } tagId = card.tagId - tagPicker = AddTag (tagId, applicationContext, this@AddCard, keyring) + tagPicker = AddTag(tagId, applicationContext, this@AddCard, keyring) nameInput.setText(card.name) notesInput.setText(card.notes) - if (card.cardNumber?.length!! == 16) cardNumberInput.setText(card.cardNumber.replace("....".toRegex(), "$0 ")) + if (card.cardNumber?.length!! == 16) cardNumberInput.setText( + card.cardNumber.replace( + "....".toRegex(), + "$0 " + ) + ) else cardNumberInput.setText(card.cardNumber) toDate.setText(card.expiry) @@ -469,7 +585,7 @@ class AddCard : AppCompatActivity() { if (card.customFields != null) { customFieldsData = card.customFields - customFieldsAdapter = CustomFieldsAdapter (customFieldsData) + customFieldsAdapter = CustomFieldsAdapter(customFieldsData) customFieldsView.adapter = customFieldsAdapter customFieldsAdapter.notifyItemInserted(customFieldsData.size) customFieldsView.invalidate() @@ -479,19 +595,34 @@ class AddCard : AppCompatActivity() { cardColor = card.color - Handler().postDelayed({ runOnUiThread { - iconFileName = card.iconFile - if (iconFileName != null) nameInputIcon.setImageDrawable(misc.getSiteIcon(iconFileName!!, nameInput.currentTextColor)) - else nameInputIcon.setImageDrawable(getDrawable(R.drawable.ic_baseline_website_24)) - } }, 100) + Handler().postDelayed({ + runOnUiThread { + iconFileName = card.iconFile + if (iconFileName != null) nameInputIcon.setImageDrawable( + misc.getSiteIcon( + iconFileName!!, + nameInput.currentTextColor + ) + ) + else nameInputIcon.setImageDrawable(getDrawable(R.drawable.ic_baseline_website_24)) + } + }, 100) return true } - inner class CustomFieldsAdapter (private val customFields: MutableList) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : ViewHolder { // create new views - val customFieldsView: View = LayoutInflater.from(parent.context).inflate(R.layout.custom_field, parent, false) - customFieldsView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + inner class CustomFieldsAdapter(private val customFields: MutableList) : + RecyclerView.Adapter() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { // create new views + val customFieldsView: View = + LayoutInflater.from(parent.context).inflate(R.layout.custom_field, parent, false) + customFieldsView.layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, + RecyclerView.LayoutParams.WRAP_CONTENT + ) return ViewHolder(customFieldsView) } @@ -500,11 +631,13 @@ class AddCard : AppCompatActivity() { var hidden = false - customFieldView.fieldName.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - customFieldView.fieldValue.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING + customFieldView.fieldName.imeOptions = + EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING + customFieldView.fieldValue.imeOptions = + EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - customFieldView.fieldName.setText (customField.name) - customFieldView.fieldValue.setText (customField.value) + customFieldView.fieldName.setText(customField.name) + customFieldView.fieldValue.setText(customField.value) if (customField.hidden) { customFieldView.fieldValue.transformationMethod = PasswordTransformationMethod() @@ -517,18 +650,42 @@ class AddCard : AppCompatActivity() { } customFieldView.fieldName.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable) { } - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} - override fun onTextChanged(data: CharSequence, start: Int, before: Int, count: Int) { + override fun afterTextChanged(s: Editable) {} + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + data: CharSequence, + start: Int, + before: Int, + count: Int + ) { addCustomFieldButton.isEnabled = data.isNotEmpty() customFieldsData[customFieldView.adapterPosition].name = data.toString() } }) customFieldView.fieldValue.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable) { } - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { } - override fun onTextChanged(data: CharSequence, start: Int, before: Int, count: Int) { + override fun afterTextChanged(s: Editable) {} + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + data: CharSequence, + start: Int, + before: Int, + count: Int + ) { addCustomFieldButton.isEnabled = data.isNotEmpty() customFieldsData[customFieldView.adapterPosition].value = data.toString() } @@ -536,12 +693,17 @@ class AddCard : AppCompatActivity() { customFieldView.deleteIcon.setOnClickListener { view -> addCustomFieldButton.isEnabled = true - Toast.makeText(applicationContext, "Deleted \"${customFieldView.fieldName.text}\"", Toast.LENGTH_SHORT).show() + Toast.makeText( + applicationContext, + "Deleted \"${customFieldView.fieldName.text}\"", + Toast.LENGTH_SHORT + ).show() customFieldView.fieldName.clearFocus() customFieldView.fieldValue.clearFocus() try { customFieldsData.remove(customFieldsData[customFieldView.adapterPosition]) - } catch (noItemsLeft: IndexOutOfBoundsException) { } + } catch (noItemsLeft: IndexOutOfBoundsException) { + } customFieldsAdapter.notifyItemRemoved(position) customFieldsView.invalidate() customFieldsView.refreshDrawableState() @@ -569,11 +731,14 @@ class AddCard : AppCompatActivity() { return customFields.size } - inner class ViewHolder (itemLayoutView: View) : RecyclerView.ViewHolder(itemLayoutView) { + inner class ViewHolder(itemLayoutView: View) : RecyclerView.ViewHolder(itemLayoutView) { var fieldName: EditText = itemLayoutView.findViewById(R.id.field_name) as EditText - var fieldValue: EditText = itemLayoutView.findViewById(R.id.field_value) as EditText - var deleteIcon: ImageView = itemLayoutView.findViewById(R.id.deleteCustomFieldButton) as ImageView - var hideIcon: ImageView = itemLayoutView.findViewById(R.id.hideCustomFieldButton) as ImageView + var fieldValue: EditText = + itemLayoutView.findViewById(R.id.field_value) as EditText + var deleteIcon: ImageView = + itemLayoutView.findViewById(R.id.deleteCustomFieldButton) as ImageView + var hideIcon: ImageView = + itemLayoutView.findViewById(R.id.hideCustomFieldButton) as ImageView } } @@ -591,12 +756,12 @@ class AddCard : AppCompatActivity() { finishAffinity() } - override fun onBackPressed () { + override fun onBackPressed() { val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() alertDialog.setTitle("Confirm exit") alertDialog.setMessage("Would you like to go back to the Dashboard?") alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Exit") { dialog, _ -> - crypto.secureStartActivity ( + crypto.secureStartActivity( nextActivity = Dashboard(), nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), keyring = keyring, @@ -605,7 +770,10 @@ class AddCard : AppCompatActivity() { super.onBackPressed() tagIdGrabber.removeCallbacksAndMessages(null) } - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Cancel") { dialog, _ -> dialog.dismiss() } + alertDialog.setButton( + AlertDialog.BUTTON_NEGATIVE, + "Cancel" + ) { dialog, _ -> dialog.dismiss() } alertDialog.show() } @@ -642,37 +810,70 @@ class AddCard : AppCompatActivity() { super.onPause() } - private fun iconFilePicker () { + private fun iconFilePicker() { val builder = MaterialAlertDialogBuilder(this@AddCard) builder.setCancelable(true) val iconsBox: View = layoutInflater.inflate(R.layout.icon_picker_dialog, null) builder.setView(iconsBox) - iconsBox.startAnimation(AnimationUtils.loadAnimation(applicationContext, R.anim.from_bottom)) + iconsBox.startAnimation( + AnimationUtils.loadAnimation( + applicationContext, + R.anim.from_bottom + ) + ) val dialog = builder.create() dialog.show() val iconFileNames = misc.getSiteIconFilenames() + class GridAdapter(var context: Context, filenames: ArrayList) : BaseAdapter() { var listFiles: ArrayList - init { listFiles = filenames } - override fun getCount(): Int { return listFiles.size } - override fun getItem(position: Int): Any { return listFiles[position] } - override fun getItemId(position: Int): Long { return position.toLong() } + + init { + listFiles = filenames + } + + override fun getCount(): Int { + return listFiles.size + } + + override fun getItem(position: Int): Any { + return listFiles[position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + @SuppressLint("ViewHolder") override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = (context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater).inflate(R.layout.site_icon, null) + val view = + (context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater).inflate( + R.layout.site_icon, + null + ) val icon = view.findViewById(R.id.icon) - icon.setImageDrawable(misc.getSiteIcon(listFiles[position], nameInput.currentTextColor)) + icon.setImageDrawable( + misc.getSiteIcon( + listFiles[position], + nameInput.currentTextColor + ) + ) val iconName = view.findViewById(R.id.iconName) iconName.text = listFiles[position].replace("_", "") icon.setOnClickListener { iconFileName = listFiles[position] - nameInputIcon.setImageDrawable(misc.getSiteIcon(listFiles[position], nameInput.currentTextColor)) + nameInputIcon.setImageDrawable( + misc.getSiteIcon( + listFiles[position], + nameInput.currentTextColor + ) + ) dialog.dismiss() } diff --git a/app/src/main/kotlin/cloud/keyspace/android/AddLogin.kt b/app/src/main/kotlin/cloud/keyspace/android/AddLogin.kt index 6e5e514..402ed20 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/AddLogin.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/AddLogin.kt @@ -4,7 +4,6 @@ import android.Manifest import android.annotation.SuppressLint import android.content.* import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN -import android.content.res.ColorStateList import android.graphics.Color import android.graphics.Rect import android.graphics.drawable.ColorDrawable @@ -18,7 +17,6 @@ import android.text.format.DateFormat import android.text.method.LinkMovementMethod import android.text.method.PasswordTransformationMethod import android.util.AttributeSet -import android.util.Log import android.view.* import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils.loadAnimation @@ -28,18 +26,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.core.view.ViewCompat import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING import androidx.core.widget.doOnTextChanged import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.budiyev.android.codescanner.* -import com.github.dhaval2404.colorpicker.MaterialColorPickerDialog -import com.github.dhaval2404.colorpicker.listener.ColorListener import com.google.android.material.button.MaterialButton -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.slider.Slider @@ -48,7 +41,6 @@ import com.google.android.material.textfield.TextInputLayout import com.keyspace.keyspacemobile.NetworkUtilities import dev.turingcomplete.kotlinonetimepassword.GoogleAuthenticator import java.text.SimpleDateFormat -import java.time.Instant import java.util.* import kotlin.concurrent.thread import kotlin.math.abs @@ -146,22 +138,30 @@ class AddLogin : AppCompatActivity() { lateinit var configData: SharedPreferences + private lateinit var itemPersistence: ItemPersistence + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.edit_login) - configData = getSharedPreferences (applicationContext.packageName + "_configuration_data", MODE_PRIVATE) - utils = MiscUtilities (applicationContext) + configData = getSharedPreferences( + applicationContext.packageName + "_configuration_data", + MODE_PRIVATE + ) + utils = MiscUtilities(applicationContext) crypto = CryptoUtilities(applicationContext, this) misc = MiscUtilities(applicationContext) - val intentData = crypto.receiveKeyringFromSecureIntent ( + val intentData = crypto.receiveKeyringFromSecureIntent( currentActivityClassNameAsString = getString(R.string.title_activity_add_login), intent = intent ) val allowScreenshots = configData.getBoolean("allowScreenshots", false) - if (!allowScreenshots) window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + if (!allowScreenshots) window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) keyring = intentData.first network = NetworkUtilities(applicationContext, this, keyring) @@ -174,32 +174,40 @@ class AddLogin : AppCompatActivity() { vault = io.getVault() if (itemId != null) { login = io.decryptLogin(io.getLogin(itemId!!, vault)!!) - loadLogin (login) + loadLogin(login) } + + itemPersistence = ItemPersistence( + applicationContext = applicationContext, + appCompatActivity = this, + keyring = keyring, + itemId = itemId + ) } - private fun initializeUI (): Boolean { - doneButton = findViewById (R.id.done) + private fun initializeUI(): Boolean { + doneButton = findViewById(R.id.done) doneButton.setOnClickListener { - if (siteNameInput.text.isNullOrBlank()) { - siteNameInput.error = "Please enter a username" - return@setOnClickListener - } - - if (emailInput.text.toString().isNotBlank()) { - if (!misc.isValidEmail(emailInput.text.toString())) { - emailInput.error = "Please enter a valid email" - return@setOnClickListener - } - } - - if (secretInput.text.toString().isNotBlank() && secretInput.text.toString().length < 6) { - secretInput.error = "Please enter a valid TOTP secret" - return@setOnClickListener - } +// if (siteNameInput.text.isNullOrBlank()) { +// siteNameInput.error = "Please enter a username" +// return@setOnClickListener +// } +// +// if (emailInput.text.toString().isNotBlank()) { +// if (!misc.isValidEmail(emailInput.text.toString())) { +// emailInput.error = "Please enter a valid email" +// return@setOnClickListener +// } +// } +// +// if (secretInput.text.toString() +// .isNotBlank() && secretInput.text.toString().length < 6 +// ) { +// secretInput.error = "Please enter a valid TOTP secret" +// return@setOnClickListener +// } saveItem() - } backButton = findViewById(R.id.backButton) @@ -207,7 +215,7 @@ class AddLogin : AppCompatActivity() { onBackPressed() } - deleteButton = findViewById (R.id.delete) + deleteButton = findViewById(R.id.delete) if (itemId != null) { deleteButton.setOnClickListener { val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() @@ -218,8 +226,8 @@ class AddLogin : AppCompatActivity() { vault.login!!.remove(io.getLogin(itemId!!, vault)) io.writeVault(vault) - network.writeQueueTask (itemId!!, mode = network.MODE_DELETE) - crypto.secureStartActivity ( + network.writeQueueTask(itemId!!, mode = network.MODE_DELETE) + crypto.secureStartActivity( nextActivity = Dashboard(), nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), keyring = keyring, @@ -227,7 +235,10 @@ class AddLogin : AppCompatActivity() { ) } - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Go back") { dialog, _ -> dialog.dismiss() } + alertDialog.setButton( + AlertDialog.BUTTON_NEGATIVE, + "Go back" + ) { dialog, _ -> dialog.dismiss() } alertDialog.show() } @@ -235,8 +246,8 @@ class AddLogin : AppCompatActivity() { deleteButton.visibility = View.GONE } - tagButton = findViewById (R.id.tag) - tagPicker = AddTag (tagId, applicationContext, this@AddLogin, keyring) + tagButton = findViewById(R.id.tag) + tagPicker = AddTag(tagId, applicationContext, this@AddLogin, keyring) tagButton.setOnClickListener { tagPicker.showPicker(tagId) tagIdGrabber.post(object : Runnable { @@ -248,38 +259,66 @@ class AddLogin : AppCompatActivity() { } favoriteButton = findViewById(R.id.favoriteButton) - favoriteButton.setImageDrawable(ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_border_24)) + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + applicationContext, + R.drawable.ic_baseline_star_border_24 + ) + ) favoriteButton.setOnClickListener { favorite = if (!favorite) { - favoriteButton.setImageDrawable (ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_24)) + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + applicationContext, + R.drawable.ic_baseline_star_24 + ) + ) favoriteButton.startAnimation(loadAnimation(applicationContext, R.anim.heartbeat)) true } else { - favoriteButton.setImageDrawable (ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_border_24)) + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + applicationContext, + R.drawable.ic_baseline_star_border_24 + ) + ) false } } - siteNameInput = findViewById (R.id.siteNameInput) + siteNameInput = findViewById(R.id.siteNameInput) siteNameInput.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING - siteNameLayout = findViewById (R.id.siteNameInputLayout) + siteNameLayout = findViewById(R.id.siteNameInputLayout) - siteNameInputIcon = findViewById (R.id.siteNameInputIcon) + siteNameInputIcon = findViewById(R.id.siteNameInputIcon) siteNameInputIcon.setOnClickListener { iconFilePicker() } - siteNameIconPicker = findViewById (R.id.pickIcon) + siteNameIconPicker = findViewById(R.id.pickIcon) siteNameIconPicker.setOnClickListener { iconFilePicker() } siteNameInput.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable) { } - override fun beforeTextChanged(siteName: CharSequence, start: Int, count: Int, after: Int) { } - override fun onTextChanged(siteName: CharSequence, start: Int, before: Int, count: Int) { + override fun afterTextChanged(s: Editable) {} + override fun beforeTextChanged( + siteName: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + siteName: CharSequence, + start: Int, + before: Int, + count: Int + ) { thread { - val siteLogo = misc.getSiteIcon(siteName.toString(), siteNameInput.currentTextColor) + val siteLogo = + misc.getSiteIcon(siteName.toString(), siteNameInput.currentTextColor) if (siteLogo != null/* && iconFileName == null*/) { iconFileName = siteName.toString() runOnUiThread { @@ -290,195 +329,299 @@ class AddLogin : AppCompatActivity() { } }) - userNameInput = findViewById (R.id.userNameInput) + userNameInput = findViewById(R.id.userNameInput) userNameInput.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING - userNameInputLayout = findViewById (R.id.userNameInputLayout) + userNameInputLayout = findViewById(R.id.userNameInputLayout) - emailInput = findViewById (R.id.emailInput) + emailInput = findViewById(R.id.emailInput) emailInput.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING - passwordInput = findViewById (R.id.passwordInput) + passwordInput = findViewById(R.id.passwordInput) passwordInput.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING - passwordHistoryButton = findViewById (R.id.passwordHistoryButton) - passwordInputLayout = findViewById (R.id.passwordInputLayout) + passwordHistoryButton = findViewById(R.id.passwordHistoryButton) + passwordInputLayout = findViewById(R.id.passwordInputLayout) - secretInput = findViewById (R.id.secretInput) + secretInput = findViewById(R.id.secretInput) secretInput.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING backupCodesInput = findViewById(R.id.backupCodesInput) backupCodesInput.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING - backupCodesHelpButton = findViewById (R.id.backupCodesHelpButton) + backupCodesHelpButton = findViewById(R.id.backupCodesHelpButton) - siteUrlsHelpButton = findViewById (R.id.siteUrlsHelpButton) + siteUrlsHelpButton = findViewById(R.id.siteUrlsHelpButton) - notesInput = findViewById (R.id.notesInput) + notesInput = findViewById(R.id.notesInput) notesInput.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING - uppercaseSwitch = findViewById (R.id.uppercaseSwitch) - lowercaseSwitch = findViewById (R.id.lowercaseSwitch) - numbersSwitch = findViewById (R.id.numbersSwitch) - symbolsSwitch = findViewById (R.id.symbolsSwitch) - phrasesSwitch = findViewById (R.id.phrasesSwitch) + uppercaseSwitch = findViewById(R.id.uppercaseSwitch) + lowercaseSwitch = findViewById(R.id.lowercaseSwitch) + numbersSwitch = findViewById(R.id.numbersSwitch) + symbolsSwitch = findViewById(R.id.symbolsSwitch) + phrasesSwitch = findViewById(R.id.phrasesSwitch) - length = findViewById (R.id.length) - refreshPassword = findViewById (R.id.refresh) - passwordLength = findViewById (R.id.passwordLength) + length = findViewById(R.id.length) + refreshPassword = findViewById(R.id.refresh) + passwordLength = findViewById(R.id.passwordLength) - copyPassword = findViewById (R.id.copyPassword) + copyPassword = findViewById(R.id.copyPassword) - mfaTokenBox = findViewById (R.id.mfaTokenBox) - tokenPreview = findViewById (R.id.tokenPreview) - mfaProgress = findViewById (R.id.mfaProgress) + mfaTokenBox = findViewById(R.id.mfaTokenBox) + tokenPreview = findViewById(R.id.tokenPreview) + mfaProgress = findViewById(R.id.mfaProgress) - secretInput = findViewById (R.id.secretInput) + secretInput = findViewById(R.id.secretInput) secretInput.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING qrCodeButton = findViewById(R.id.qrCodeButton) passwordHistoryButton.visibility = View.GONE // password box logic - var upper = false; var lower = false; var numbers = false; var symbols = false; var passwordLengthInt = 32 - - uppercaseSwitch.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - upper = true - phrasesSwitch.isChecked = false - try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) - } catch (weirdArgs: IllegalArgumentException) { } - } else { - uppercaseSwitch.isChecked = false - lowercaseSwitch.isChecked = false - numbersSwitch.isChecked = false - symbolsSwitch.isChecked = false - upper = false - try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) - } catch (weirdArgs: IllegalArgumentException) { } + var upper = false; + var lower = false; + var numbers = false; + var symbols = false; + var passwordLengthInt = 32 + + uppercaseSwitch.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + upper = true + phrasesSwitch.isChecked = false + try { + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) + } catch (weirdArgs: IllegalArgumentException) { + } + } else { + uppercaseSwitch.isChecked = false + lowercaseSwitch.isChecked = false + numbersSwitch.isChecked = false + symbolsSwitch.isChecked = false + upper = false + try { + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) + } catch (weirdArgs: IllegalArgumentException) { } } - uppercaseSwitch.isChecked = true + } + uppercaseSwitch.isChecked = true - lowercaseSwitch.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - uppercaseSwitch.isChecked = true - lower = true - phrasesSwitch.isChecked = false - try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) - } catch (weirdArgs: IllegalArgumentException) { } - } else { - uppercaseSwitch.isChecked = false - lowercaseSwitch.isChecked = false - numbersSwitch.isChecked = false - symbolsSwitch.isChecked = false - lower = false - try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) - } catch (weirdArgs: IllegalArgumentException) { } + lowercaseSwitch.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + uppercaseSwitch.isChecked = true + lower = true + phrasesSwitch.isChecked = false + try { + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) + } catch (weirdArgs: IllegalArgumentException) { + } + } else { + uppercaseSwitch.isChecked = false + lowercaseSwitch.isChecked = false + numbersSwitch.isChecked = false + symbolsSwitch.isChecked = false + lower = false + try { + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) + } catch (weirdArgs: IllegalArgumentException) { } } - lowercaseSwitch.isChecked = true - - numbersSwitch.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - uppercaseSwitch.isChecked = true - lowercaseSwitch.isChecked = true - numbers = true - uppercaseSwitch.isEnabled - try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) - } catch (weirdArgs: IllegalArgumentException) { } - } else { - uppercaseSwitch.isChecked = false - lowercaseSwitch.isChecked = false - numbers = false - try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) - } catch (weirdArgs: IllegalArgumentException) { } + } + lowercaseSwitch.isChecked = true + + numbersSwitch.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + uppercaseSwitch.isChecked = true + lowercaseSwitch.isChecked = true + numbers = true + uppercaseSwitch.isEnabled + try { + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) + } catch (weirdArgs: IllegalArgumentException) { + } + } else { + uppercaseSwitch.isChecked = false + lowercaseSwitch.isChecked = false + numbers = false + try { + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) + } catch (weirdArgs: IllegalArgumentException) { } } + } - symbolsSwitch.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - uppercaseSwitch.isChecked = true - lowercaseSwitch.isChecked = true - numbersSwitch.isChecked = true - symbols = true - try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) - } catch (weirdArgs: IllegalArgumentException) { } - } else { - uppercaseSwitch.isChecked = false - lowercaseSwitch.isChecked = false - numbersSwitch.isChecked = false - symbols = false - try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) - } catch (weirdArgs: IllegalArgumentException) { } + symbolsSwitch.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + uppercaseSwitch.isChecked = true + lowercaseSwitch.isChecked = true + numbersSwitch.isChecked = true + symbols = true + try { + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) + } catch (weirdArgs: IllegalArgumentException) { + } + } else { + uppercaseSwitch.isChecked = false + lowercaseSwitch.isChecked = false + numbersSwitch.isChecked = false + symbols = false + try { + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) + } catch (weirdArgs: IllegalArgumentException) { } } + } - phrasesSwitch.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - uppercaseSwitch.isChecked = false - lowercaseSwitch.isChecked = false - numbersSwitch.isChecked = false - symbolsSwitch.isChecked = false + phrasesSwitch.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + uppercaseSwitch.isChecked = false + lowercaseSwitch.isChecked = false + numbersSwitch.isChecked = false + symbolsSwitch.isChecked = false - passwordLength.valueFrom = 2F - passwordLength.valueTo = 24F - passwordLength.value = 3F + passwordLength.valueFrom = 2F + passwordLength.valueTo = 24F + passwordLength.value = 3F - length.text = "${passwordLengthInt} words" + length.text = "${passwordLengthInt} words" - passwordInput.setText(utils.passphraseGenerator(passwordLengthInt)) - } else { - uppercaseSwitch.isChecked = true - lowercaseSwitch.isChecked = true - numbersSwitch.isChecked = true - symbolsSwitch.isChecked = true + passwordInput.setText(utils.passphraseGenerator(passwordLengthInt)) + } else { + uppercaseSwitch.isChecked = true + lowercaseSwitch.isChecked = true + numbersSwitch.isChecked = true + symbolsSwitch.isChecked = true - passwordLength.valueFrom = 4F - passwordLength.valueTo = 128F - passwordLength.value = 16F + passwordLength.valueFrom = 4F + passwordLength.valueTo = 128F + passwordLength.value = 16F - length.text = "${passwordLengthInt} characters" - try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) - } catch (weirdArgs: IllegalArgumentException) { } + length.text = "${passwordLengthInt} characters" + try { + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) + } catch (weirdArgs: IllegalArgumentException) { } } + } passwordInput.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) { passwordInput.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable) { } - - override fun beforeTextChanged(input: CharSequence, start: Int, count: Int, after: Int) { } + override fun afterTextChanged(s: Editable) {} + + override fun beforeTextChanged( + input: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } - override fun onTextChanged(input: CharSequence, start: Int, before: Int, count: Int) { + override fun onTextChanged( + input: CharSequence, + start: Int, + before: Int, + count: Int + ) { length.text = "${input.length} characters" } }) } } - passwordLength.addOnChangeListener (Slider.OnChangeListener { _, value, _ -> + passwordLength.addOnChangeListener(Slider.OnChangeListener { _, value, _ -> passwordLengthInt = value.toInt() if (phrasesSwitch.isChecked) { passwordLength.valueFrom = 3F passwordLength.valueTo = 24F length.text = "${passwordLengthInt} words" - passwordInput.setText(utils.passphraseGenerator (passwordLengthInt)) + passwordInput.setText(utils.passphraseGenerator(passwordLengthInt)) } else { passwordLength.valueFrom = 4F passwordLength.valueTo = 128F length.text = "${passwordLengthInt} characters" try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) } catch (weirdArgs: IllegalArgumentException) { lowercaseSwitch.isChecked = true } @@ -491,12 +634,20 @@ class AddLogin : AppCompatActivity() { Toast.makeText(applicationContext, "Password changed!", Toast.LENGTH_SHORT).show() if (phrasesSwitch.isChecked) { - passwordInput.setText(utils.passphraseGenerator (passwordLengthInt)) + passwordInput.setText(utils.passphraseGenerator(passwordLengthInt)) } if (!phrasesSwitch.isChecked) { try { - passwordInput.setText(utils.passwordGenerator (passwordLengthInt, upper, lower, numbers, symbols)) + passwordInput.setText( + utils.passwordGenerator( + passwordLengthInt, + upper, + lower, + numbers, + symbols + ) + ) } catch (weirdArgs: IllegalArgumentException) { lowercaseSwitch.isChecked = true } @@ -519,9 +670,9 @@ class AddLogin : AppCompatActivity() { Toast.makeText(applicationContext, "Copied!", Toast.LENGTH_SHORT).show() } - fun codeScannerDialog () { + fun codeScannerDialog() { val inflater = layoutInflater - val dialogView: View = inflater.inflate (R.layout.qr_code_scanner, null) + val dialogView: View = inflater.inflate(R.layout.qr_code_scanner, null) val dialogBuilder = MaterialAlertDialogBuilder(this) dialogBuilder .setView(dialogView) @@ -551,24 +702,38 @@ class AddLogin : AppCompatActivity() { if (decoded2faData?.secret != null) { secretInput.setText(decoded2faData.secret) - if (siteNameInput.text.isNullOrBlank()) siteNameInput.setText(decoded2faData.issuer ?: decoded2faData.account ?: decoded2faData.label ) - if (emailInput.text.isNullOrBlank() && misc.isValidEmail(decoded2faData.issuer ?: decoded2faData.account ?: decoded2faData.label)) emailInput.setText(decoded2faData.account) + if (siteNameInput.text.isNullOrBlank()) siteNameInput.setText( + decoded2faData.issuer ?: decoded2faData.account + ?: decoded2faData.label + ) + if (emailInput.text.isNullOrBlank() && misc.isValidEmail( + decoded2faData.issuer ?: decoded2faData.account + ?: decoded2faData.label + ) + ) emailInput.setText(decoded2faData.account) mfaDialog() } else { alertDialog.setTitle("Invalid QR code") - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Go back") { dialog, _ -> + alertDialog.setButton( + AlertDialog.BUTTON_POSITIVE, + "Go back" + ) { dialog, _ -> dialog.dismiss() } - alertDialog.setMessage ("This QR Code does not contain valid two-factor authentication data.") + alertDialog.setMessage("This QR Code does not contain valid two-factor authentication data.") alertDialog.show() } } catch (noSecret: Exception) { when (noSecret) { is IllegalStateException, is NullPointerException -> { - val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() + val alertDialog: AlertDialog = + MaterialAlertDialogBuilder(this).create() alertDialog.setTitle("Invalid QR code") alertDialog.setMessage("This QR Code does not contain valid two-factor authentication data.") - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Go back") { _, _ -> alertDialog.dismiss() } + alertDialog.setButton( + AlertDialog.BUTTON_POSITIVE, + "Go back" + ) { _, _ -> alertDialog.dismiss() } alertDialog.show() secretInput.text = null } @@ -593,12 +758,19 @@ class AddLogin : AppCompatActivity() { qrCodeDialog.dismiss() } - qrCodeDialog.setOnDismissListener (object : PopupMenu.OnDismissListener, DialogInterface.OnDismissListener { - override fun onDismiss(menu: PopupMenu?) { qrCodeDialog.dismiss() } - override fun onDismiss(p0: DialogInterface?) { qrCodeDialog.dismiss() } + qrCodeDialog.setOnDismissListener(object : PopupMenu.OnDismissListener, + DialogInterface.OnDismissListener { + override fun onDismiss(menu: PopupMenu?) { + qrCodeDialog.dismiss() + } + + override fun onDismiss(p0: DialogInterface?) { + qrCodeDialog.dismiss() + } }) - qrCodeDialog.setOnDismissListener (object : PopupMenu.OnDismissListener, DialogInterface.OnDismissListener { + qrCodeDialog.setOnDismissListener(object : PopupMenu.OnDismissListener, + DialogInterface.OnDismissListener { override fun onDismiss(menu: PopupMenu?) { codeScanner.stopPreview() codeScanner.releaseResources() @@ -612,7 +784,7 @@ class AddLogin : AppCompatActivity() { } qrCodeButton.setOnClickListener { - codeScannerDialog () + codeScannerDialog() } mfaTokenBox.visibility = View.GONE @@ -620,27 +792,40 @@ class AddLogin : AppCompatActivity() { mfaDialog() } - secretInput.addTextChangedListener (object : TextWatcher { - override fun afterTextChanged (s: Editable) { } - override fun beforeTextChanged (s: CharSequence, start: Int, count: Int, after: Int) { } - override fun onTextChanged (s: CharSequence, start: Int, before: Int, count: Int) { + secretInput.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable) {} + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { if (s.length >= 6) { try { - otpCode = GoogleAuthenticator(base32secret = secretInput.text.toString()).generate() - runOnUiThread { tokenPreview.text = otpCode!!.replace("...".toRegex(), "$0 ") } + otpCode = + GoogleAuthenticator(base32secret = secretInput.text.toString()).generate() + runOnUiThread { + tokenPreview.text = otpCode!!.replace("...".toRegex(), "$0 ") + } timer.scheduleAtFixedRate(object : TimerTask() { override fun run() { - val currentSeconds = SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt() - var halfMinuteElapsed = abs((60-currentSeconds)) + val currentSeconds = + SimpleDateFormat("s", Locale.getDefault()).format(Date()) + .toInt() + var halfMinuteElapsed = abs((60 - currentSeconds)) if (halfMinuteElapsed >= 30) halfMinuteElapsed -= 30 - try { mfaProgress.progress = halfMinuteElapsed } catch (_: Exception) { } + try { + mfaProgress.progress = halfMinuteElapsed + } catch (_: Exception) { + } if (halfMinuteElapsed == 30) { - otpCode = GoogleAuthenticator(base32secret = secretInput.text.toString()).generate() - runOnUiThread { tokenPreview.text = otpCode!!.replace("...".toRegex(), "$0 ") } + otpCode = + GoogleAuthenticator(base32secret = secretInput.text.toString()).generate() + runOnUiThread { + tokenPreview.text = + otpCode!!.replace("...".toRegex(), "$0 ") + } } } }, 0, 1000) // 1000 milliseconds = 1 second - } catch (timerError: IllegalStateException) { } + } catch (timerError: IllegalStateException) { + } mfaTokenBox.visibility = View.VISIBLE @@ -654,7 +839,10 @@ class AddLogin : AppCompatActivity() { val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() alertDialog.setTitle("Backup codes") alertDialog.setMessage("Some websites give you a set of backup 2FA codes in case you lose your 2FA-capable device.\n\nJust drag your cursor across them, then copy and paste them into this box. You can also manually type them in.\n\nKeyspace will auto-trim any whitespaces, numbers and commas.") - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Got it") { _, _ -> alertDialog.dismiss() } + alertDialog.setButton( + AlertDialog.BUTTON_POSITIVE, + "Got it" + ) { _, _ -> alertDialog.dismiss() } alertDialog.show() } @@ -666,7 +854,10 @@ class AddLogin : AppCompatActivity() { override fun onPaste() { val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager var pasteData = "" - if (clipboard.hasPrimaryClip() && clipboard.primaryClipDescription!!.hasMimeType(MIMETYPE_TEXT_PLAIN)) { + if (clipboard.hasPrimaryClip() && clipboard.primaryClipDescription!!.hasMimeType( + MIMETYPE_TEXT_PLAIN + ) + ) { val item = clipboard.primaryClip!!.getItemAt(0) pasteData = item.text.toString() } @@ -674,12 +865,24 @@ class AddLogin : AppCompatActivity() { if (pasteData.length > 8) { val trimmedBackupCodes = utils.backup2faCodesToList(pasteData) backupCodesAlertDialog.setTitle("Backup codes") - backupCodesAlertDialog.setMessage("Confirm before saving:\n\n${trimmedBackupCodes.joinToString("\n\n")}") - backupCodesAlertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Trim and paste") { dialog, _ -> + backupCodesAlertDialog.setMessage( + "Confirm before saving:\n\n${ + trimmedBackupCodes.joinToString( + "\n\n" + ) + }" + ) + backupCodesAlertDialog.setButton( + AlertDialog.BUTTON_POSITIVE, + "Trim and paste" + ) { dialog, _ -> backupCodesInput.setText(trimmedBackupCodes.joinToString(",\n")) dialog.dismiss() } - backupCodesAlertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Paste without trimming") { dialog, _ -> + backupCodesAlertDialog.setButton( + AlertDialog.BUTTON_NEGATIVE, + "Paste without trimming" + ) { dialog, _ -> backupCodesInput.setText(pasteData) dialog.dismiss() } @@ -692,7 +895,7 @@ class AddLogin : AppCompatActivity() { customFieldsView.layoutManager = LinearLayoutManager(this) customFieldsData = mutableListOf() - customFieldsAdapter = CustomFieldsAdapter (customFieldsData) + customFieldsAdapter = CustomFieldsAdapter(customFieldsData) customFieldsView.adapter = customFieldsAdapter addCustomFieldButton = findViewById(R.id.addCustomFieldButton) @@ -708,14 +911,21 @@ class AddLogin : AppCompatActivity() { siteUrlsHelpButton.setOnClickListener { val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() alertDialog.setTitle("Site URLs") - alertDialog.setMessage(Html.fromHtml(""" + alertDialog.setMessage( + Html.fromHtml( + """ Keyspace will automatically fill in your credentials on the websites in this list. By default, Keyspace uses an exact URL match.

You can also use regular expressions to do specific matches. For example,

[a-zA-Z0-9]*.?google.com

will match google.com, mail.google.com, and so on.

Tap here to learn more about regular expressions. - """.trimIndent())) - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Got it") { _, _ -> alertDialog.dismiss() } + """.trimIndent() + ) + ) + alertDialog.setButton( + AlertDialog.BUTTON_POSITIVE, + "Got it" + ) { _, _ -> alertDialog.dismiss() } alertDialog.show() val textView: TextView = alertDialog.findViewById(android.R.id.message)!! textView.movementMethod = LinkMovementMethod.getInstance() @@ -725,7 +935,7 @@ class AddLogin : AppCompatActivity() { siteUrlsView.layoutManager = LinearLayoutManager(this) siteUrlsData = mutableListOf() - siteUrlsAdapter = SiteUrlsAdapter (siteUrlsData) + siteUrlsAdapter = SiteUrlsAdapter(siteUrlsData) siteUrlsView.adapter = siteUrlsAdapter addSiteUrlButton = findViewById(R.id.addSiteUrlButton) @@ -765,13 +975,23 @@ class AddLogin : AppCompatActivity() { private fun loadLogin(login: IOUtilities.Login): Boolean { favorite = if (login.favorite) { - favoriteButton.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_baseline_star_24)); true + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + this, + R.drawable.ic_baseline_star_24 + ) + ); true } else { - favoriteButton.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_baseline_star_border_24)); false + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + this, + R.drawable.ic_baseline_star_border_24 + ) + ); false } tagId = login.tagId - tagPicker = AddTag (tagId, applicationContext, this@AddLogin, keyring) + tagPicker = AddTag(tagId, applicationContext, this@AddLogin, keyring) siteNameInput.setText(login.name) @@ -811,12 +1031,18 @@ class AddLogin : AppCompatActivity() { val passwordHistoryBox: View = inflater.inflate(R.layout.password_history, null) builder.setView(passwordHistoryBox) - passwordHistoryBox.startAnimation(loadAnimation(applicationContext, R.anim.from_top)) + passwordHistoryBox.startAnimation( + loadAnimation( + applicationContext, + R.anim.from_top + ) + ) - passwordHistoryView = passwordHistoryBox.findViewById(R.id.passwordHistoryRecycler) as RecyclerView + passwordHistoryView = + passwordHistoryBox.findViewById(R.id.passwordHistoryRecycler) as RecyclerView passwordHistoryView.layoutManager = LinearLayoutManager(this) - passwordHistoryAdapter = PasswordHistoryAdapter (passwordHistoryData) + passwordHistoryAdapter = PasswordHistoryAdapter(passwordHistoryData) passwordHistoryView.adapter = passwordHistoryAdapter passwordHistoryAdapter.notifyItemInserted(passwordHistoryData.size) passwordHistoryView.invalidate() @@ -831,7 +1057,7 @@ class AddLogin : AppCompatActivity() { if (login.loginData.siteUrls != null) { siteUrlsData = login.loginData.siteUrls - siteUrlsAdapter = SiteUrlsAdapter (siteUrlsData) + siteUrlsAdapter = SiteUrlsAdapter(siteUrlsData) siteUrlsView.adapter = siteUrlsAdapter siteUrlsAdapter.notifyItemInserted(siteUrlsData.size) siteUrlsView.invalidate() @@ -843,7 +1069,7 @@ class AddLogin : AppCompatActivity() { if (login.customFields != null) { customFieldsData = login.customFields - customFieldsAdapter = CustomFieldsAdapter (customFieldsData) + customFieldsAdapter = CustomFieldsAdapter(customFieldsData) customFieldsView.adapter = customFieldsAdapter customFieldsAdapter.notifyItemInserted(customFieldsData.size) customFieldsView.invalidate() @@ -851,16 +1077,23 @@ class AddLogin : AppCompatActivity() { customFieldsView.scheduleLayoutAnimation() } - Handler().postDelayed({ runOnUiThread { - iconFileName = login.iconFile - if (iconFileName != null) siteNameInputIcon.setImageDrawable(misc.getSiteIcon(iconFileName!!, siteNameInput.currentTextColor)) - else siteNameInputIcon.setImageDrawable(getDrawable(R.drawable.ic_baseline_website_24)) - } }, 100) + Handler().postDelayed({ + runOnUiThread { + iconFileName = login.iconFile + if (iconFileName != null) siteNameInputIcon.setImageDrawable( + misc.getSiteIcon( + iconFileName!!, + siteNameInput.currentTextColor + ) + ) + else siteNameInputIcon.setImageDrawable(getDrawable(R.drawable.ic_baseline_website_24)) + } + }, 100) return true } - private fun mfaDialog () { + private fun mfaDialog() { if (secretInput.text.toString().trim().isNotEmpty()) { var otpCode = GoogleAuthenticator(base32secret = secretInput.text.toString()).generate() @@ -873,14 +1106,16 @@ class AddLogin : AppCompatActivity() { val qrCode = accountInfoBox.findViewById(R.id.qrCode) val mfaLabel = accountInfoBox.findViewById(R.id.mfaLabel) - if (siteNameInput.text.toString().trim().isNotEmpty()) mfaLabel.text = siteNameInput.text - else mfaLabel.visibility = View.GONE + if (siteNameInput.text.toString().trim().isNotEmpty()) mfaLabel.text = + siteNameInput.text + else mfaLabel.visibility = View.GONE val mfaCode = accountInfoBox.findViewById(R.id.mfaCode) mfaCode.text = otpCode.replace("...".toRegex(), "$0 ") val accountName = accountInfoBox.findViewById(R.id.accountName) - if (emailInput.text.toString().trim().isNotEmpty()) accountName.text = emailInput.text.toString() + if (emailInput.text.toString().trim().isNotEmpty()) accountName.text = + emailInput.text.toString() else accountName.visibility = View.GONE val secret = accountInfoBox.findViewById(R.id.secret) @@ -892,17 +1127,23 @@ class AddLogin : AppCompatActivity() { runOnUiThread { mfaCode.text = otpCode!!.replace("...".toRegex(), "$0 ") } timer.scheduleAtFixedRate(object : TimerTask() { override fun run() { - val currentSeconds = SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt() - var halfMinuteElapsed = abs((60-currentSeconds)) + val currentSeconds = + SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt() + var halfMinuteElapsed = abs((60 - currentSeconds)) if (halfMinuteElapsed >= 30) halfMinuteElapsed -= 30 - try { mfaProgress.progress = halfMinuteElapsed } catch (_: Exception) { } + try { + mfaProgress.progress = halfMinuteElapsed + } catch (_: Exception) { + } if (halfMinuteElapsed == 30) { - otpCode = GoogleAuthenticator(base32secret = secretInput.text.toString()).generate() + otpCode = + GoogleAuthenticator(base32secret = secretInput.text.toString()).generate() runOnUiThread { mfaCode.text = otpCode.replace("...".toRegex(), "$0 ") } } } }, 0, 1000) // 1000 milliseconds = 1 second - } catch (timerError: IllegalStateException) { } + } catch (timerError: IllegalStateException) { + } val mfaMode = accountInfoBox.findViewById(R.id.mfaMode) mfaMode.visibility = View.GONE @@ -911,7 +1152,7 @@ class AddLogin : AppCompatActivity() { siteNameInput.text.toString() } else if (emailInput.text.toString().trim().isNotEmpty()) { emailInput.text.toString() - } else { + } else { "Unknown account" } @@ -936,87 +1177,118 @@ class AddLogin : AppCompatActivity() { } } - private fun saveItem () { - var dateCreated = Instant.now().epochSecond - - if (itemId != null) { - dateCreated = io.getLogin(itemId!!, vault)?.dateCreated!! - vault.login?.remove(io.getLogin(itemId!!, vault)) - - if (login.loginData != null) { - if (!login.loginData!!.password.isNullOrEmpty()) { - if (passwordInput.text.toString() != login.loginData?.password) { - passwordHistoryData.add ( - IOUtilities.Password( - password = passwordInput.text.toString(), - created = Instant.now().epochSecond - ) - ) - } - } - } - - } else { + private fun saveItem() { + if (!::passwordHistoryData.isInitialized) { passwordHistoryData = mutableListOf() - passwordHistoryData.add ( - IOUtilities.Password( - password = passwordInput.text.toString(), - created = Instant.now().epochSecond - ) - ) } - val data = IOUtilities.Login( - id = itemId ?: UUID.randomUUID().toString(), - organizationId = null, - type = io.TYPE_LOGIN, - name = siteNameInput.text.toString(), - notes = notesInput.text.toString(), - favorite = favorite, + itemPersistence.saveLogin( + siteName = siteNameInput.text.toString(), + siteUrlsData = siteUrlsData, + userName = userNameInput.text.toString(), + email = emailInput.text.toString(), + password = passwordInput.text.toString(), + passwordHistoryData = passwordHistoryData, + secret = secretInput.text.toString(), + backupCodes = backupCodesInput.text.toString(), + iconFileName = iconFileName, + isFavorite = favorite, tagId = tagPicker.getSelectedTagId() ?: tagId, - loginData = IOUtilities.LoginData( - username = userNameInput.text.toString(), - password = passwordInput.text.toString(), - passwordHistory = if (passwordHistoryData.size > 0) passwordHistoryData else null, - email = emailInput.text.toString(), - totp = IOUtilities.Totp( - secret = secretInput.text.toString(), - backupCodes = backupCodesInput.text?.toString()?.replace("\n", "")?.split(",")?.toMutableList() - ), - siteUrls = if (siteUrlsData.size > 0) siteUrlsData else null - ), - dateCreated = dateCreated, - dateModified = Instant.now().epochSecond, - frequencyAccessed = 0, - customFields = customFieldsData, - iconFile = iconFileName - ) - - val encryptedLogin = io.encryptLogin(data) - - vault.login?.add (encryptedLogin) - io.writeVault(vault) - - if (itemId != null) network.writeQueueTask (encryptedLogin, mode = network.MODE_PUT) - else network.writeQueueTask (encryptedLogin, mode = network.MODE_POST) - - crypto.secureStartActivity ( - nextActivity = Dashboard(), - nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), - keyring = keyring, - itemId = null - ) - + notes = notesInput.text.toString(), + customFieldsData = customFieldsData + ) { error -> + siteNameInput.error = error.siteNameError + emailInput.error = error.emailError + secretInput.error = error.secretError + } } - override fun onBackPressed () { - try { qrCodeDialog.dismiss() } catch (qrCodeBoxUninitialized: UninitializedPropertyAccessException) { } + //region Original saveItem() +// private fun saveItem () { +// var dateCreated = Instant.now().epochSecond +// +// if (itemId != null) { +// dateCreated = io.getLogin(itemId!!, vault)?.dateCreated!! +// vault.login?.remove(io.getLogin(itemId!!, vault)) +// +// if (login.loginData != null) { +// if (!login.loginData!!.password.isNullOrEmpty()) { +// if (passwordInput.text.toString() != login.loginData?.password) { +// passwordHistoryData.add ( +// IOUtilities.Password( +// password = passwordInput.text.toString(), +// created = Instant.now().epochSecond +// ) +// ) +// } +// } +// } +// +// } else { +// passwordHistoryData = mutableListOf() +// passwordHistoryData.add ( +// IOUtilities.Password( +// password = passwordInput.text.toString(), +// created = Instant.now().epochSecond +// ) +// ) +// } +// +// val data = IOUtilities.Login( +// id = itemId ?: UUID.randomUUID().toString(), +// organizationId = null, +// type = io.TYPE_LOGIN, +// name = siteNameInput.text.toString(), +// notes = notesInput.text.toString(), +// favorite = favorite, +// tagId = tagPicker.getSelectedTagId() ?: tagId, +// loginData = IOUtilities.LoginData( +// username = userNameInput.text.toString(), +// password = passwordInput.text.toString(), +// passwordHistory = if (passwordHistoryData.size > 0) passwordHistoryData else null, +// email = emailInput.text.toString(), +// totp = IOUtilities.Totp( +// secret = secretInput.text.toString(), +// backupCodes = backupCodesInput.text?.toString()?.replace("\n", "")?.split(",")?.toMutableList() +// ), +// siteUrls = if (siteUrlsData.size > 0) siteUrlsData else null +// ), +// dateCreated = dateCreated, +// dateModified = Instant.now().epochSecond, +// frequencyAccessed = 0, +// customFields = customFieldsData, +// iconFile = iconFileName +// ) +// +// val encryptedLogin = io.encryptLogin(data) +// +// vault.login?.add (encryptedLogin) +// io.writeVault(vault) +// +// if (itemId != null) network.writeQueueTask (encryptedLogin, mode = network.MODE_PUT) +// else network.writeQueueTask (encryptedLogin, mode = network.MODE_POST) +// +// crypto.secureStartActivity ( +// nextActivity = Dashboard(), +// nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), +// keyring = keyring, +// itemId = null +// ) +// +// } + //endregion Original saveItem() + + override fun onBackPressed() { + try { + qrCodeDialog.dismiss() + } catch (qrCodeBoxUninitialized: UninitializedPropertyAccessException) { + } val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() alertDialog.setTitle("Confirm exit") alertDialog.setMessage("Would you like to go back to the Dashboard?") alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Exit") { dialog, _ -> - crypto.secureStartActivity ( + crypto.secureStartActivity( nextActivity = Dashboard(), nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), keyring = keyring, @@ -1025,7 +1297,10 @@ class AddLogin : AppCompatActivity() { super.onBackPressed() tagIdGrabber.removeCallbacksAndMessages(null) } - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Cancel") { dialog, _ -> dialog.dismiss() } + alertDialog.setButton( + AlertDialog.BUTTON_NEGATIVE, + "Cancel" + ) { dialog, _ -> dialog.dismiss() } alertDialog.show() } @@ -1043,7 +1318,10 @@ class AddLogin : AppCompatActivity() { System.gc() finish() finishAffinity() - try { qrCodeDialog.dismiss() } catch (qrCodeBoxUninitialized: UninitializedPropertyAccessException) { } + try { + qrCodeDialog.dismiss() + } catch (qrCodeBoxUninitialized: UninitializedPropertyAccessException) { + } } override fun onPause() { @@ -1084,10 +1362,18 @@ class AddLogin : AppCompatActivity() { super.onUserLeaveHint() } - inner class CustomFieldsAdapter (private val customFields: MutableList) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : ViewHolder { // create new views - val customFieldsView: View = LayoutInflater.from(parent.context).inflate(R.layout.custom_field, parent, false) - customFieldsView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + inner class CustomFieldsAdapter(private val customFields: MutableList) : + RecyclerView.Adapter() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { // create new views + val customFieldsView: View = + LayoutInflater.from(parent.context).inflate(R.layout.custom_field, parent, false) + customFieldsView.layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, + RecyclerView.LayoutParams.WRAP_CONTENT + ) return ViewHolder(customFieldsView) } @@ -1096,11 +1382,13 @@ class AddLogin : AppCompatActivity() { var hidden = false - customFieldView.fieldName.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - customFieldView.fieldValue.imeOptions = EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING + customFieldView.fieldName.imeOptions = + EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING + customFieldView.fieldValue.imeOptions = + EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - customFieldView.fieldName.setText (customField.name) - customFieldView.fieldValue.setText (customField.value) + customFieldView.fieldName.setText(customField.name) + customFieldView.fieldValue.setText(customField.value) if (customField.hidden) { customFieldView.fieldValue.transformationMethod = PasswordTransformationMethod() @@ -1113,18 +1401,42 @@ class AddLogin : AppCompatActivity() { } customFieldView.fieldName.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable) { } - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} - override fun onTextChanged(data: CharSequence, start: Int, before: Int, count: Int) { + override fun afterTextChanged(s: Editable) {} + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + data: CharSequence, + start: Int, + before: Int, + count: Int + ) { addCustomFieldButton.isEnabled = data.isNotEmpty() customFieldsData[customFieldView.adapterPosition].name = data.toString() } }) customFieldView.fieldValue.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable) { } - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { } - override fun onTextChanged(data: CharSequence, start: Int, before: Int, count: Int) { + override fun afterTextChanged(s: Editable) {} + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + data: CharSequence, + start: Int, + before: Int, + count: Int + ) { addCustomFieldButton.isEnabled = data.isNotEmpty() customFieldsData[customFieldView.adapterPosition].value = data.toString() } @@ -1132,12 +1444,17 @@ class AddLogin : AppCompatActivity() { customFieldView.deleteIcon.setOnClickListener { view -> addCustomFieldButton.isEnabled = true - Toast.makeText(applicationContext, "Deleted \"${customFieldView.fieldName.text}\"", Toast.LENGTH_SHORT).show() + Toast.makeText( + applicationContext, + "Deleted \"${customFieldView.fieldName.text}\"", + Toast.LENGTH_SHORT + ).show() customFieldView.fieldName.clearFocus() customFieldView.fieldValue.clearFocus() try { customFieldsData.remove(customFieldsData[customFieldView.adapterPosition]) - } catch (noItemsLeft: IndexOutOfBoundsException) { } + } catch (noItemsLeft: IndexOutOfBoundsException) { + } customFieldsAdapter.notifyItemRemoved(position) customFieldsView.invalidate() customFieldsView.refreshDrawableState() @@ -1165,18 +1482,29 @@ class AddLogin : AppCompatActivity() { return customFields.size } - inner class ViewHolder (itemLayoutView: View) : RecyclerView.ViewHolder(itemLayoutView) { + inner class ViewHolder(itemLayoutView: View) : RecyclerView.ViewHolder(itemLayoutView) { var fieldName: EditText = itemLayoutView.findViewById(R.id.field_name) as EditText - var fieldValue: EditText = itemLayoutView.findViewById(R.id.field_value) as EditText - var deleteIcon: ImageView = itemLayoutView.findViewById(R.id.deleteCustomFieldButton) as ImageView - var hideIcon: ImageView = itemLayoutView.findViewById(R.id.hideCustomFieldButton) as ImageView + var fieldValue: EditText = + itemLayoutView.findViewById(R.id.field_value) as EditText + var deleteIcon: ImageView = + itemLayoutView.findViewById(R.id.deleteCustomFieldButton) as ImageView + var hideIcon: ImageView = + itemLayoutView.findViewById(R.id.hideCustomFieldButton) as ImageView } } - inner class SiteUrlsAdapter (private val siteUrls: MutableList) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : ViewHolder { // create new views - val siteUrlsView: View = LayoutInflater.from(parent.context).inflate(R.layout.site_url, parent, false) - siteUrlsView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + inner class SiteUrlsAdapter(private val siteUrls: MutableList) : + RecyclerView.Adapter() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { // create new views + val siteUrlsView: View = + LayoutInflater.from(parent.context).inflate(R.layout.site_url, parent, false) + siteUrlsView.layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, + RecyclerView.LayoutParams.WRAP_CONTENT + ) return ViewHolder(siteUrlsView) } @@ -1184,12 +1512,24 @@ class AddLogin : AppCompatActivity() { val siteUrl = siteUrlsData[siteUrlView.adapterPosition] siteUrlView.siteUrl.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING - siteUrlView.siteUrl.setText (siteUrl) + siteUrlView.siteUrl.setText(siteUrl) siteUrlView.siteUrl.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable) { } - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { } - override fun onTextChanged(data: CharSequence, start: Int, before: Int, count: Int) { + override fun afterTextChanged(s: Editable) {} + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + data: CharSequence, + start: Int, + before: Int, + count: Int + ) { addSiteUrlButton.isEnabled = data.isNotEmpty() siteUrlsData[siteUrlView.adapterPosition] = data.toString() } @@ -1198,10 +1538,15 @@ class AddLogin : AppCompatActivity() { siteUrlView.deleteIcon.setOnClickListener { view -> addSiteUrlButton.isEnabled = true siteUrlView.siteUrl.clearFocus() - Toast.makeText(applicationContext, "Deleted \"${siteUrlView.siteUrl.text}\"", Toast.LENGTH_SHORT).show() + Toast.makeText( + applicationContext, + "Deleted \"${siteUrlView.siteUrl.text}\"", + Toast.LENGTH_SHORT + ).show() try { siteUrlsData.remove(siteUrlsData[siteUrlView.adapterPosition]) - } catch (noItemsLeft: IndexOutOfBoundsException) { } + } catch (noItemsLeft: IndexOutOfBoundsException) { + } siteUrlsAdapter.notifyItemRemoved(siteUrlView.adapterPosition) siteUrlsView.invalidate() siteUrlsView.refreshDrawableState() @@ -1213,16 +1558,25 @@ class AddLogin : AppCompatActivity() { return siteUrls.size } - inner class ViewHolder (itemLayoutView: View) : RecyclerView.ViewHolder(itemLayoutView) { + inner class ViewHolder(itemLayoutView: View) : RecyclerView.ViewHolder(itemLayoutView) { var siteUrl: EditText = itemLayoutView.findViewById(R.id.siteUrlInput) as EditText - var deleteIcon: ImageView = itemLayoutView.findViewById(R.id.deleteSiteUrlButton) as ImageView + var deleteIcon: ImageView = + itemLayoutView.findViewById(R.id.deleteSiteUrlButton) as ImageView } } - inner class PasswordHistoryAdapter (private val oldPasswords: MutableList) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : ViewHolder { // create new views - val passwordHistoryView: View = LayoutInflater.from(parent.context).inflate(R.layout.password_history_card, parent, false) - passwordHistoryView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + inner class PasswordHistoryAdapter(private val oldPasswords: MutableList) : + RecyclerView.Adapter() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { // create new views + val passwordHistoryView: View = LayoutInflater.from(parent.context) + .inflate(R.layout.password_history_card, parent, false) + passwordHistoryView.layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, + RecyclerView.LayoutParams.WRAP_CONTENT + ) return ViewHolder(passwordHistoryView) } @@ -1231,8 +1585,8 @@ class AddLogin : AppCompatActivity() { val calendar = Calendar.getInstance(Locale.getDefault()) calendar.timeInMillis = passwordHistory.created * 1000L - val date = DateFormat.format("MMM dd, yyyy",calendar).toString() - val time = DateFormat.format("HH:mm",calendar).toString() + val date = DateFormat.format("MMM dd, yyyy", calendar).toString() + val time = DateFormat.format("HH:mm", calendar).toString() passwordHistoryView.oldPassword.text = passwordHistory.password passwordHistoryView.createdDate.text = date @@ -1240,7 +1594,10 @@ class AddLogin : AppCompatActivity() { passwordHistoryView.copyOldPasswordButton.setOnClickListener { view -> val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Keyspace", passwordHistoryView.oldPassword.text.toString()) + val clip = ClipData.newPlainText( + "Keyspace", + passwordHistoryView.oldPassword.text.toString() + ) clipboard.setPrimaryClip(clip) Toast.makeText(applicationContext, "Copied!", Toast.LENGTH_SHORT).show() passwordCopied = true @@ -1252,11 +1609,15 @@ class AddLogin : AppCompatActivity() { return oldPasswords.size } - inner class ViewHolder (itemLayoutView: View) : RecyclerView.ViewHolder(itemLayoutView) { - var oldPassword: TextView = itemLayoutView.findViewById(R.id.oldPassword) as TextView - var copyOldPasswordButton: Button = itemLayoutView.findViewById(R.id.copyOldPasswordButton) as Button - var createdDate: TextView = itemLayoutView.findViewById(R.id.createdDate) as TextView - var createdTime: TextView = itemLayoutView.findViewById(R.id.createdTime) as TextView + inner class ViewHolder(itemLayoutView: View) : RecyclerView.ViewHolder(itemLayoutView) { + var oldPassword: TextView = + itemLayoutView.findViewById(R.id.oldPassword) as TextView + var copyOldPasswordButton: Button = + itemLayoutView.findViewById(R.id.copyOldPasswordButton) as Button + var createdDate: TextView = + itemLayoutView.findViewById(R.id.createdDate) as TextView + var createdTime: TextView = + itemLayoutView.findViewById(R.id.createdTime) as TextView } } @@ -1265,7 +1626,11 @@ class AddLogin : AppCompatActivity() { constructor(context: Context?) : super(context!!) constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context!!, attrs, defStyleAttr) + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( + context!!, + attrs, + defStyleAttr + ) fun setUpdateListener(listener: UpdateListener?) { this.listener = listener @@ -1288,37 +1653,70 @@ class AddLogin : AppCompatActivity() { } } - private fun iconFilePicker () { + private fun iconFilePicker() { val builder = MaterialAlertDialogBuilder(this@AddLogin) builder.setCancelable(true) val iconsBox: View = layoutInflater.inflate(R.layout.icon_picker_dialog, null) builder.setView(iconsBox) - iconsBox.startAnimation(AnimationUtils.loadAnimation(applicationContext, R.anim.from_bottom)) + iconsBox.startAnimation( + AnimationUtils.loadAnimation( + applicationContext, + R.anim.from_bottom + ) + ) val dialog = builder.create() dialog.show() val iconFileNames = misc.getSiteIconFilenames() + class GridAdapter(var context: Context, filenames: ArrayList) : BaseAdapter() { var listFiles: ArrayList - init { listFiles = filenames } - override fun getCount(): Int { return listFiles.size } - override fun getItem(position: Int): Any { return listFiles[position] } - override fun getItemId(position: Int): Long { return position.toLong() } + + init { + listFiles = filenames + } + + override fun getCount(): Int { + return listFiles.size + } + + override fun getItem(position: Int): Any { + return listFiles[position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + @SuppressLint("ViewHolder") override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = (context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater).inflate(R.layout.site_icon, null) + val view = + (context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater).inflate( + R.layout.site_icon, + null + ) val icon = view.findViewById(R.id.icon) - icon.setImageDrawable(misc.getSiteIcon(listFiles[position], siteNameInput.currentTextColor)) + icon.setImageDrawable( + misc.getSiteIcon( + listFiles[position], + siteNameInput.currentTextColor + ) + ) val iconName = view.findViewById(R.id.iconName) iconName.text = listFiles[position].replace("_", "") icon.setOnClickListener { iconFileName = listFiles[position] - siteNameInputIcon.setImageDrawable(misc.getSiteIcon(listFiles[position], siteNameInput.currentTextColor)) + siteNameInputIcon.setImageDrawable( + misc.getSiteIcon( + listFiles[position], + siteNameInput.currentTextColor + ) + ) dialog.dismiss() } diff --git a/app/src/main/kotlin/cloud/keyspace/android/AddNote.kt b/app/src/main/kotlin/cloud/keyspace/android/AddNote.kt index 928f0b1..1294933 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/AddNote.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/AddNote.kt @@ -28,7 +28,6 @@ import androidx.core.content.ContextCompat import androidx.core.widget.doOnTextChanged import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat -import com.fasterxml.jackson.module.kotlin.SequenceSerializer.properties import com.github.dhaval2404.colorpicker.MaterialColorPickerDialog import com.github.dhaval2404.colorpicker.listener.ColorListener import com.google.android.material.button.MaterialButton @@ -98,19 +97,27 @@ class AddNote : AppCompatActivity() { lateinit var configData: SharedPreferences + private lateinit var itemPersistence: ItemPersistence + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.edit_note) - configData = getSharedPreferences(applicationContext.packageName + "_configuration_data", MODE_PRIVATE) + configData = getSharedPreferences( + applicationContext.packageName + "_configuration_data", + MODE_PRIVATE + ) val allowScreenshots = configData.getBoolean("allowScreenshots", false) - if (!allowScreenshots) window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + if (!allowScreenshots) window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) - utils = MiscUtilities (applicationContext) + utils = MiscUtilities(applicationContext) crypto = CryptoUtilities(applicationContext, this) - utils = MiscUtilities (applicationContext) + utils = MiscUtilities(applicationContext) crypto = CryptoUtilities(applicationContext, this) val intent = intent @@ -120,7 +127,8 @@ class AddNote : AppCompatActivity() { if ("android.intent.action.SEND" == action && type != null && "text/plain" == type) { val biometricPromptThread = Handler(Looper.getMainLooper()) - val executor: Executor = ContextCompat.getMainExecutor(this@AddNote) // execute on different thread awaiting response + val executor: Executor = + ContextCompat.getMainExecutor(this@AddNote) // execute on different thread awaiting response try { val biometricManager = BiometricManager.from(this@AddNote) @@ -142,18 +150,29 @@ class AddNote : AppCompatActivity() { val decryptingDialog = decryptingDialogBuilder.create() decryptingDialog.show() - decryptingDialogBox.findViewById(R.id.authenticateButton).visibility = View.GONE - val authenticateDescription = decryptingDialogBox.findViewById(R.id.fingerprint_description) + decryptingDialogBox.findViewById(R.id.authenticateButton).visibility = + View.GONE + val authenticateDescription = + decryptingDialogBox.findViewById(R.id.fingerprint_description) authenticateDescription.text = "Enter credentials to continue" - val authenticationIcon = decryptingDialogBox.findViewById(R.id.fingerprint_icon) + val authenticationIcon = + decryptingDialogBox.findViewById(R.id.fingerprint_icon) authenticationIcon.setPadding(0, 50, 0, 0) - val authenticateTitle = decryptingDialogBox.findViewById(R.id.fingerprint_title) - val keystoreProgress = decryptingDialogBox.findViewById(R.id.keystoreProgress) - - val keyguardToUnlock = AnimatedVectorDrawableCompat.create(applicationContext, R.drawable.keyguardtolock) - val fingerprintToUnlock = AnimatedVectorDrawableCompat.create(applicationContext, R.drawable.fingerprinttolock) + val authenticateTitle = + decryptingDialogBox.findViewById(R.id.fingerprint_title) + val keystoreProgress = + decryptingDialogBox.findViewById(R.id.keystoreProgress) + + val keyguardToUnlock = AnimatedVectorDrawableCompat.create( + applicationContext, + R.drawable.keyguardtolock + ) + val fingerprintToUnlock = AnimatedVectorDrawableCompat.create( + applicationContext, + R.drawable.fingerprinttolock + ) val zoomSpin = loadAnimation(applicationContext, R.anim.zoom_spin) val authenticationCallback = object : BiometricPrompt.AuthenticationCallback() { @@ -161,10 +180,18 @@ class AddNote : AppCompatActivity() { Handler().postDelayed({ authenticateDescription.text = "Ed25519 public key" - Handler().postDelayed({ authenticateDescription.text = "Ed25519 private key" - Handler().postDelayed({ authenticateDescription.text = "XChaCha20-Poly1305 symmetric key" }, 50) }, 100) }, 100) - - val keyringThread = Thread { keyring = crypto.retrieveKeys(crypto.getKeystoreMasterKey())!! } + Handler().postDelayed({ + authenticateDescription.text = "Ed25519 private key" + Handler().postDelayed({ + authenticateDescription.text = + "XChaCha20-Poly1305 symmetric key" + }, 50) + }, 100) + }, 100) + + val keyringThread = Thread { + keyring = crypto.retrieveKeys(crypto.getKeystoreMasterKey())!! + } keyringThread.start() if (applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)) { // Check if strongbox exists @@ -173,10 +200,13 @@ class AddNote : AppCompatActivity() { authenticateTitle.text = "Reading HAL Keystore" } else authenticateTitle.text = "Reading Keystore" - if (utils.biometricsExist()) authenticationIcon.setImageDrawable(fingerprintToUnlock) + if (utils.biometricsExist()) authenticationIcon.setImageDrawable( + fingerprintToUnlock + ) else authenticationIcon.setImageDrawable(keyguardToUnlock) - fingerprintToUnlock!!.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() { + fingerprintToUnlock!!.registerAnimationCallback(object : + Animatable2Compat.AnimationCallback() { override fun onAnimationEnd(drawable: Drawable) { authenticationIcon.setImageDrawable(applicationContext.getDrawable(R.drawable.ic_baseline_check_24)) keyringThread.join() @@ -188,16 +218,24 @@ class AddNote : AppCompatActivity() { authenticateTitle.text = "All done!" } - override fun onAnimationRepeat(animation: Animation) { } + override fun onAnimationRepeat(animation: Animation) {} @SuppressLint("UseCompatLoadingForDrawables") override fun onAnimationEnd(animation: Animation) { keyringThread.join() - network = NetworkUtilities(applicationContext, this@AddNote, keyring) + network = NetworkUtilities( + applicationContext, + this@AddNote, + keyring + ) - network = NetworkUtilities(applicationContext, this@AddNote, keyring) + network = NetworkUtilities( + applicationContext, + this@AddNote, + keyring + ) io = IOUtilities(applicationContext, this@AddNote, keyring) @@ -207,16 +245,26 @@ class AddNote : AppCompatActivity() { if (itemId != null) { note = io.decryptNote(io.getNote(itemId!!, vault)!!) - loadNote (note) + loadNote(note) frequencyAccessed = note.frequencyAccessed!! } + itemPersistence = ItemPersistence( + applicationContext = applicationContext, + appCompatActivity = this@AddNote, + keyring = keyring, + itemId = itemId + ) + decryptingDialog.dismiss() biometricPromptThread.removeCallbacksAndMessages(null) - noteEditor.setText(intent.getStringExtra("android.intent.extra.TEXT").toString()) + noteEditor.setText( + intent.getStringExtra("android.intent.extra.TEXT") + .toString() + ) } @@ -232,22 +280,34 @@ class AddNote : AppCompatActivity() { Log.d("Keyspace", "Authentication successful") } - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { // Authentication error. Verify error code and message + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence + ) { // Authentication error. Verify error code and message biometricPromptThread.removeCallbacksAndMessages(null) - (applicationContext.getSystemService(VIBRATOR_SERVICE) as Vibrator).vibrate(150) + (applicationContext.getSystemService(VIBRATOR_SERVICE) as Vibrator).vibrate( + 150 + ) Log.d("Keyspace", "Authentication canceled") - Toast.makeText(applicationContext, "Couldn't unlock Keyspace due to incorrect credentials.", Toast.LENGTH_LONG).show() + Toast.makeText( + applicationContext, + "Couldn't unlock Keyspace due to incorrect credentials.", + Toast.LENGTH_LONG + ).show() finishAffinity() } override fun onAuthenticationFailed() { // Authentication failed. User may not have been recognized biometricPromptThread.removeCallbacksAndMessages(null) - (applicationContext.getSystemService(VIBRATOR_SERVICE) as Vibrator).vibrate(150) + (applicationContext.getSystemService(VIBRATOR_SERVICE) as Vibrator).vibrate( + 150 + ) Log.d("Keyspace", "Incorrect credentials supplied") } } - val biometricPrompt = BiometricPrompt(this@AddNote, executor, authenticationCallback) + val biometricPrompt = + BiometricPrompt(this@AddNote, executor, authenticationCallback) val builder = BiometricPrompt.PromptInfo.Builder() .setTitle(resources.getString(R.string.app_name)) @@ -273,7 +333,8 @@ class AddNote : AppCompatActivity() { val timeoutDialog: AlertDialog = timeoutDialogBuilder.create() timeoutDialog.setCancelable(true) timeoutDialog.show() - } catch (_: WindowManager.BadTokenException) { } + } catch (_: WindowManager.BadTokenException) { + } }, (crypto.DEFAULT_AUTHENTICATION_DELAY - 2).toLong() * 1000) @@ -315,7 +376,7 @@ class AddNote : AppCompatActivity() { } else { - val intentData = crypto.receiveKeyringFromSecureIntent ( + val intentData = crypto.receiveKeyringFromSecureIntent( currentActivityClassNameAsString = getString(R.string.title_activity_add_note), intent = intent ) @@ -338,22 +399,29 @@ class AddNote : AppCompatActivity() { if (itemId != null) { note = io.decryptNote(io.getNote(itemId!!, vault)!!) - loadNote (note) + loadNote(note) frequencyAccessed = note.frequencyAccessed!! } - } + itemPersistence = ItemPersistence( + applicationContext = applicationContext, + appCompatActivity = this@AddNote, + keyring = keyring, + itemId = itemId + ) + } } @SuppressLint("UseCompatLoadingForDrawables", "ClickableViewAccessibility", "SetTextI18n") - private fun initializeUI (): Boolean { - theme = when (applicationContext.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK)) { - Configuration.UI_MODE_NIGHT_YES -> ThemeDesert() - Configuration.UI_MODE_NIGHT_NO -> ThemeDefault() - Configuration.UI_MODE_NIGHT_UNDEFINED -> ThemeDefault() - else -> ThemeDefault() - } + private fun initializeUI(): Boolean { + theme = + when (applicationContext.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK)) { + Configuration.UI_MODE_NIGHT_YES -> ThemeDesert() + Configuration.UI_MODE_NIGHT_NO -> ThemeDefault() + Configuration.UI_MODE_NIGHT_UNDEFINED -> ThemeDefault() + else -> ThemeDefault() + } noteEditor = findViewById(R.id.noteEditor) noteEditor.isActivated = true @@ -371,7 +439,13 @@ class AddNote : AppCompatActivity() { } .setLinkFontColor(noteEditor.currentTextColor) .setOnTodoClickCallback(object : OnTodoClickCallback { - override fun onTodoClicked(view: View?, line: String?, lineNumber: Int): CharSequence { return "" } + override fun onTodoClicked( + view: View?, + line: String?, + lineNumber: Int + ): CharSequence { + return "" + } }) .setRxMDImageLoader(DefaultLoader(applicationContext)) .build() @@ -389,7 +463,11 @@ class AddNote : AppCompatActivity() { } .setLinkFontColor(noteEditor.currentTextColor) .setOnTodoClickCallback(object : OnTodoClickCallback { - override fun onTodoClicked(view: View?, text: String?, lineNumber: Int): CharSequence { + override fun onTodoClicked( + view: View?, + text: String?, + lineNumber: Int + ): CharSequence { return text.toString() } }) @@ -412,7 +490,7 @@ class AddNote : AppCompatActivity() { findViewById(R.id.helpButton).setOnClickListener { val inflater = layoutInflater - val dialogView: View = inflater.inflate (R.layout.markdown_help, null) + val dialogView: View = inflater.inflate(R.layout.markdown_help, null) val dialogBuilder = MaterialAlertDialogBuilder(this) dialogBuilder .setView(dialogView) @@ -422,14 +500,16 @@ class AddNote : AppCompatActivity() { val markdownDialog = dialogBuilder.show() val markdownUnrendered = markdownDialog.findViewById(R.id.guide) as TextView - val markdownRendered = markdownDialog.findViewById(R.id.guideRendered) as com.yydcdut.markdown.MarkdownEditText + val markdownRendered = + markdownDialog.findViewById(R.id.guideRendered) as com.yydcdut.markdown.MarkdownEditText previewMarkdownProcessor.live(markdownRendered) markdownUnrendered.visibility = View.VISIBLE markdownRendered.visibility = View.GONE markdownUnrendered.startAnimation(loadAnimation(applicationContext, R.anim.from_top)) - val renderButton = markdownDialog.findViewById(R.id.renderButton) as MaterialButton + val renderButton = + markdownDialog.findViewById(R.id.renderButton) as MaterialButton var rendered = false renderButton.setOnClickListener { if (!rendered) { @@ -446,7 +526,7 @@ class AddNote : AppCompatActivity() { renderButton.icon = getDrawable(R.drawable.ic_baseline_visibility_24) } } - val backButton = markdownDialog.findViewById(R.id.backButton) as MaterialButton + val backButton = markdownDialog.findViewById(R.id.backButton) as MaterialButton backButton.setOnClickListener { markdownDialog.dismiss() } dialogBuilder.create() } @@ -455,7 +535,10 @@ class AddNote : AppCompatActivity() { val start = noteEditor.selectionStart.coerceAtLeast(0) val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) - if (selectedText.isNotEmpty()) noteEditor.setText(noteEditor.text.toString().replace(selectedText, utils.stringToNumberedString(selectedText))) + if (selectedText.isNotEmpty()) noteEditor.setText( + noteEditor.text.toString() + .replace(selectedText, utils.stringToNumberedString(selectedText)) + ) else noteEditor.append(utils.stringToNumberedString(selectedText)) noteEditor.setSelection(noteEditor.text.toString().length) } @@ -464,7 +547,10 @@ class AddNote : AppCompatActivity() { val start = noteEditor.selectionStart.coerceAtLeast(0) val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) - if (selectedText.isNotEmpty()) noteEditor.setText(noteEditor.text.toString().replace(selectedText, utils.stringToBulletedString(selectedText))) + if (selectedText.isNotEmpty()) noteEditor.setText( + noteEditor.text.toString() + .replace(selectedText, utils.stringToBulletedString(selectedText)) + ) else noteEditor.append(utils.stringToBulletedString(selectedText)) noteEditor.setSelection(noteEditor.text.toString().length) } @@ -474,12 +560,22 @@ class AddNote : AppCompatActivity() { val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) if (selectedText.trim().replace(" ", "").isNotEmpty()) { - noteEditor.setText(noteEditor.text.toString().replace(selectedText, "[${selectedText}]()")) - noteEditor.setSelection(noteEditor.text.toString().indexOf(selectedText) + selectedText.length + 2) + noteEditor.setText( + noteEditor.text.toString().replace(selectedText, "[${selectedText}]()") + ) + noteEditor.setSelection( + noteEditor.text.toString().indexOf(selectedText) + selectedText.length + 2 + ) } else { val markdown = "[text](url)" try { - noteEditor.text.replace(start.coerceAtMost(end), start.coerceAtLeast(end), markdown, 0, markdown.length) + noteEditor.text.replace( + start.coerceAtMost(end), + start.coerceAtLeast(end), + markdown, + 0, + markdown.length + ) } catch (_: Exception) { noteEditor.text.append(markdown) } @@ -491,12 +587,22 @@ class AddNote : AppCompatActivity() { val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) if (selectedText.trim().replace(" ", "").isNotEmpty()) { - noteEditor.setText(noteEditor.text.toString().replace(selectedText, "_${selectedText}_")) - noteEditor.setSelection(noteEditor.text.toString().indexOf(selectedText) + selectedText.length) + noteEditor.setText( + noteEditor.text.toString().replace(selectedText, "_${selectedText}_") + ) + noteEditor.setSelection( + noteEditor.text.toString().indexOf(selectedText) + selectedText.length + ) } else { val markdown = "_text_" try { - noteEditor.text.replace(start.coerceAtMost(end), start.coerceAtLeast(end), markdown, 0, markdown.length) + noteEditor.text.replace( + start.coerceAtMost(end), + start.coerceAtLeast(end), + markdown, + 0, + markdown.length + ) } catch (_: Exception) { noteEditor.text.append(markdown) } @@ -507,7 +613,10 @@ class AddNote : AppCompatActivity() { val start = noteEditor.selectionStart.coerceAtLeast(0) val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) - if (selectedText.isNotEmpty()) noteEditor.setText(noteEditor.text.toString().replace(selectedText, utils.stringToCheckedString(selectedText))) + if (selectedText.isNotEmpty()) noteEditor.setText( + noteEditor.text.toString() + .replace(selectedText, utils.stringToCheckedString(selectedText)) + ) else noteEditor.append(utils.stringToCheckedString(selectedText)) noteEditor.setSelection(noteEditor.text.toString().length) } @@ -516,7 +625,10 @@ class AddNote : AppCompatActivity() { val start = noteEditor.selectionStart.coerceAtLeast(0) val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) - if (selectedText.isNotEmpty()) noteEditor.setText(noteEditor.text.toString().replace(selectedText, utils.stringToUncheckedString(selectedText))) + if (selectedText.isNotEmpty()) noteEditor.setText( + noteEditor.text.toString() + .replace(selectedText, utils.stringToUncheckedString(selectedText)) + ) else noteEditor.append(utils.stringToUncheckedString(selectedText)) noteEditor.setSelection(noteEditor.text.toString().length) } @@ -526,12 +638,22 @@ class AddNote : AppCompatActivity() { val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) if (selectedText.trim().replace(" ", "").isNotEmpty()) { - noteEditor.setText(noteEditor.text.toString().replace(selectedText, "![${selectedText}]()")) - noteEditor.setSelection(noteEditor.text.toString().indexOf(selectedText) + selectedText.length + 2) + noteEditor.setText( + noteEditor.text.toString().replace(selectedText, "![${selectedText}]()") + ) + noteEditor.setSelection( + noteEditor.text.toString().indexOf(selectedText) + selectedText.length + 2 + ) } else { val markdown = "![caption](url)" try { - noteEditor.text.replace(start.coerceAtMost(end), start.coerceAtLeast(end), markdown, 0, markdown.length) + noteEditor.text.replace( + start.coerceAtMost(end), + start.coerceAtLeast(end), + markdown, + 0, + markdown.length + ) } catch (_: Exception) { noteEditor.text.append(markdown) } @@ -543,12 +665,22 @@ class AddNote : AppCompatActivity() { val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) if (selectedText.trim().replace(" ", "").isNotEmpty()) { - noteEditor.setText(noteEditor.text.toString().replace(selectedText, "$selectedText\n****")) - noteEditor.setSelection(noteEditor.text.toString().indexOf(selectedText) + selectedText.length) + noteEditor.setText( + noteEditor.text.toString().replace(selectedText, "$selectedText\n****") + ) + noteEditor.setSelection( + noteEditor.text.toString().indexOf(selectedText) + selectedText.length + ) } else { val markdown = "\n****" try { - noteEditor.text.replace(start.coerceAtMost(end), start.coerceAtLeast(end), markdown, 0, markdown.length) + noteEditor.text.replace( + start.coerceAtMost(end), + start.coerceAtLeast(end), + markdown, + 0, + markdown.length + ) } catch (_: Exception) { noteEditor.text.append(markdown) } @@ -560,12 +692,22 @@ class AddNote : AppCompatActivity() { val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) if (selectedText.trim().replace(" ", "").isNotEmpty()) { - noteEditor.setText(noteEditor.text.toString().replace(selectedText, "> $selectedText")) - noteEditor.setSelection(noteEditor.text.toString().indexOf(selectedText) + selectedText.length) + noteEditor.setText( + noteEditor.text.toString().replace(selectedText, "> $selectedText") + ) + noteEditor.setSelection( + noteEditor.text.toString().indexOf(selectedText) + selectedText.length + ) } else { val markdown = "\n> " try { - noteEditor.text.replace(start.coerceAtMost(end), start.coerceAtLeast(end), "> ", 0, "> ".length) + noteEditor.text.replace( + start.coerceAtMost(end), + start.coerceAtLeast(end), + "> ", + 0, + "> ".length + ) } catch (_: Exception) { noteEditor.text.append(markdown) } @@ -577,12 +719,22 @@ class AddNote : AppCompatActivity() { val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) if (selectedText.trim().replace(" ", "").isNotEmpty()) { - noteEditor.setText(noteEditor.text.toString().replace(selectedText, "~~$selectedText~~")) - noteEditor.setSelection(noteEditor.text.toString().indexOf(selectedText) + selectedText.length) + noteEditor.setText( + noteEditor.text.toString().replace(selectedText, "~~$selectedText~~") + ) + noteEditor.setSelection( + noteEditor.text.toString().indexOf(selectedText) + selectedText.length + ) } else { val markdown = "~~text~~" try { - noteEditor.text.replace(start.coerceAtMost(end), start.coerceAtLeast(end), markdown, 0, markdown.length) + noteEditor.text.replace( + start.coerceAtMost(end), + start.coerceAtLeast(end), + markdown, + 0, + markdown.length + ) } catch (_: Exception) { noteEditor.text.append(markdown) } @@ -594,12 +746,22 @@ class AddNote : AppCompatActivity() { val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) if (selectedText.trim().replace(" ", "").isNotEmpty()) { - noteEditor.setText(noteEditor.text.toString().replace(selectedText, "\n```\n$selectedText\n```")) - noteEditor.setSelection(noteEditor.text.toString().indexOf(selectedText) + selectedText.length) + noteEditor.setText( + noteEditor.text.toString().replace(selectedText, "\n```\n$selectedText\n```") + ) + noteEditor.setSelection( + noteEditor.text.toString().indexOf(selectedText) + selectedText.length + ) } else { val markdown = "```\ntext\n```" try { - noteEditor.text.replace(start.coerceAtMost(end), start.coerceAtLeast(end), "```\ntext\n```", 0, "```\ntext\n```".length) + noteEditor.text.replace( + start.coerceAtMost(end), + start.coerceAtLeast(end), + "```\ntext\n```", + 0, + "```\ntext\n```".length + ) } catch (_: Exception) { noteEditor.text.append(markdown) } @@ -611,12 +773,22 @@ class AddNote : AppCompatActivity() { val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) if (selectedText.trim().replace(" ", "").isNotEmpty()) { - noteEditor.setText(noteEditor.text.toString().replace(selectedText, "**$selectedText**")) - noteEditor.setSelection(noteEditor.text.toString().indexOf(selectedText) + selectedText.length) + noteEditor.setText( + noteEditor.text.toString().replace(selectedText, "**$selectedText**") + ) + noteEditor.setSelection( + noteEditor.text.toString().indexOf(selectedText) + selectedText.length + ) } else { val markdown = "**text**" try { - noteEditor.text.replace(start.coerceAtMost(end), start.coerceAtLeast(end), markdown, 0, markdown.length) + noteEditor.text.replace( + start.coerceAtMost(end), + start.coerceAtLeast(end), + markdown, + 0, + markdown.length + ) } catch (_: Exception) { noteEditor.text.append(markdown) } @@ -628,24 +800,36 @@ class AddNote : AppCompatActivity() { val end = noteEditor.selectionEnd.coerceAtLeast(0) val selectedText = noteEditor.text.toString().substring(start, end) if (selectedText.trim().replace(" ", "").isNotEmpty()) { - noteEditor.setText(noteEditor.text.toString().replace(selectedText, utils.stringToTitledStrings(selectedText))) + noteEditor.setText( + noteEditor.text.toString() + .replace(selectedText, utils.stringToTitledStrings(selectedText)) + ) } else { val markdown = "# Title" try { - noteEditor.text.replace(start.coerceAtMost(end), start.coerceAtLeast(end), markdown, 0, markdown.length) + noteEditor.text.replace( + start.coerceAtMost(end), + start.coerceAtLeast(end), + markdown, + 0, + markdown.length + ) } catch (_: Exception) { noteEditor.text.append(markdown) } } } - doneButton = findViewById (R.id.done) + doneButton = findViewById(R.id.done) doneButton.setOnClickListener { if (noteEditor.text.isNullOrBlank() || noteEditor.text.toString().length <= 1) { val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() alertDialog.setTitle("Blank Note") alertDialog.setMessage("Note can't be blank") - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Go back") { dialog, _ -> dialog.dismiss() } + alertDialog.setButton( + AlertDialog.BUTTON_NEGATIVE, + "Go back" + ) { dialog, _ -> dialog.dismiss() } alertDialog.show() } else saveNote() @@ -658,7 +842,7 @@ class AddNote : AppCompatActivity() { onBackPressed() } - deleteButton = findViewById (R.id.delete) + deleteButton = findViewById(R.id.delete) if (itemId != null) { deleteButton.setOnClickListener { val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() @@ -669,8 +853,8 @@ class AddNote : AppCompatActivity() { vault.note!!.remove(io.getNote(itemId!!, vault)) io.writeVault(vault) - network.writeQueueTask (itemId!!, mode = network.MODE_DELETE) - crypto.secureStartActivity ( + network.writeQueueTask(itemId!!, mode = network.MODE_DELETE) + crypto.secureStartActivity( nextActivity = Dashboard(), nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), keyring = keyring, @@ -678,7 +862,10 @@ class AddNote : AppCompatActivity() { ) } - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Go back") { dialog, _ -> dialog.dismiss() } + alertDialog.setButton( + AlertDialog.BUTTON_NEGATIVE, + "Go back" + ) { dialog, _ -> dialog.dismiss() } alertDialog.show() } @@ -686,8 +873,8 @@ class AddNote : AppCompatActivity() { deleteButton.visibility = View.GONE } - tagButton = findViewById (R.id.tag) - tagPicker = AddTag (tagId, applicationContext, this@AddNote, keyring) + tagButton = findViewById(R.id.tag) + tagPicker = AddTag(tagId, applicationContext, this@AddNote, keyring) tagButton.setOnClickListener { tagPicker.showPicker(tagId) @@ -700,14 +887,29 @@ class AddNote : AppCompatActivity() { } favoriteButton = findViewById(R.id.favoriteButton) - favoriteButton.setImageDrawable(ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_border_24)) + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + applicationContext, + R.drawable.ic_baseline_star_border_24 + ) + ) favoriteButton.setOnClickListener { favorite = if (!favorite) { - favoriteButton.setImageDrawable (ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_24)) + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + applicationContext, + R.drawable.ic_baseline_star_24 + ) + ) favoriteButton.startAnimation(loadAnimation(applicationContext, R.anim.heartbeat)) true } else { - favoriteButton.setImageDrawable (ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_border_24)) + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + applicationContext, + R.drawable.ic_baseline_star_border_24 + ) + ) false } } @@ -726,9 +928,11 @@ class AddNote : AppCompatActivity() { noteEditor.setBackgroundColor(Color.parseColor(noteColor)) if (noteColor != null) { val intColor: Int = noteColor!!.replace("#", "").toInt(16) - val r = intColor shr 16 and 0xFF; val g = intColor shr 8 and 0xFF; val b = intColor shr 0 and 0xFF + val r = intColor shr 16 and 0xFF; + val g = intColor shr 8 and 0xFF; + val b = intColor shr 0 and 0xFF if (g >= 200 || b >= 200) { - noteEditor.setTextColor (Color.BLACK) + noteEditor.setTextColor(Color.BLACK) noteEditor.setHintTextColor(Color.BLACK) } else { noteEditor.setTextColor(Color.WHITE) @@ -749,7 +953,7 @@ class AddNote : AppCompatActivity() { previewButton.setOnClickListener { notePreview.clear() - notePreview.setText (noteData) + notePreview.setText(noteData) preview = if (preview) { var intColor: Int @@ -758,25 +962,30 @@ class AddNote : AppCompatActivity() { notePreview.setBackgroundColor(Color.parseColor(noteColor)) intColor = noteColor!!.replace("#", "").toInt(16) } catch (_: Exception) { - intColor = when (applicationContext.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK)) { - Configuration.UI_MODE_NIGHT_YES -> { - notePreview.setBackgroundColor(Color.BLACK) - Color.BLACK - } - Configuration.UI_MODE_NIGHT_NO or Configuration.COLOR_MODE_HDR_UNDEFINED -> { - notePreview.setBackgroundColor(Color.WHITE) - Color.WHITE - } - else -> { - notePreview.setBackgroundColor(Color.WHITE) - Color.WHITE + intColor = + when (applicationContext.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK)) { + Configuration.UI_MODE_NIGHT_YES -> { + notePreview.setBackgroundColor(Color.BLACK) + Color.BLACK + } + + Configuration.UI_MODE_NIGHT_NO or Configuration.COLOR_MODE_HDR_UNDEFINED -> { + notePreview.setBackgroundColor(Color.WHITE) + Color.WHITE + } + + else -> { + notePreview.setBackgroundColor(Color.WHITE) + Color.WHITE + } } - } } - val r = intColor shr 16 and 0xFF; val g = intColor shr 8 and 0xFF; val b = intColor shr 0 and 0xFF + val r = intColor shr 16 and 0xFF; + val g = intColor shr 8 and 0xFF; + val b = intColor shr 0 and 0xFF if (g >= 200 || b >= 200) { - notePreview.setTextColor (Color.BLACK) + notePreview.setTextColor(Color.BLACK) } else { notePreview.setTextColor(Color.WHITE) } @@ -813,16 +1022,26 @@ class AddNote : AppCompatActivity() { return true } - private fun loadNote (note: IOUtilities.Note): Boolean { + private fun loadNote(note: IOUtilities.Note): Boolean { favorite = if (note.favorite) { - favoriteButton.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_baseline_star_24)); true + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + this, + R.drawable.ic_baseline_star_24 + ) + ); true } else { - favoriteButton.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_baseline_star_border_24)); false + favoriteButton.setImageDrawable( + ContextCompat.getDrawable( + this, + R.drawable.ic_baseline_star_border_24 + ) + ); false } tagId = note.tagId - tagPicker = AddTag (tagId, applicationContext, this@AddNote, keyring) + tagPicker = AddTag(tagId, applicationContext, this@AddNote, keyring) dateAndTime.visibility = View.VISIBLE @@ -830,19 +1049,22 @@ class AddNote : AppCompatActivity() { val time = Calendar.getInstance(Locale.getDefault()) time.timeInMillis = previousTimestamp?.times(1000L)!! - dateAndTime.text = "Last edited on " + DateFormat.format("MMM dd, yyyy â‹… hh:mm a", time).toString() + dateAndTime.text = + "Last edited on " + DateFormat.format("MMM dd, yyyy â‹… hh:mm a", time).toString() if (!note.notes.isNullOrEmpty()) { - noteEditor.setText (note.notes) + noteEditor.setText(note.notes) } if (!note.color.isNullOrEmpty()) { noteColor = note.color noteEditor.setBackgroundColor(Color.parseColor(noteColor)) val intColor: Int = noteColor!!.replace("#", "").toInt(16) - val r = intColor shr 16 and 0xFF; val g = intColor shr 8 and 0xFF; val b = intColor shr 0 and 0xFF + val r = intColor shr 16 and 0xFF; + val g = intColor shr 8 and 0xFF; + val b = intColor shr 0 and 0xFF if (g >= 200 || b >= 200) { - noteEditor.setTextColor (Color.BLACK) + noteEditor.setTextColor(Color.BLACK) noteEditor.setHintTextColor(Color.BLACK) } else { noteEditor.setTextColor(Color.WHITE) @@ -867,54 +1089,67 @@ class AddNote : AppCompatActivity() { return true } - private fun saveNote () { - var dateCreated = Instant.now().epochSecond - - if (itemId != null) { - dateCreated = note.dateCreated!! - vault.note?.remove(io.getNote(itemId!!, vault)) - } - - val data = IOUtilities.Note ( - id = itemId ?: UUID.randomUUID().toString(), - organizationId = null, - type = io.TYPE_NOTE, - notes = noteEditor.text.toString(), - color = noteColor, - favorite = favorite, + private fun saveNote() { + itemPersistence.saveNote( + note = noteEditor.text.toString(), + noteColor = noteColor, + isFavorite = favorite, tagId = tagPicker.getSelectedTagId() ?: tagId, - dateCreated = dateCreated, - dateModified = timestamp, + timestamp = timestamp, frequencyAccessed = frequencyAccessed ) - - val encryptedNote = io.encryptNote(data) - - vault.note?.add (encryptedNote) - io.writeVault(vault) - - if (itemId != null) network.writeQueueTask (encryptedNote, mode = network.MODE_PUT) - else network.writeQueueTask (encryptedNote, mode = network.MODE_POST) - - crypto.secureStartActivity ( - nextActivity = Dashboard(), - nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), - keyring = keyring, - itemId = null - ) - } - private fun getKeyringForShareSheet () { + //region Original saveNote() +// private fun saveNote () { +// var dateCreated = Instant.now().epochSecond +// +// if (itemId != null) { +// dateCreated = note.dateCreated!! +// vault.note?.remove(io.getNote(itemId!!, vault)) +// } +// +// val data = IOUtilities.Note ( +// id = itemId ?: UUID.randomUUID().toString(), +// organizationId = null, +// type = io.TYPE_NOTE, +// notes = noteEditor.text.toString(), +// color = noteColor, +// favorite = favorite, +// tagId = tagPicker.getSelectedTagId() ?: tagId, +// dateCreated = dateCreated, +// dateModified = timestamp, +// frequencyAccessed = frequencyAccessed +// ) +// +// val encryptedNote = io.encryptNote(data) +// +// vault.note?.add (encryptedNote) +// io.writeVault(vault) +// +// if (itemId != null) network.writeQueueTask (encryptedNote, mode = network.MODE_PUT) +// else network.writeQueueTask (encryptedNote, mode = network.MODE_POST) +// +// crypto.secureStartActivity ( +// nextActivity = Dashboard(), +// nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), +// keyring = keyring, +// itemId = null +// ) +// +// } + //endregion Original saveNote() + + private fun getKeyringForShareSheet() { } - override fun onBackPressed () { + override fun onBackPressed() { val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() alertDialog.setTitle("Confirm exit") alertDialog.setMessage("Would you like to go back to the Dashboard?") alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Exit") { dialog, _ -> - crypto.secureStartActivity ( + crypto.secureStartActivity( nextActivity = Dashboard(), nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), keyring = keyring, @@ -923,7 +1158,10 @@ class AddNote : AppCompatActivity() { super.onBackPressed() tagIdGrabber.removeCallbacksAndMessages(null) } - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Cancel") { dialog, _ -> dialog.dismiss() } + alertDialog.setButton( + AlertDialog.BUTTON_NEGATIVE, + "Cancel" + ) { dialog, _ -> dialog.dismiss() } alertDialog.show() } diff --git a/app/src/main/kotlin/cloud/keyspace/android/Dashboard.kt b/app/src/main/kotlin/cloud/keyspace/android/Dashboard.kt index 22e1e20..22a994f 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/Dashboard.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/Dashboard.kt @@ -1207,7 +1207,16 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi killBottomSheet() - if (vault.login.isNullOrEmpty()) { + logins.clear() + for (encryptedLogin in io.getLogins(vault)) { + val login = io.decryptLogin(encryptedLogin) + + if (login.id != null) { + logins.add(login) + } + } + + if (logins.isEmpty()) { try { fragmentRoot.removeView(fragmentView) } catch (uninflated: UninitializedPropertyAccessException) { } fragmentView = inflater.inflate(R.layout.no_vault_data, null) fragmentRoot.addView(fragmentView) @@ -1218,10 +1227,6 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi fragmentView = inflater.inflate(R.layout.dashboard_fragment_logins, null) fragmentRoot.addView(fragmentView) - logins.clear() - for (encryptedLogin in io.getLogins(vault)) - logins.add(io.decryptLogin(encryptedLogin)) - loginsRecycler = fragmentView.findViewById(R.id.logins_recycler) loginsRecycler.layoutManager = LinearLayoutManager(this@Dashboard) diff --git a/app/src/main/kotlin/cloud/keyspace/android/ImportAccountsBitwarden.kt b/app/src/main/kotlin/cloud/keyspace/android/ImportAccountsBitwarden.kt index 14e8bd4..d2c40c1 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/ImportAccountsBitwarden.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/ImportAccountsBitwarden.kt @@ -1,21 +1,28 @@ package cloud.keyspace.android import android.content.Intent -import android.net.Uri import android.os.Bundle import android.os.Handler +import android.os.Looper import android.util.Log +import android.util.Patterns import android.view.View import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import com.anggrayudi.storage.SimpleStorageHelper import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.google.android.material.button.MaterialButton import com.keyspace.keyspacemobile.NetworkUtilities -import java.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.OffsetDateTime +import java.util.Date class ImportAccountsBitwarden : AppCompatActivity() { @@ -41,7 +48,7 @@ class ImportAccountsBitwarden : AppCompatActivity() { crypto = CryptoUtilities(applicationContext, this) - val intentData = crypto.receiveKeyringFromSecureIntent ( + val intentData = crypto.receiveKeyringFromSecureIntent( currentActivityClassNameAsString = getString(R.string.title_activity_import_bitwarden), intent = intent ) @@ -68,43 +75,58 @@ class ImportAccountsBitwarden : AppCompatActivity() { storageHelper = SimpleStorageHelper(this, FILES_REQUEST_CODE, savedInstanceState) filePickerButton.setOnClickListener { - storageHelper.openFilePicker ( + storageHelper.openFilePicker( allowMultiple = false, - filterMimeTypes = arrayOf (JSON_MIME_TYPE) + filterMimeTypes = arrayOf(JSON_MIME_TYPE) ) } storageHelper.onFileSelected = { _, files -> - setImporting(files[0].name.toString()) - var vault: ExtractedItems? = null - val parseThread = Thread { - val file = contentResolver.openInputStream(files[0].uri) - val fileData = String(file?.readBytes()!!) - file.close() + lifecycleScope.launch(Dispatchers.Main) { + val vault: ExtractedItems? = withContext(Dispatchers.IO) { + val file = contentResolver.openInputStream(files[0].uri) + val fileData = String(file?.readBytes()!!) + file.close() + + try { + parseBitwardenJsonFile(fileData) + } catch (e: Exception) { + Log.e("parseThread", "Error parsing Bitwarden file.", e) + null + } + } - try { - vault = parseBitwardenJsonFile(fileData) - } catch (_: Exception) { } + if (vault != null) { + withContext(Dispatchers.Main) { + saveItems(vault) + } + } - } - parseThread.start() + delay(1000) - Handler().postDelayed({ - parseThread.join() // Save to KeyspaceFS try { - if (vault == null) setNoData() else { - Log.d("BWDATA", vault!!.logins[4].name.toString()) + if (vault == null) { + setNoData() + } else { + val logins = vault.logins.map { it.name ?: "" }.toTypedArray() + val cards = vault.cards.map { it.name ?: "" }.toTypedArray() + val notes = vault.notes.map { it.name ?: "" }.toTypedArray() + + val accounts = listOf( + *logins, + *cards, + *notes + ) + setDone(accounts) } } catch (_: Exception) { setNoData() } - }, 1000) - + } } - } private fun setImporting(accounts: String) { @@ -117,13 +139,14 @@ class ImportAccountsBitwarden : AppCompatActivity() { private fun setDone(accounts: List) { label.text = "Import successful" progressBar.visibility = View.GONE - description.text = "Imported ${accounts.size} account${if (accounts.size > 1) "s" else ""} successfully! Make sure all your accounts are in the list below" - description.append ("\n") + description.text = + "Imported ${accounts.size} account${if (accounts.size > 1) "s" else ""} successfully! Make sure all your accounts are in the list below" + description.append("\n") filePickerButton.visibility = View.VISIBLE filePickerButton.text = "Finish" filePickerButton.setOnClickListener { - crypto.secureStartActivity ( + crypto.secureStartActivity( nextActivity = ImportAccountsBitwarden(), nextActivityClassNameAsString = getString(R.string.title_activity_settings), keyring = keyring, @@ -144,24 +167,29 @@ class ImportAccountsBitwarden : AppCompatActivity() { } } - data class Folder ( + data class Folder( val id: String, val name: String ) - data class Uri ( + data class Uri( val match: String?, val uri: String ) - data class Login ( + data class Login( val uris: List, val username: String?, val password: String?, val totp: String? ) - data class Item1 ( + data class PasswordHistory( + val lastUsedDate: String, + val password: String + ) + + data class Item1( val id: String, val organizationId: String?, val folderId: String?, @@ -171,22 +199,26 @@ class ImportAccountsBitwarden : AppCompatActivity() { val notes: String?, val favorite: Boolean?, val fields: List?, - val login: IOUtilities.Login?, - val collectionIds: String? + val login: Login?, + val passwordHistory: List?, + val collectionIds: String?, + val revisionDate: String?, + val creationDate: String?, + val deletedDate: String?, ) - data class ItemField ( + data class ItemField( val name: String?, val value: String?, val type: Int, val linkedId: String? ) - data class SecureNote ( + data class SecureNote( val type: Int ) - data class Item2 ( + data class Item2( val id: String, val organizationId: String?, val folderId: String?, @@ -197,10 +229,12 @@ class ImportAccountsBitwarden : AppCompatActivity() { val favorite: Boolean?, val fields: List?, val secureNote: SecureNote, - val collectionIds: String? + val collectionIds: String?, + val creationDate: String?, + val revisionDate: String?, ) - data class Card ( + data class Card( val cardholderName: String?, val brand: String?, val number: String?, @@ -209,7 +243,7 @@ class ImportAccountsBitwarden : AppCompatActivity() { val code: String? ) - data class Item3 ( + data class Item3( val id: String, val organizationId: String?, val folderId: String?, @@ -223,7 +257,7 @@ class ImportAccountsBitwarden : AppCompatActivity() { val collectionIds: String? ) - data class Identity ( + data class Identity( val title: String?, val firstName: String?, val middleName: String?, @@ -244,7 +278,7 @@ class ImportAccountsBitwarden : AppCompatActivity() { val licenseNumber: String? ) - data class Item4 ( + data class Item4( val id: String, val organizationId: String?, val folderId: String?, @@ -258,13 +292,13 @@ class ImportAccountsBitwarden : AppCompatActivity() { val collectionIds: String? ) - data class BitwardenVault ( + data class BitwardenVault( val encrypted: Boolean, val folders: List, val items: List ) - data class ExtractedItems ( + data class ExtractedItems( val logins: MutableList, val notes: MutableList, val cards: MutableList, @@ -272,18 +306,18 @@ class ImportAccountsBitwarden : AppCompatActivity() { val folders: List, ) - private fun parseBitwardenJsonFile (fileData: String): ExtractedItems? { + private fun parseBitwardenJsonFile(fileData: String): ExtractedItems? { val mapper = jacksonObjectMapper() mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - val vault: BitwardenVault = mapper.readValue (fileData, BitwardenVault::class.java) + val vault: BitwardenVault = mapper.readValue(fileData, BitwardenVault::class.java) if (vault.encrypted) throw NullPointerException() var extractedItems: ExtractedItems? = null if (vault.items.isNotEmpty()) { - extractedItems = ExtractedItems ( + extractedItems = ExtractedItems( logins = mutableListOf(), notes = mutableListOf(), cards = mutableListOf(), @@ -291,19 +325,174 @@ class ImportAccountsBitwarden : AppCompatActivity() { folders = vault.folders ) for (item in vault.items) { + Log.d("parseBitwardenJsonFile", "item = $item") + when (item.toString().substringAfter(", type=").substringBefore(", ").toInt()) { - 1 -> extractedItems.logins.add(mapper.convertValue(item, Item1::class.java)) - 2 -> extractedItems.notes.add(mapper.convertValue(item, Item2::class.java)) - 3 -> extractedItems.cards.add(mapper.convertValue(item, Item3::class.java)) - 4 -> extractedItems.identities.add(mapper.convertValue(item, Item4::class.java)) + 1 -> extractedItems.logins.add( + mapper.convertValue(item, Item1::class.java).also { + Log.d("parseBitwardenJsonFile", "login = $it") + } + ) + + 2 -> extractedItems.notes.add( + mapper.convertValue(item, Item2::class.java).also { + Log.d("parseBitwardenJsonFile", "note = $it") + } + ) + + 3 -> extractedItems.cards.add( + mapper.convertValue(item, Item3::class.java).also { + Log.d("parseBitwardenJsonFile", "card = $it") + } + ) + + 4 -> extractedItems.identities.add( + mapper.convertValue(item, Item4::class.java).also { + Log.d("parseBitwardenJsonFile", "identity = $it") + } + ) } } } - return extractedItems + return extractedItems.also { + Log.d("parseBitwardenJsonFile", "extractedItems = $it") + } } + private fun saveItems(items: ExtractedItems) { + val itemPersistence = ItemPersistence( + applicationContext = applicationContext, + appCompatActivity = this, + keyring = keyring, + itemId = null + ) + + val cards = items.cards.map { it.copy() } + Log.d("saveItems", "cards = ${cards.size}") + + val logins = items.logins.map { it.copy() } + Log.d("saveItems", "logins = ${logins.size}") + + val notes = items.notes.map { it.copy() } + Log.d("saveItems", "notes = ${notes.size}") + + cards.forEach { card -> + Log.d("saveItems", "Saving card ${card.name} (${card.id})") + + val expMonth = card.card.expMonth?.takeLast(2)?.toIntOrNull() ?: 0 + val expYear = card.card.expYear?.takeLast(2)?.toIntOrNull() ?: 0 + val expDate = String.format("%02d/%02d", expMonth, expYear) + + itemPersistence.saveCard( + cardName = card.name ?: "", + cardNumber = card.card.number ?: "", + cardholderName = card.card.cardholderName ?: "", + toDate = expDate, + securityCode = card.card.code ?: "", + atmPin = "", + isAtmCard = false, + hasRfidChip = false, + iconFileName = null, + cardColor = null, + isFavorite = card.favorite ?: false, + tagId = card.folderId, + notes = card.notes ?: "", + customFieldsData = card.fields?.map { field -> + IOUtilities.CustomField( + name = field.name ?: "", + value = field.value ?: "", + hidden = false + ) + }?.toMutableList() ?: mutableListOf(), + frequencyAccessed = 0 + ) { error -> + Log.e("saveItems", "Error saving card: $error") + } + } + + logins.forEach { login -> + Log.d("saveItems", "Saving login ${login.name} (${login.id})") + + val username = login.login?.username ?: "" + + itemPersistence.saveLogin( + siteName = login.name ?: "", + siteUrlsData = login.login?.uris?.map { + it.uri + }?.toMutableList() ?: mutableListOf(), + userName = username, + email = if (Regex(Patterns.EMAIL_ADDRESS.pattern()).matches(username)) { + username + } else { + "" + }, + password = login.login?.password ?: "", + passwordHistoryData = login.passwordHistory?.map { + IOUtilities.Password( + password = it.password, + created = try { + OffsetDateTime.parse(it.lastUsedDate).toEpochSecond() + } catch (e: Exception) { + Log.e("saveItems", "Failed to parse date.", e) + Date().time + } + ) + }?.toMutableList() ?: mutableListOf(), + secret = login.login?.totp ?: "", + backupCodes = "", + iconFileName = null, + isFavorite = login.favorite ?: false, + tagId = login.folderId, + notes = login.notes ?: "", + customFieldsData = login.fields?.map { field -> + IOUtilities.CustomField( + name = field.name ?: "", + value = field.value ?: "", + hidden = false + ) + }?.toMutableList() ?: mutableListOf() + ) { error -> + Log.e("saveItems", "Error saving login: $error") + } + } + + notes.forEach { note -> + Log.d("saveItems", "Saving note ${note.name} (${note.id})") + + val consolidatedNote = (note.notes ?: "") + note.fields?.joinToString( + separator = "\n", + prefix = "\n" + ) { field -> + "${field.name}: ${field.value}" + } + + itemPersistence.saveNote( + note = consolidatedNote, + noteColor = null, + isFavorite = note.favorite ?: false, + tagId = note.folderId, + timestamp = note.revisionDate?.let { + try { + OffsetDateTime.parse(it).toEpochSecond() + } catch (e: Exception) { + Log.w("saveItems", "Failed to parse creation date.", e) + null + } + } ?: note.creationDate?.let { + try { + OffsetDateTime.parse(it).toEpochSecond() + } catch (e: Exception) { + Log.w("saveItems", "Failed to parse revision date.", e) + null + } + } ?: Date().time, + frequencyAccessed = 0 + ) + } + } + override fun onSaveInstanceState(outState: Bundle) { storageHelper.onSaveInstanceState(outState) super.onSaveInstanceState(outState) @@ -320,7 +509,11 @@ class ImportAccountsBitwarden : AppCompatActivity() { storageHelper.storage.onActivityResult(requestCode, resultCode, data) } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults) } diff --git a/app/src/main/kotlin/cloud/keyspace/android/ItemPersistence.kt b/app/src/main/kotlin/cloud/keyspace/android/ItemPersistence.kt new file mode 100644 index 0000000..21d75aa --- /dev/null +++ b/app/src/main/kotlin/cloud/keyspace/android/ItemPersistence.kt @@ -0,0 +1,379 @@ +package cloud.keyspace.android + +import android.content.Context +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import com.keyspace.keyspacemobile.NetworkUtilities +import java.time.Instant +import java.util.UUID + +data class Error( + val cardNumberError: String? = null, + val securityCodeError: String? = null, + val toDateError: String? = null, + val cardholderNameError: String? = null, + val nameError: String? = null, + val atmPinError: String? = null, + val siteNameError: String? = null, + val emailError: String? = null, + val secretError: String? = null, +) + +class ItemPersistence( + applicationContext: Context, + appCompatActivity: AppCompatActivity, + private val keyring: CryptoUtilities.Keyring, + private var itemId: String? +) { + private val crypto: CryptoUtilities = CryptoUtilities(applicationContext, appCompatActivity) + private val misc: MiscUtilities = MiscUtilities(applicationContext) + private val io: IOUtilities = IOUtilities(applicationContext, appCompatActivity, keyring) + private val vault: IOUtilities.Vault = io.getVault() + private val network: NetworkUtilities = + NetworkUtilities(applicationContext, appCompatActivity, keyring) + + private var _card: IOUtilities.Card? = null + private val card: IOUtilities.Card + get() = _card!! + + private var _login: IOUtilities.Login? = null + private val login: IOUtilities.Login + get() = _login!! + + private var _note: IOUtilities.Note? = null + private val note: IOUtilities.Note + get() = _note!! + + private val nextActivityClassName: String = + applicationContext.getString(R.string.title_activity_dashboard) + + init { + _card = try { + io.decryptCard(io.getCard(itemId!!, vault)!!) + } catch (e: Exception) { + Log.w("ItemPersistence", "Unable to decrypt card.", e) + null + } + Log.d("ItemPersistence", "card = $_card") + + _login = try { + io.decryptLogin(io.getLogin(itemId!!, vault)!!) + } catch (e: Exception) { + Log.w("ItemPersistence", "Unable to decrypt login.", e) + null + } + Log.d("ItemPersistence", "login = $_login") + + _note = try { + io.decryptNote(io.getNote(itemId!!, vault)!!) + } catch (e: Exception) { + Log.w("ItemPersistence", "Unable to decrypt note.", e) + null + } + Log.d("ItemPersistence", "note = $_note") + } + + fun saveLogin( + siteName: String, + siteUrlsData: MutableList, + userName: String, + email: String, + password: String, + passwordHistoryData: MutableList, + secret: String, + backupCodes: String, + iconFileName: String?, + isFavorite: Boolean, + tagId: String?, + notes: String, + customFieldsData: MutableList, + onInputError: (error: Error) -> Unit + ) { + Log.d("saveLogin", "itemId = $itemId") + + if (siteName.isBlank()) { + onInputError( + Error( + siteNameError = "Please enter a site name" + ).also { + Log.e("saveLogin", "error = $it") + } + ) + + return + } + + if (email.isNotBlank()) { + if (!misc.isValidEmail(email)) { + onInputError( + Error( + emailError = "Please enter a valid email" + ).also { + Log.e("saveLogin", "error = $it") + } + ) + + return + } + } + + if (secret.isNotBlank() && secret.length < 6) { + onInputError( + Error( + secretError = "Please enter a valid TOTP secret" + ).also { + Log.e("saveLogin", "error = $it") + } + ) + + return + } + + var dateCreated = Instant.now().epochSecond + + if (itemId != null) { + dateCreated = io.getLogin(itemId!!, vault)?.dateCreated!! + vault.login?.remove(io.getLogin(itemId!!, vault)) + + if (login.loginData != null) { + if (!login.loginData!!.password.isNullOrEmpty()) { + if (password != login.loginData?.password) { + passwordHistoryData.add( + IOUtilities.Password( + password = password, + created = Instant.now().epochSecond + ) + ) + } + } + } + } else { + passwordHistoryData.clear() + passwordHistoryData.add( + IOUtilities.Password( + password = password, + created = Instant.now().epochSecond + ) + ) + } + + val data = IOUtilities.Login( + id = itemId ?: UUID.randomUUID().toString(), + organizationId = null, + type = io.TYPE_LOGIN, + name = siteName, + notes = notes, + favorite = isFavorite, + tagId = tagId, + loginData = IOUtilities.LoginData( + username = userName, + password = password, + passwordHistory = if (passwordHistoryData.size > 0) { + passwordHistoryData + } else { + null + }, + email = email, + totp = IOUtilities.Totp( + secret = secret, + backupCodes = backupCodes.replace("\n", "").split(",").toMutableList() + ), + siteUrls = if (siteUrlsData.size > 0) { + siteUrlsData + } else { + null + } + ), + dateCreated = dateCreated, + dateModified = Instant.now().epochSecond, + frequencyAccessed = 0, + customFields = customFieldsData, + iconFile = iconFileName + ) + Log.d("saveLogin", "data = $data") + + val encryptedLogin = io.encryptLogin(data) + Log.d("saveLogin", "encryptedLogin = $encryptedLogin") + + vault.login?.add(encryptedLogin) + io.writeVault(vault) + + if (itemId != null) { + network.writeQueueTask(encryptedLogin, mode = network.MODE_PUT) + Log.d("saveLogin", "Updating existing login...") + } else { + network.writeQueueTask(encryptedLogin, mode = network.MODE_POST) + Log.d("saveLogin", "Saving new login...") + } + + crypto.secureStartActivity( + nextActivity = Dashboard(), + nextActivityClassNameAsString = nextActivityClassName, + keyring = keyring, + itemId = null + ) + } + + fun saveCard( + cardName: String, + cardNumber: String, + cardholderName: String, + toDate: String, + securityCode: String, + atmPin: String, + isAtmCard: Boolean, + hasRfidChip: Boolean, + iconFileName: String?, + cardColor: String?, + isFavorite: Boolean, + tagId: String?, + notes: String, + customFieldsData: MutableList, + frequencyAccessed: Long, + onInputError: (error: Error) -> Unit + ) { + var dateCreated = Instant.now().epochSecond + + if (itemId != null) { + dateCreated = card.dateCreated!! + vault.card?.remove(io.getCard(itemId!!, vault)) + } + + if (cardNumber.replace(" ", "").length < 16) { + onInputError( + Error( + cardNumberError = "Enter a valid 16 digit card number" + ) + ) + } else if (cardNumber.replace(" ", "").length in 17..18 + || cardNumber.replace(" ", "").length > 19 + ) { + onInputError( + Error( + cardNumberError = "Enter a valid 19 digit card number" + ) + ) + } else if (securityCode.length !in 3..4) { + onInputError( + Error( + securityCodeError = "Enter a valid security code" + ) + ) + } else if (toDate.isEmpty()) { + onInputError( + Error( + toDateError = "Enter an expiry date" + ) + ) + } else if (cardholderName.isEmpty()) { + onInputError( + Error( + cardholderNameError = "Enter card holder's name" + ) + ) + } else if (cardName.isEmpty()) { + onInputError( + Error( + nameError = "Enter a name. This can be your bank's name." + ) + ) + } else if (isAtmCard && atmPin.length < 4) { + onInputError( + Error( + atmPinError = "Enter a valid Personal Identification Number" + ) + ) + } else { + val data = IOUtilities.Card( + id = itemId ?: UUID.randomUUID().toString(), + organizationId = null, + type = io.TYPE_CARD, + name = cardName, + color = cardColor, + favorite = isFavorite, + tagId = tagId, + dateCreated = dateCreated, + dateModified = Instant.now().epochSecond, + frequencyAccessed = frequencyAccessed + 1, + cardNumber = cardNumber.filter { !it.isWhitespace() }, + cardholderName = cardholderName, + expiry = toDate, + notes = notes, + pin = if (atmPin.length == 4 && isAtmCard) { + atmPin + } else { + "" + }, + securityCode = securityCode, + customFields = customFieldsData, + rfid = hasRfidChip, + iconFile = iconFileName + ) + + val encryptedCard = io.encryptCard(data) + + vault.card?.add(encryptedCard) + io.writeVault(vault) + + if (itemId != null) { + network.writeQueueTask(encryptedCard, mode = network.MODE_PUT) + } else { + network.writeQueueTask(encryptedCard, mode = network.MODE_POST) + } + + crypto.secureStartActivity( + nextActivity = Dashboard(), + nextActivityClassNameAsString = nextActivityClassName, + keyring = keyring, + itemId = null + ) + } + } + + fun saveNote( + note: String, + noteColor: String?, + isFavorite: Boolean, + tagId: String?, + timestamp: Long, + frequencyAccessed: Long, + ) { + var dateCreated = Instant.now().epochSecond + + if (itemId != null) { + dateCreated = this.note.dateCreated!! + vault.note?.remove(io.getNote(itemId!!, vault)) + } + + val data = IOUtilities.Note( + id = itemId ?: UUID.randomUUID().toString(), + organizationId = null, + type = io.TYPE_NOTE, + notes = note, + color = noteColor, + favorite = isFavorite, + tagId = tagId, + dateCreated = dateCreated, + dateModified = timestamp, + frequencyAccessed = frequencyAccessed + ) + + val encryptedNote = io.encryptNote(data) + + vault.note?.add(encryptedNote) + io.writeVault(vault) + + if (itemId != null) { + network.writeQueueTask(encryptedNote, mode = network.MODE_PUT) + } else { + network.writeQueueTask(encryptedNote, mode = network.MODE_POST) + } + + crypto.secureStartActivity( + nextActivity = Dashboard(), + nextActivityClassNameAsString = nextActivityClassName, + keyring = keyring, + itemId = null + ) + } +} diff --git a/app/src/main/kotlin/cloud/keyspace/android/MiscUtilities.kt b/app/src/main/kotlin/cloud/keyspace/android/MiscUtilities.kt index 931e636..cf60066 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/MiscUtilities.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/MiscUtilities.kt @@ -1,6 +1,5 @@ package cloud.keyspace.android -import android.R.drawable import android.R.raw import android.annotation.SuppressLint import android.content.Context @@ -25,13 +24,14 @@ import java.lang.reflect.Field import java.net.URLDecoder import java.security.SecureRandom import java.util.* +import kotlin.math.abs /** * Miscellaneous utilities such as datatype converters, 2FA code generators, QR code scanners etc live here. * // @param context The context of the activity that a `MiscUtilities` object is initialized in, example: `applicationContext`, `this` etc. */ -class MiscUtilities (applicationContext: Context) { +class MiscUtilities(applicationContext: Context) { val context = applicationContext fun getPaymentGateway(cardNumber: String): String? { try { @@ -78,7 +78,7 @@ class MiscUtilities (applicationContext: Context) { } } - fun checkIfCardExpired (expiryDate:String): String? { + fun checkIfCardExpired(expiryDate: String): String? { var status: String? = null val year = expiryDate.substringAfter("/") val month = expiryDate.substringBefore("/") @@ -86,8 +86,9 @@ class MiscUtilities (applicationContext: Context) { val yearInt = year.toInt() val monthInt = month.toInt() - val currentYear = Calendar.getInstance().get(Calendar.YEAR).toString().takeLast(2).toInt() - val currentMonth = Calendar.getInstance().get(Calendar.MONTH).toString().toInt()+1 + val currentYear = + Calendar.getInstance().get(Calendar.YEAR).toString().takeLast(2).toInt() + val currentMonth = Calendar.getInstance().get(Calendar.MONTH).toString().toInt() + 1 if ( (currentYear > yearInt) @@ -105,7 +106,13 @@ class MiscUtilities (applicationContext: Context) { } // password generator - fun passwordGenerator (length: Int, uppercase: Boolean, lowercase: Boolean, numbers: Boolean, symbols: Boolean): String { + fun passwordGenerator( + length: Int, + uppercase: Boolean, + lowercase: Boolean, + numbers: Boolean, + symbols: Boolean + ): String { var inputAlphabet = "" if (uppercase) inputAlphabet += "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if (lowercase) inputAlphabet += "abcdefghijklmnopqrstuvwxyz" @@ -129,7 +136,7 @@ class MiscUtilities (applicationContext: Context) { return words.joinToString("-") } - fun passwordStrength (password: String): Int { + fun passwordStrength(password: String): Int { var score = 0 if (password.length in 0..16) score = 3 else if (password.length in 17..32) score = 6 @@ -137,7 +144,7 @@ class MiscUtilities (applicationContext: Context) { return score } - fun passwordStrengthRating (score: Int): String { + fun passwordStrengthRating(score: Int): String { var strength = "Strong" when (score) { 3 -> strength = "Weak" @@ -147,7 +154,7 @@ class MiscUtilities (applicationContext: Context) { return strength } - fun color (color: String?): Int { + fun color(color: String?): Int { var colorData: Int = Color.parseColor("#000000") if (color == null) return Color.parseColor("#000000") when (color.lowercase()) { @@ -165,12 +172,19 @@ class MiscUtilities (applicationContext: Context) { fun generateProfilePicture(email: String): Drawable { - fun generateColorHash (email: String): Int { + fun generateColorHash(email: String): Int { var hashCode = email.hashCode() - val colorArray: ArrayList = context.resources.getStringArray(R.array.vault_item_colors).toList() as ArrayList + val colorArray: ArrayList = + context.resources.getStringArray(R.array.vault_item_colors) + .toList() as ArrayList if (hashCode < 0) hashCode = -hashCode - hashCode = (hashCode.toString().toCharArray()[2].code * hashCode.toString().toCharArray()[4].code).toString().takeLast(2).toInt() - 5 - return try { Color.parseColor(colorArray[hashCode]) } catch (unsupportedValues: ArrayIndexOutOfBoundsException) { context.getColor(R.color.lightFinesseColor) } + hashCode = (hashCode.toString().toCharArray()[2].code * hashCode.toString() + .toCharArray()[4].code).toString().takeLast(2).toInt() - 5 + return try { + Color.parseColor(colorArray[hashCode]) + } catch (unsupportedValues: ArrayIndexOutOfBoundsException) { + context.getColor(R.color.lightFinesseColor) + } } val text = email.first().toString().uppercase() @@ -190,21 +204,25 @@ class MiscUtilities (applicationContext: Context) { val canvas = Canvas(image) canvas.drawColor(generateColorHash(email)) - canvas.drawText (text, width/2 - trueWidth/2, baseline, paint) + canvas.drawText(text, width / 2 - trueWidth / 2, baseline, paint) return BitmapDrawable(context.resources, image) } - fun isValidPackageName (packageName: String): Boolean { + fun isValidPackageName(packageName: String): Boolean { val isPackage: Boolean - val isRegexMatched = Regex("^(?:[a-zA-Z]+(?:\\d*[a-zA-Z_]*)*)(?:\\.[a-zA-Z]+(?:\\d*[a-zA-Z_]*)*)+\$").containsMatchIn(packageName) + val isRegexMatched = + Regex("^(?:[a-zA-Z]+(?:\\d*[a-zA-Z_]*)*)(?:\\.[a-zA-Z]+(?:\\d*[a-zA-Z_]*)*)+\$").containsMatchIn( + packageName + ) isPackage = isRegexMatched return isPackage } - fun grabURLFromString (text: String): String? { + fun grabURLFromString(text: String): String? { var url: String? = null - val urlRegex = Regex("""^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$""") + val urlRegex = + Regex("""^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$""") val ipAddressRegex = Regex("""((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d{1,5})*(\/*.*)*)""") if (urlRegex.containsMatchIn(text)) { @@ -231,7 +249,7 @@ class MiscUtilities (applicationContext: Context) { val label: String?, ) - fun decodeOTPAuthURL (OTPAuthURL: String): MfaCode? { + fun decodeOTPAuthURL(OTPAuthURL: String): MfaCode? { val url = URLDecoder.decode(OTPAuthURL, "UTF-8") if (url.contains("otpauth") && url.contains("://")) { val type: String = @@ -241,17 +259,31 @@ class MiscUtilities (applicationContext: Context) { if (url.substringAfter("://").substringBefore("/").contains("totp")) "totp" else "hotp" - val issuer: String? = if (url.contains("issuer")) url.substringAfter("issuer=").substringBefore("&") else null - val account: String? = if (url.contains("otp")) url.substringAfter("otp/").substringBefore("?").replace(":", "") else null - val secret: String? = if (url.contains("secret")) url.substringAfter("secret=").substringBefore("&") else null - val algorithm: String? = if (url.contains("algorithm")) url.substringAfter("algorithm=").substringBefore("&") else null - val digits: Int? = if (url.contains("digits")) url.substringAfter("digits=").substringBefore("&").toInt() else null - val period: Int? = if (url.contains("period")) url.substringAfter("period=").substringBefore("&").toInt() else null - val lock: Boolean? = if (url.contains("lock")) url.substringAfter("lock=").substringBefore("&").toBoolean() else null - val counter: Int? = if (url.contains("counter")) url.substringAfter("counter=").substringBefore("&").toInt() else null - val label: String? = if (url.contains("label")) url.substringAfter("label=").substringBefore("&") else null - - return MfaCode ( + val issuer: String? = if (url.contains("issuer")) url.substringAfter("issuer=") + .substringBefore("&") else null + val account: String? = + if (url.contains("otp")) url.substringAfter("otp/").substringBefore("?") + .replace(":", "") else null + val secret: String? = if (url.contains("secret")) url.substringAfter("secret=") + .substringBefore("&") else null + val algorithm: String? = if (url.contains("algorithm")) url.substringAfter("algorithm=") + .substringBefore("&") else null + val digits: Int? = + if (url.contains("digits")) url.substringAfter("digits=").substringBefore("&") + .toInt() else null + val period: Int? = + if (url.contains("period")) url.substringAfter("period=").substringBefore("&") + .toInt() else null + val lock: Boolean? = + if (url.contains("lock")) url.substringAfter("lock=").substringBefore("&") + .toBoolean() else null + val counter: Int? = + if (url.contains("counter")) url.substringAfter("counter=").substringBefore("&") + .toInt() else null + val label: String? = if (url.contains("label")) url.substringAfter("label=") + .substringBefore("&") else null + + return MfaCode( type = type, mode = mode, issuer = issuer, @@ -269,18 +301,23 @@ class MiscUtilities (applicationContext: Context) { } } - fun encodeOTPAuthURL (mfaCodeObject: MfaCode): String? { + fun encodeOTPAuthURL(mfaCodeObject: MfaCode): String? { lateinit var type: String lateinit var issuer: String lateinit var account: String lateinit var secret: String if (mfaCodeObject.type.toString().isNotEmpty()) - type = if (mfaCodeObject.type.toString().lowercase().contains("backup") || mfaCodeObject.type.toString().lowercase().contains("migration")) "otpauth-migration" else "otpauth" + type = if (mfaCodeObject.type.toString().lowercase() + .contains("backup") || mfaCodeObject.type.toString().lowercase() + .contains("migration") + ) "otpauth-migration" else "otpauth" else return null val mode: String = if (mfaCodeObject.mode.toString().isNotEmpty()) - if (mfaCodeObject.mode.toString().lowercase().contains("time") || mfaCodeObject.mode.toString().contains("totp")) "totp" else "hotp" + if (mfaCodeObject.mode.toString().lowercase() + .contains("time") || mfaCodeObject.mode.toString().contains("totp") + ) "totp" else "hotp" else "totp" if (mfaCodeObject.account.toString().isNotEmpty()) @@ -329,7 +366,7 @@ class MiscUtilities (applicationContext: Context) { return output.toString() } - fun areSimilar (string1: String, string2: String): Boolean { + fun areSimilar(string1: String, string2: String): Boolean { val strippedString1 = string1 .lowercase(Locale.getDefault()) // ignore case .replace(" ", "") // remove spaces @@ -360,16 +397,18 @@ class MiscUtilities (applicationContext: Context) { } - fun scanQrCode (viewfinderMessage: String): String { + fun scanQrCode(viewfinderMessage: String): String { return viewfinderMessage } - fun generateQrCode (text: String): Bitmap? { + fun generateQrCode(text: String): Bitmap? { val colorPalette = com.github.sumimakito.awesomeqr.option.color.Color() colorPalette.light = 0xFFFFFFFF.toInt() // for blank spaces colorPalette.dark = 0xFF000000.toInt() // for non-blank spaces - colorPalette.background = 0xFFFFFFFF.toInt() // for the background (will be overriden by background images, if set) - colorPalette.auto = false // set to true to automatically pick out colors from the background image (will only work if background image is present) + colorPalette.background = + 0xFFFFFFFF.toInt() // for the background (will be overriden by background images, if set) + colorPalette.auto = + false // set to true to automatically pick out colors from the background image (will only work if background image is present) val renderOption = RenderOption() renderOption.content = text // content to encode @@ -409,21 +448,29 @@ class MiscUtilities (applicationContext: Context) { @SuppressLint("DiscouragedApi") fun getSiteIcon(siteName: String, color: Int?): Drawable? { - var trimmedSiteName = siteName .lowercase() .replace(" ", "") val svg: Sharp? = try { - Sharp.loadResource(context.resources, context.resources.getIdentifier(trimmedSiteName, "raw", context.packageName)) + Sharp.loadResource( + context.resources, + context.resources.getIdentifier(trimmedSiteName, "raw", context.packageName) + ) } catch (_: Exception) { trimmedSiteName = trimmedSiteName.replace(("[^\\w\\d ]").toRegex(), "") try { - Sharp.loadResource(context.resources, context.resources.getIdentifier(trimmedSiteName, "raw", context.packageName)) + Sharp.loadResource( + context.resources, + context.resources.getIdentifier(trimmedSiteName, "raw", context.packageName) + ) } catch (_: Exception) { trimmedSiteName = "_${trimmedSiteName}" try { - Sharp.loadResource(context.resources, context.resources.getIdentifier(trimmedSiteName, "raw", context.packageName)) + Sharp.loadResource( + context.resources, + context.resources.getIdentifier(trimmedSiteName, "raw", context.packageName) + ) } catch (_: Exception) { null } @@ -431,22 +478,34 @@ class MiscUtilities (applicationContext: Context) { } svg?.setOnElementListener(object : OnSvgElementListener { - override fun onSvgStart(canvas: Canvas, bounds: RectF?) { } - override fun onSvgEnd(canvas: Canvas, bounds: RectF?) { } - override fun onSvgElementDrawn(id: String?, element: T, canvas: Canvas, paint: Paint?) { } - - override fun onSvgElement(p0: String?, p1: T, elementBounds: RectF?, canvas: Canvas, canvasBounds: RectF?, paint: Paint?): T { + override fun onSvgStart(canvas: Canvas, bounds: RectF?) {} + override fun onSvgEnd(canvas: Canvas, bounds: RectF?) {} + override fun onSvgElementDrawn( + id: String?, + element: T, + canvas: Canvas, + paint: Paint? + ) { + } + override fun onSvgElement( + p0: String?, + p1: T, + elementBounds: RectF?, + canvas: Canvas, + canvasBounds: RectF?, + paint: Paint? + ): T { if (color != null) { - paint?.color = Color.argb ( + paint?.color = Color.argb( 255, Color.red(color), Color.blue(color), Color.green(color) ) } else { - val palette = context.resources.getColor(android.R.attr.textColorPrimary) - paint?.color = Color.argb ( + val palette = context.attrToColor(android.R.attr.textColorPrimary) + paint?.color = Color.argb( 255, Color.red(palette), Color.blue(palette), @@ -456,16 +515,17 @@ class MiscUtilities (applicationContext: Context) { return p1 } - }) - return try { svg?.drawable?.current } catch (_: Exception) { null } - + return try { + svg?.drawable?.current + } catch (_: Exception) { + null + } } - fun getSiteIconFilenames (): ArrayList { + fun getSiteIconFilenames(): ArrayList { val filenames = arrayListOf() - val rawResources = raw() val rawClass = R.raw::class.java val fields: Array = rawClass.declaredFields @@ -477,15 +537,19 @@ class MiscUtilities (applicationContext: Context) { return filenames } - fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } // to print, not related to crypto - - fun screenLockEnabled() : Boolean { + fun ByteArray.toHex(): String = + joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } // to print, not related to crypto + fun screenLockEnabled(): Boolean { var isKeyguardSet = false // don't allow successful authentication by default try { val biometricManager = BiometricManager.from(context) - val canAuthenticate = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.DEVICE_CREDENTIAL) + val canAuthenticate = biometricManager + .canAuthenticate( + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) isKeyguardSet = if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) { Log.d("Keyspace", "Device lock found") @@ -493,16 +557,11 @@ class MiscUtilities (applicationContext: Context) { } else { Log.e("Keyspace", "Device lock not set") throw NoSuchMethodError() - false } - } catch (noLockSet: NoSuchMethodError) { - isKeyguardSet = false Log.e("Keyspace", "Please set a screen lock.") noLockSet.stackTrace - } catch (incorrectCredentials: Exception) { - isKeyguardSet = false Log.e("Keyspace", "Your identity could not be verified.") incorrectCredentials.stackTrace } @@ -510,12 +569,16 @@ class MiscUtilities (applicationContext: Context) { return isKeyguardSet } - fun biometricsExist() : Boolean { + fun biometricsExist(): Boolean { var isKeyguardSet = false // don't allow successful authentication by default try { val biometricManager = BiometricManager.from(context) - val canAuthenticate = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.BIOMETRIC_STRONG) + val canAuthenticate = biometricManager + .canAuthenticate( + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.BIOMETRIC_STRONG + ) isKeyguardSet = if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) { Log.d("Keyspace", "Device lock found") @@ -523,16 +586,12 @@ class MiscUtilities (applicationContext: Context) { } else { Log.e("Keyspace", "Device lock not set") throw NoSuchMethodError() - false } - } catch (noLockSet: NoSuchMethodError) { - isKeyguardSet = false Log.e("Keyspace", "Please set a screen lock.") noLockSet.stackTrace } catch (incorrectCredentials: Exception) { - isKeyguardSet = false Log.e("Keyspace", "Your identity could not be verified.") incorrectCredentials.stackTrace } @@ -541,50 +600,77 @@ class MiscUtilities (applicationContext: Context) { } fun backup2faCodesToList(pastedCodesAsString: String): List { - var pastedCodes: String = pastedCodesAsString - pastedCodes = Regex("(([0-9a-zA-Z ]{4,} +[0-9a-zA-Z]{4,}+)|([0-9a-zA-Z-]{4,}))").findAll(pastedCodes).map { it.groupValues[1] }.joinToString() + val pastedCodes = + Regex( + "(([0-9a-zA-Z ]{4,} +[0-9a-zA-Z]{4,}+)|([0-9a-zA-Z-]{4,}))" + ).findAll(pastedCodesAsString).map { it.groupValues[1] }.joinToString() + val trim1 = pastedCodes.trim().split(",").toList() val trimList1: MutableList = mutableListOf() + for (item in trim1) { trimList1.add(item.trim()) } + val trimList2: MutableList = mutableListOf() + for (item in trimList1) { - if (item.any {it in "0123456789/?!:;%"}) { + if (item.any { it in "0123456789/?!:;%" }) { trimList2.add(item) } } + return trimList2 } - fun containsNonAlphabet (string: String): Boolean { + fun containsNonAlphabet(string: String): Boolean { return ( string.contains(" ") || - string.contains(Regex("""[!@#$%?,^&*)(+=._\-<>{}\[\]|]+$""")) || - string.contains(Regex("""[0-9].*""")) - ) + string.contains(Regex("""[!@#$%?,^&*)(+=._\-<>{}\[\]|]+$""")) || + string.contains(Regex("""[0-9].*""")) + ) } - fun checkIfListContainsSubstring (list: List, searchTerm: String): Boolean { + fun checkIfListContainsSubstring(list: List, searchTerm: String): Boolean { val newList = mutableListOf() - if (searchTerm.isEmpty()) return false - val searchTerm = searchTerm.replace(Regex("[^A-Za-z0-9 ]"), "").replace("", "").replace(" ", "").lowercase() - for (item in list) newList.add(item.replace(Regex("[^A-Za-z0-9 ]"), "").replace(" ", "").lowercase()) - for (item in newList) if (item.equals (searchTerm, ignoreCase = true)) return true + + if (searchTerm.isEmpty()) { + return false + } + + val term = searchTerm + .replace(Regex("[^A-Za-z0-9 ]"), "") + .replace("", "") + .replace(" ", "") + .lowercase() + + for (item in list) newList.add( + item.replace(Regex("[^A-Za-z0-9 ]"), "").replace(" ", "").lowercase() + ) + + for (item in newList) { + if (item.equals(term, ignoreCase = true)) { + return true + } + } + return false } - fun stringToNumberedString (string: String): String { - var string = string - if (string.replace(" ", "").isNotEmpty()) { + fun stringToNumberedString(string: String): String { + var result = string + + if (result.replace(" ", "").isNotEmpty()) { var lineBreakCounter = 1 - if (string.contains("\n")) { + + if (result.contains("\n")) { val stringCharacters = mutableListOf() stringCharacters.add(lineBreakCounter.toString().single()) stringCharacters.add('.') stringCharacters.add(' ') lineBreakCounter += 1 - for (c in string) { + + for (c in result) { stringCharacters.add(c) if (c == '\n') { stringCharacters.add(lineBreakCounter.toString().single()) @@ -593,44 +679,52 @@ class MiscUtilities (applicationContext: Context) { lineBreakCounter += 1 } } - string = String(stringCharacters.toCharArray()) + + result = String(stringCharacters.toCharArray()) } else { - string = "\n1. $string" + result = "\n1. $result" } } else { - string = "\n1. one\n2. two\n3. three\n" + result = "\n1. one\n2. two\n3. three\n" } - return string + + return result } - fun stringToBulletedString (string: String): String { - var string = string - if (string.replace(" ", "").isNotEmpty()) { - string = if (string.contains("\n")) { + fun stringToBulletedString(string: String): String { + var result = string + + if (result.replace(" ", "").isNotEmpty()) { + result = if (result.contains("\n")) { val stringCharacters = mutableListOf() stringCharacters.add('-') stringCharacters.add(' ') - for (c in string) { + + for (c in result) { stringCharacters.add(c) + if (c == '\n') { stringCharacters.add('-') stringCharacters.add(' ') } } + String(stringCharacters.toCharArray()) } else { - "\n- $string" + "\n- $result" } } else { - string = "\n- one\n- two\n- three\n" + result = "\n- one\n- two\n- three\n" } - return string + + return result } - fun stringToUncheckedString (string: String): String { - var string = string - if (string.replace(" ", "").isNotEmpty()) { - string = if (string.contains("\n")) { + fun stringToUncheckedString(string: String): String { + var result = string + + if (result.replace(" ", "").isNotEmpty()) { + result = if (result.contains("\n")) { val stringCharacters = mutableListOf() stringCharacters.add('-') stringCharacters.add(' ') @@ -638,8 +732,10 @@ class MiscUtilities (applicationContext: Context) { stringCharacters.add(' ') stringCharacters.add(']') stringCharacters.add(' ') - for (c in string) { + + for (c in result) { stringCharacters.add(c) + if (c == '\n') { stringCharacters.add('-') stringCharacters.add(' ') @@ -649,20 +745,23 @@ class MiscUtilities (applicationContext: Context) { stringCharacters.add(' ') } } + String(stringCharacters.toCharArray()) } else { - "\n- [ ] $string" + "\n- [ ] $result" } } else { - string = "\n- [ ] one\n- [ ] two\n- [ ] three\n" + result = "\n- [ ] one\n- [ ] two\n- [ ] three\n" } - return string + + return result } - fun stringToCheckedString (string: String): String { - var string = string - if (string.replace(" ", "").isNotEmpty()) { - string = if (string.contains("\n")) { + fun stringToCheckedString(string: String): String { + var result = string + + if (result.replace(" ", "").isNotEmpty()) { + result = if (result.contains("\n")) { val stringCharacters = mutableListOf() stringCharacters.add('-') stringCharacters.add(' ') @@ -670,8 +769,10 @@ class MiscUtilities (applicationContext: Context) { stringCharacters.add('x') stringCharacters.add(']') stringCharacters.add(' ') - for (c in string) { + + for (c in result) { stringCharacters.add(c) + if (c == '\n') { stringCharacters.add('-') stringCharacters.add(' ') @@ -681,113 +782,126 @@ class MiscUtilities (applicationContext: Context) { stringCharacters.add(' ') } } + String(stringCharacters.toCharArray()) } else { - "\n- [x] $string" + "\n- [x] $result" } } else { - string = "\n- [x] one\n- [x] two\n- [x] three\n" + result = "\n- [x] one\n- [x] two\n- [x] three\n" } - return string + + return result } - fun stringToTitledStrings (string: String): String { - var string = string - if (string.replace(" ", "").isNotEmpty()) { - string = if (string.contains("\n")) { + fun stringToTitledStrings(string: String): String { + var result = string + + if (result.replace(" ", "").isNotEmpty()) { + result = if (result.contains("\n")) { val stringCharacters = mutableListOf() stringCharacters.add('\n') stringCharacters.add('#') stringCharacters.add(' ') - for (c in string) { + + for (c in result) { stringCharacters.add(c) + if (c == '\n') { stringCharacters.add('\n') stringCharacters.add('#') stringCharacters.add(' ') } } + String(stringCharacters.toCharArray()) + "\n" } else { - "# $string\n" + "# $result\n" } } else { - string = "# " + result = "# " } - return string - } -// UI stuff -open class OnSwipeTouchListener(c: Context?) : View.OnTouchListener { - private val gestureDetector: GestureDetector - override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean { - return gestureDetector.onTouchEvent(motionEvent!!) + return result } - private inner class GestureListener : GestureDetector.SimpleOnGestureListener() { - - private val SWIPE_THRESHOLD = 100 - private val SWIPE_VELOCITY_THRESHOLD = 100 + // UI stuff + open class OnSwipeTouchListener(c: Context?) : View.OnTouchListener { + private val gestureDetector: GestureDetector - override fun onDown(e: MotionEvent): Boolean { - return true + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean { + return gestureDetector.onTouchEvent(motionEvent!!) } - override fun onSingleTapUp(e: MotionEvent): Boolean { - onClick() - return super.onSingleTapUp(e) - } + private inner class GestureListener : GestureDetector.SimpleOnGestureListener() { + private val SWIPE_THRESHOLD = 100 + private val SWIPE_VELOCITY_THRESHOLD = 100 - override fun onDoubleTap(e: MotionEvent): Boolean { - onDoubleClick() - return super.onDoubleTap(e) - } + override fun onDown(e: MotionEvent): Boolean { + return true + } - override fun onLongPress(e: MotionEvent) { - onLongClick() - super.onLongPress(e) - } + override fun onSingleTapUp(e: MotionEvent): Boolean { + onClick() + return super.onSingleTapUp(e) + } - // Determines the fling velocity and then fires the appropriate swipe event accordingly - override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { - val result = false - try { - val diffY = e2.y - e1.y - val diffX = e2.x - e1.x - if (Math.abs(diffX) > Math.abs(diffY)) { - if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { - if (diffX > 0) { - onSwipeRight() - } else { - onSwipeLeft() + override fun onDoubleTap(e: MotionEvent): Boolean { + onDoubleClick() + return super.onDoubleTap(e) + } + + override fun onLongPress(e: MotionEvent) { + onLongClick() + super.onLongPress(e) + } + + // Determines the fling velocity and then fires the appropriate swipe event accordingly + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + val result = false + try { + val diffY = e2.y - (e1?.y ?: 0f) + val diffX = e2.x - (e1?.x ?: 0f) + if (abs(diffX) > abs(diffY)) { + if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { + if (diffX > 0) { + onSwipeRight() + } else { + onSwipeLeft() + } } - } - } else { - if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { - if (diffY > 0) { - onSwipeDown() - } else { - onSwipeUp() + } else { + if (abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { + if (diffY > 0) { + onSwipeDown() + } else { + onSwipeUp() + } } } + } catch (exception: java.lang.Exception) { + exception.printStackTrace() } - } catch (exception: java.lang.Exception) { - exception.printStackTrace() + return result } - return result } - } - - open fun onSwipeRight() {} - open fun onSwipeLeft() {} - open fun onSwipeUp() {} - open fun onSwipeDown() {} - open fun onClick() {} - fun onDoubleClick() {} - open fun onLongClick() {} + open fun onSwipeRight() {} + open fun onSwipeLeft() {} + open fun onSwipeUp() {} + open fun onSwipeDown() {} + open fun onClick() {} + fun onDoubleClick() {} + open fun onLongClick() {} - init { - gestureDetector = GestureDetector(c, GestureListener()) + init { + gestureDetector = GestureDetector(c, GestureListener()) + } } -}} \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/kotlin/cloud/keyspace/android/ResourceUtilities.kt b/app/src/main/kotlin/cloud/keyspace/android/ResourceUtilities.kt new file mode 100644 index 0000000..294c79b --- /dev/null +++ b/app/src/main/kotlin/cloud/keyspace/android/ResourceUtilities.kt @@ -0,0 +1,15 @@ +package cloud.keyspace.android + +import android.content.Context +import android.util.TypedValue +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt + +@ColorInt +fun Context.attrToColor( + @AttrRes + attr: Int +): Int = with(TypedValue()) { + theme.resolveAttribute(attr, this, true) + data +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index c60a1e1..0000000 --- a/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - ext.kotlin_version = "1.4.31" - repositories { - //noinspection JcenterRepositoryObsolete - jcenter() - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0-RC2' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - //noinspection JcenterRepositoryObsolete - jcenter() - google() - maven { url "https://jitpack.io" } - mavenCentral() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..043ae80 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.kotlinAndroid) apply false +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..181a987 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,9 @@ +import org.gradle.kotlin.dsl.`kotlin-dsl` + +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() +} diff --git a/buildSrc/src/main/kotlin/AppBuildConfig.kt b/buildSrc/src/main/kotlin/AppBuildConfig.kt new file mode 100644 index 0000000..b407fff --- /dev/null +++ b/buildSrc/src/main/kotlin/AppBuildConfig.kt @@ -0,0 +1,67 @@ +object AppBuildConfig { + const val compileSdk = 34 + const val minSdk = 27 + const val targetSdk = 33 + + const val appId = "cloud.keyspace.android" + const val namespace = "cloud.keyspace.android" + const val versionName = "1.4.2" + val versionCode = versionNameToVersionCode(versionName) + + /** + * Converts the given version name (String) to a version code (Integer). + * + * @param versionName The version name to be converted. It must be in the format: + * x.y.z
+ * where: + *
    + *
  • x is the major version (at least 1 digit) + *
  • y is the minor version (at least 1 digit, at most 3 digits) + *
  • y is the patch number (at least 1 digit, at most 3 digits) + *
+ * + * @return The version code encoded in the format: xyyyzzz
+ * where: + *
    + *
  • x is the major version (at least 1 digit) + *
  • yyy is the minor version (3 digits; from 000 to 999) + *
  • zzz is the patch number (3 digits; from 000 to 999) + *
      + */ + fun versionNameToVersionCode(versionName: String): Int { + println("versionNameToVersionCode: versionName = $versionName") + + val parts = versionName.split(".") + + if (parts.size != 3) { + throw IllegalArgumentException("version name must be in the format: x.y.z") + } + + val major = parts[0] + println("versionNameToVersionCode: major = $major") + + val minor = parts[1] + println("versionNameToVersionCode: minor = $minor") + + val patch = parts[2] + println("versionNameToVersionCode: patch = $patch") + + if (major.isEmpty()) { + throw IllegalArgumentException("major version must be at least 1 digit") + } + + if ((minor.length > 3) || (minor.isEmpty())) { + throw IllegalArgumentException("minor version must be at least 1 digit, at most 3 digits") + } + + if ((patch.length > 3) || (patch.isEmpty())) { + throw IllegalArgumentException("patch number must be at least 1 digit, at most 3 digits") + } + + val versionCodeString = parts[0] + parts[1].padStart(3, '0') + parts[2].padStart(3, '0') + val versionCode = versionCodeString.toInt() + println("versionNameToVersionCode: versionCode = $versionCode") + + return versionCode + } +} diff --git a/gradle.properties b/gradle.properties index 2206ad2..268da06 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,3 +17,5 @@ org.gradle.parallel=true org.gradle.jvmargs=-Xmx4608M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8 android.enableJetifier=true android.useAndroidX=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d6dce6a --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,147 @@ +[versions] +agp = "8.2.0" +kotlin = "1.9.10" +java = "17" + +kotlinx-coroutines-android = "1.7.3" +kotlinx-coroutines-core = "1.7.3" + +android-multidex = "2.0.1" +android-volley = "1.2.1" + +androidx-appcompat = "1.6.1" +androidx-autofill = "1.1.0" +androidx-biometric = "1.2.0-alpha05" +androidx-cardview = "1.0.0" +androidx-constraintlayout = "2.1.4" +androidx-core-ktx = "1.12.0" +androidx-core-splashscreen = "1.0.1" +androidx-drawerlayout = "1.2.0" +androidx-fragment-ktx = "1.6.2" +androidx-gridlayout = "1.0.0" +androidx-legacy-support-v4 = "1.0.0" +androidx-lifecycle-livedata-ktx = "2.6.2" +androidx-lifecycle-viewmodel-ktx = "2.6.2" +androidx-navigation-fragment-ktx = "2.7.5" +androidx-navigation-ui-ktx = "2.7.5" +androidx-preference-ktx = "1.2.1" +androidx-security-app-authenticator = "1.0.0-alpha02" +androidx-security-crypto = "1.1.0-alpha06" + +google-material = "1.10.0" +google-gson = "2.10.1" +google-zxing = "3.5.1" + +compose-kotlin-compiler-extension = "1.5.3" +compose-bom = "2023.10.01" +compose-activity = "1.8.1" +compose-date-picker = "1.0.1" +compose-lifecycle-viewmodel = "2.6.2" + +kotlin-bip39 = "1.0.2" +code-scanner = "2.1.0" +jackson-module-kotlin = "2.12.1" +colorpicker = "2.3" +awesomeqrcode = "1.2.0" +swipeable-recyclerview = "1.1" +lazysodium-android = "5.0.2" #@aar" +permissionx = "1.6.4" +nv-websocket-client = "2.14" +zxcvbn = "1.7.0" +pixplicity-library = "1.1.2" #@aar" +rootbeer-lib = "0.1.0" +markdown-processor = "0.1.3" +rxmarkdown-wrapper = "0.1.3" +kotlin-onetimepassword = "2.4.0" +markdownedittext = "1.1.3" +rxandroid = "1.2.1" +rxjava = "1.3.8" +jna = "5.10.0" #@aar" +acra = "5.9.7" +anggrayudi-storage = "1.5.4" + +androidx-test-core-ktx = "1.5.0" +androidx-test-junit = "1.1.5" +androidx-test-espresso = "3.5.1" +junit = "4.13.2" + +[libraries] + +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" } + +android-multidex = { group = "com.android.support", name = "multidex", version.ref = "android-multidex" } +android-volley = { group = "com.android.volley", name = "volley", version.ref = "android-volley" } + +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } +androidx-autofill = { group = "androidx.autofill", name = "autofill", version.ref = "androidx-autofill" } +androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidx-biometric" } +androidx-cardview = { group = "androidx.cardview", name = "cardview", version.ref = "androidx-cardview" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" } +androidx-drawerlayout = { group = "androidx.drawerlayout", name = "drawerlayout", version.ref = "androidx-drawerlayout" } +androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "androidx-fragment-ktx" } +androidx-gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "androidx-gridlayout" } +androidx-legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "androidx-legacy-support-v4" } +androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidx-lifecycle-livedata-ktx" } +androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-viewmodel-ktx" } +androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "androidx-navigation-fragment-ktx" } +androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "androidx-navigation-ui-ktx" } +androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "androidx-preference-ktx" } +androidx-security-app-authenticator = { group = "androidx.security", name = "security-app-authenticator", version.ref = "androidx-security-app-authenticator" } +androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "androidx-security-crypto" } + +google-material = { group = "com.google.android.material", name = "material", version.ref = "google-material" } +google-gson = { group = "com.google.code.gson", name = "gson", version.ref = "google-gson" } +google-zxing = { group = "com.google.zxing", name = "core", version.ref = "google-zxing" } + +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } +compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "compose-activity" } +compose-animation = { group = "androidx.compose.animation", name = "animation"} +compose-date-picker = { group = "com.github.DogusTeknoloji", name = "compose-date-picker", version.ref = "compose-date-picker" } +compose-foundation = { group = "androidx.compose.foundation", name = "foundation"} +compose-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "compose-lifecycle-viewmodel" } +compose-material = { group = "androidx.compose.material", name = "material"} +compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended"} +compose-material3 = { group = "androidx.compose.material3", name = "material3" } +compose-paging = { group = "androidx.paging", name = "paging-compose"} +compose-runtime = { group = "androidx.compose.runtime", name = "runtime"} +compose-ui = { group = "androidx.compose.ui", name = "ui" } +compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } + +kotlin-bip39 = { group = "cash.z.ecc.android", name = "kotlin-bip39", version.ref = "kotlin-bip39" } +code-scanner = { group = "com.budiyev.android", name = "code-scanner", version.ref = "code-scanner" } +jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jackson-module-kotlin" } +colorpicker = { group = "com.github.dhaval2404", name = "colorpicker", version.ref = "colorpicker" } +awesomeqrcode = { group = "com.github.sumimakito", name = "awesomeqrcode", version.ref = "awesomeqrcode" } +swipeable-recyclerview = { group = "com.github.tsuryo", name = "Swipeable-RecyclerView", version.ref = "swipeable-recyclerview" } +lazysodium-android = { group = "com.goterl", name = "lazysodium-android", version.ref = "lazysodium-android" } +permissionx = { group = "com.guolindev.permissionx", name = "permissionx", version.ref = "permissionx" } +nv-websocket-client = { group = "com.neovisionaries", name = "nv-websocket-client", version.ref = "nv-websocket-client" } +zxcvbn = { group = "com.nulab-inc", name = "zxcvbn", version.ref = "zxcvbn" } +pixplicity-library = { group = "com.pixplicity.sharp", name = "library", version.ref = "pixplicity-library" } +rootbeer-lib = { group = "com.scottyab", name = "rootbeer-lib", version.ref = "rootbeer-lib" } +markdown-processor = { group = "com.yydcdut", name = "markdown-processor", version.ref = "markdown-processor" } +rxmarkdown-wrapper = { group = "com.yydcdut", name = "rxmarkdown-wrapper", version.ref = "rxmarkdown-wrapper" } +kotlin-onetimepassword = { group = "dev.turingcomplete", name = "kotlin-onetimepassword", version.ref = "kotlin-onetimepassword" } +markdownedittext = { group = "io.github.yahiaangelo.markdownedittext", name = "markdownedittext", version.ref = "markdownedittext" } +rxandroid = { group = "io.reactivex", name = "rxandroid", version.ref = "rxandroid" } +rxjava = { group = "io.reactivex", name = "rxjava", version.ref = "rxjava" } +jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } +acra-mail = { group = "ch.acra", name = "acra-mail", version.ref = "acra" } +acra-dialog = { group = "ch.acra", name = "acra-dialog", version.ref = "acra" } +anggrayudi-storage = { group = "com.anggrayudi", name = "storage", version.ref = "anggrayudi-storage" } + +androidx-test-core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "androidx-test-core-ktx" } +androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } +androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-test-espresso" } +junit = { group = "junit", name = "junit", version.ref = "junit" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index eae431e..acd7f33 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Dec 07 19:55:29 IST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index ba6bea6..0000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = "Keyspace" -include ':app' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..cd63370 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + //noinspection JcenterRepositoryObsolete + jcenter() + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + //noinspection JcenterRepositoryObsolete + jcenter() + google() + mavenCentral() + maven("https://jitpack.io") + } +} + +rootProject.name = "Keyspace" +include(":app")