diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml index 2b50263..ef7dbc8 100644 --- a/.idea/assetWizardSettings.xml +++ b/.idea/assetWizardSettings.xml @@ -11,30 +11,11 @@ - diff --git a/app/build.gradle b/app/build.gradle index 22699d1..dc44059 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "cloud.keyspace.android" minSdkVersion 27 targetSdkVersion 33 - versionCode 141 - versionName "1.4.1" + versionCode 142 + versionName "1.4.2" multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -51,7 +51,7 @@ 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.5.1' + 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' @@ -67,8 +67,8 @@ dependencies { 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.4' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' + 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' @@ -99,4 +99,8 @@ dependencies { 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 -} \ No newline at end of file + + // ACRA for crash logging + implementation 'ch.acra:acra-mail:5.9.7' // mail component + implementation 'ch.acra:acra-dialog:5.9.7' // dialog component +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb43..63ab79d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,21 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +#ACRA specifics +# Restore some Source file names and restore approximate line numbers in the stack traces, +# otherwise the stack traces are pretty useless +-keepattributes SourceFile, LineNumberTable + +# ACRA needs "annotations" so add this... +# Note: This may already be defined in the default "proguard-android-optimize.txt" +# file in the SDK. If it is, then you don't need to duplicate it. See your +# "project.properties" file to get the path to the default "proguard-android-optimize.txt". +-keepattributes *Annotation* + +# Keep all the ACRA classes +-keep class org.acra.** { *; } + +# Don't warn about removed methods from AppCompat +-dontwarn android.support.v4.app.NotificationCompat* \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2239f5f..788cabe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ android:fullBackupContent="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:name="cloud.keyspace.android.Keyspace" android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" @@ -73,6 +74,12 @@ android:theme="@style/Theme.KeyspaceMobile.NoActionBar" android:configChanges="keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustPan" /> + - - vault.card!!.remove(io.getCard(itemId!!, vault)) - io.writeVault(vault) - - network.writeQueueTask (itemId!!, mode = network.MODE_DELETE) - crypto.secureStartActivity ( - nextActivity = Dashboard(), - nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), - keyring = keyring, - itemId = null - ) - - } - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Go back") { dialog, _ -> dialog.dismiss() } - alertDialog.show() - + val builder = MaterialAlertDialogBuilder(this@AddCard) + .setTitle("Delete card") + .setCancelable(true) + .setMessage("Would you like to delete this card?") + .setNegativeButton("Go back"){ _, _ -> } + .setPositiveButton("Delete"){ _, _ -> + deleted = !deleted + saveItem() + } + builder.show() } - } else { - deleteButton.visibility = View.GONE - } + } else deleteButton.visibility = View.GONE tagButton = findViewById (R.id.tag) tagPicker = AddTag (tagId, applicationContext, this@AddCard, keyring) @@ -266,12 +256,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 (s.isNotEmpty() && s.length % 5 == 0) { - val c = s[s.length - 1] 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.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) + } + } + } + if (s.toString().replace(" ", "").length > 19) { + 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) { @@ -372,7 +374,9 @@ class AddCard : AppCompatActivity() { vault.card?.remove(io.getCard(itemId!!, vault)) } - if (cardNumberInput.text.toString().length < 16) cardNumberInput.error = "Enter a valid 16 digit card number" + 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" @@ -388,6 +392,7 @@ class AddCard : AppCompatActivity() { name = nameInput.text.toString(), color = cardColor, favorite = favorite, + deleted = deleted, tagId = tagPicker.getSelectedTagId() ?: tagId, dateCreated = dateCreated, dateModified = Instant.now().epochSecond, @@ -438,7 +443,9 @@ class AddCard : AppCompatActivity() { notesInput.setText(card.notes) - 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) securityCode.setText(card.securityCode) cardholderNameInput.setText(card.cardholderName) diff --git a/app/src/main/kotlin/cloud/keyspace/android/AddLogin.kt b/app/src/main/kotlin/cloud/keyspace/android/AddLogin.kt index 6e1dba2..4d0769b 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/AddLogin.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/AddLogin.kt @@ -74,8 +74,6 @@ class AddLogin : AppCompatActivity() { lateinit var emailInputLayout: TextInputLayout lateinit var emailInput: TextInputEditText - lateinit var emailAsUsername: MaterialSwitch - lateinit var passwordInputLayout: TextInputLayout lateinit var passwordInput: TextInputEditText lateinit var clearButton: ImageView @@ -136,6 +134,7 @@ class AddLogin : AppCompatActivity() { lateinit var doneButton: ImageView lateinit var backButton: ImageView lateinit var deleteButton: ImageView + var deleted: Boolean = false lateinit var keyring: CryptoUtilities.Keyring private var itemId: String? = null @@ -195,6 +194,11 @@ class AddLogin : AppCompatActivity() { } } + if (secretInput.text.toString().isNotBlank() && secretInput.text.toString().length < 6) { + secretInput.error = "Please enter a valid TOTP secret" + return@setOnClickListener + } + saveItem() } @@ -206,31 +210,20 @@ class AddLogin : AppCompatActivity() { deleteButton = findViewById (R.id.delete) if (itemId != null) { + deleteButton.visibility = View.VISIBLE deleteButton.setOnClickListener { - val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() - alertDialog.setTitle("Delete") - alertDialog.setMessage("Would you like to delete \"${login.name}\"") - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Delete") { dialog, _ -> - - vault.login!!.remove(io.getLogin(itemId!!, vault)) - io.writeVault(vault) - - network.writeQueueTask (itemId!!, mode = network.MODE_DELETE) - crypto.secureStartActivity ( - nextActivity = Dashboard(), - nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), - keyring = keyring, - itemId = null - ) - - } - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Go back") { dialog, _ -> dialog.dismiss() } - alertDialog.show() - + val builder = MaterialAlertDialogBuilder(this@AddLogin) + .setTitle("Delete login") + .setCancelable(true) + .setMessage("Would you like to delete this login?") + .setNegativeButton("Go back"){ _, _ -> } + .setPositiveButton("Delete"){ _, _ -> + deleted = !deleted + saveItem() + } + builder.show() } - } else { - deleteButton.visibility = View.GONE - } + } else deleteButton.visibility = View.GONE tagButton = findViewById (R.id.tag) tagPicker = AddTag (tagId, applicationContext, this@AddLogin, keyring) @@ -290,14 +283,6 @@ class AddLogin : AppCompatActivity() { userNameInput = findViewById (R.id.userNameInput) userNameInput.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING userNameInputLayout = findViewById (R.id.userNameInputLayout) - userNameInputLayout.visibility = View.GONE - - emailAsUsername = findViewById (R.id.emailAsUsername) - emailAsUsername.isChecked = true - emailAsUsername.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) userNameInputLayout.visibility = View.GONE - else userNameInputLayout.visibility = View.VISIBLE - } emailInput = findViewById (R.id.emailInput) emailInput.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING @@ -629,7 +614,10 @@ class AddLogin : AppCompatActivity() { 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 >= 8) { + if (s.length >= 6) { + + mfaTokenBox.visibility = View.VISIBLE + try { otpCode = GoogleAuthenticator(base32secret = secretInput.text.toString()).generate() runOnUiThread { tokenPreview.text = otpCode!!.replace("...".toRegex(), "$0 ") } @@ -645,9 +633,7 @@ class AddLogin : AppCompatActivity() { } } }, 0, 1000) // 1000 milliseconds = 1 second - } catch (timerError: IllegalStateException) { } - - mfaTokenBox.visibility = View.VISIBLE + } catch (_: IllegalStateException) { } catch (_: IllegalArgumentException) { mfaTokenBox.visibility = View.GONE } } else { mfaTokenBox.visibility = View.GONE @@ -784,10 +770,6 @@ class AddLogin : AppCompatActivity() { if (!login.loginData!!.username.isNullOrBlank()) { userNameInput.setText(login.loginData.username) - emailAsUsername.isChecked = false - } else { - userNameInputLayout.visibility = View.GONE - emailAsUsername.isChecked = true } if (!login.loginData.password.isNullOrEmpty()) { @@ -982,6 +964,7 @@ class AddLogin : AppCompatActivity() { name = siteNameInput.text.toString(), notes = notesInput.text.toString(), favorite = favorite, + deleted = deleted, tagId = tagPicker.getSelectedTagId() ?: tagId, loginData = IOUtilities.LoginData( username = userNameInput.text.toString(), diff --git a/app/src/main/kotlin/cloud/keyspace/android/AddNote.kt b/app/src/main/kotlin/cloud/keyspace/android/AddNote.kt index 928f0b1..d6716bb 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/AddNote.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/AddNote.kt @@ -84,6 +84,7 @@ class AddNote : AppCompatActivity() { lateinit var doneButton: ImageView lateinit var backButton: ImageView lateinit var deleteButton: ImageView + var deleted: Boolean = false lateinit var noteToolbar: HorizontalScrollView @@ -647,7 +648,7 @@ class AddNote : AppCompatActivity() { alertDialog.setMessage("Note can't be blank") alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Go back") { dialog, _ -> dialog.dismiss() } alertDialog.show() - } else saveNote() + } else saveItem() } @@ -661,30 +662,18 @@ class AddNote : AppCompatActivity() { deleteButton = findViewById (R.id.delete) if (itemId != null) { deleteButton.setOnClickListener { - val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() - alertDialog.setTitle("Delete") - alertDialog.setMessage("Would you like to delete this note?") - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Delete") { dialog, _ -> - - vault.note!!.remove(io.getNote(itemId!!, vault)) - io.writeVault(vault) - - network.writeQueueTask (itemId!!, mode = network.MODE_DELETE) - crypto.secureStartActivity ( - nextActivity = Dashboard(), - nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), - keyring = keyring, - itemId = null - ) - - } - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Go back") { dialog, _ -> dialog.dismiss() } - alertDialog.show() - + val builder = MaterialAlertDialogBuilder(this@AddNote) + .setTitle("Delete note") + .setCancelable(true) + .setMessage("Would you like to delete this note?") + .setNegativeButton("Go back"){ _, _ -> } + .setPositiveButton("Delete"){ _, _ -> + deleted = !deleted + saveItem() + } + builder.show() } - } else { - deleteButton.visibility = View.GONE - } + } else deleteButton.visibility = View.GONE tagButton = findViewById (R.id.tag) tagPicker = AddTag (tagId, applicationContext, this@AddNote, keyring) @@ -867,7 +856,7 @@ class AddNote : AppCompatActivity() { return true } - private fun saveNote () { + private fun saveItem () { var dateCreated = Instant.now().epochSecond if (itemId != null) { @@ -882,6 +871,7 @@ class AddNote : AppCompatActivity() { notes = noteEditor.text.toString(), color = noteColor, favorite = favorite, + deleted = deleted, tagId = tagPicker.getSelectedTagId() ?: tagId, dateCreated = dateCreated, dateModified = timestamp, diff --git a/app/src/main/kotlin/cloud/keyspace/android/Dashboard.kt b/app/src/main/kotlin/cloud/keyspace/android/Dashboard.kt index 942c084..3440864 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/Dashboard.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/Dashboard.kt @@ -166,7 +166,6 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi currentActivityClassNameAsString = getString(R.string.title_activity_dashboard), intent = intent ).first - } } catch (themeSwitched: Exception) { Toast.makeText(applicationContext, "Restarting app to apply theme", Toast.LENGTH_LONG).show() @@ -635,7 +634,7 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi loginSearchableData.add(login.loginData?.siteUrls.toString()) val loginSearchableDataString = loginSearchableData.filterNotNull().joinToString("").lowercase(Locale.getDefault()).filter { it.isLetterOrDigit() } - if (loginSearchableDataString.contains(searchTerms.toString().lowercase(Locale.getDefault()).filter { it.isLetterOrDigit() })) { // other search + if (loginSearchableDataString.contains(searchTerms.toString().lowercase(Locale.getDefault()).filter { it.isLetterOrDigit() })) { searchTermsList.add(login) } @@ -1086,10 +1085,14 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi if (!login.loginData?.email.isNullOrEmpty()) { loginCard.usernameText.text = login.loginData!!.email loginCard.usernameText.setCompoundDrawablesRelativeWithIntrinsicBounds (emailIcon, null, null, null) - } else if (!login.loginData?.username.isNullOrEmpty()) { + } + + if (!login.loginData?.username.isNullOrEmpty()) { loginCard.usernameText.text = login.loginData!!.username loginCard.usernameText.setCompoundDrawablesRelativeWithIntrinsicBounds (loginIcon, null, null, null) - } else loginCard.usernameText.visibility = View.GONE + } + + if (login.loginData?.username.isNullOrEmpty() && login.loginData?.email.isNullOrEmpty()) loginCard.usernameText.visibility = View.GONE loginCard.miscText.visibility = View.GONE @@ -1186,6 +1189,8 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi private fun renderLoginsFragment () { + vault.login?.forEach { if (!it.deleted) logins.add(it) } + sortBy = configData.getString("sort_by", io.SORT_LAST_EDITED)!! vault = io.vaultSorter(vault, sortBy) @@ -1201,7 +1206,7 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi killBottomSheet() - if (vault.login.isNullOrEmpty()) { + if (logins.isEmpty()) { try { fragmentRoot.removeView(fragmentView) } catch (uninflated: UninitializedPropertyAccessException) { } fragmentView = inflater.inflate(R.layout.no_vault_data, null) fragmentRoot.addView(fragmentView) @@ -1213,8 +1218,7 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi fragmentRoot.addView(fragmentView) logins.clear() - for (encryptedLogin in io.getLogins(vault)) - logins.add(io.decryptLogin(encryptedLogin)) + for (encryptedLogin in io.getLogins(vault)) if (!encryptedLogin.deleted) logins.add(io.decryptLogin(encryptedLogin)) loginsRecycler = fragmentView.findViewById(R.id.logins_recycler) loginsRecycler.layoutManager = LinearLayoutManager(this@Dashboard) @@ -1966,7 +1970,9 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi } } - cardCard.cardNumber.text = card.cardNumber?.replace("....".toRegex(), "$0 ") + if (card.cardNumber?.length == 16) cardCard.cardNumber.text = card.cardNumber.replace("....".toRegex(), "$0 ") + else cardCard.cardNumber.text = card.cardNumber + cardCard.toDate.text = card.expiry cardCard.cardHolder.text = card.cardholderName @@ -2128,9 +2134,11 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi ) } + if (!card.pin.isNullOrBlank()) cardCard.pin.text = card.pin else cardCard.pinLayout.visibility = View.GONE + fun hideCodes () { - cardCard.pin.text = "●●●●" - cardCard.securityCode.text = "●●●" + cardCard.pin.text = "••••" + cardCard.securityCode.text = "•••" cardCard.hideCodes.setImageDrawable(getDrawable(R.drawable.ic_baseline_visibility_off_24)) cardCard.pin.setOnClickListener(null) cardCard.securityCode.setOnClickListener(null) @@ -2138,6 +2146,7 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi var codesHidden = true hideCodes() + cardCard.hideCodes.setOnClickListener { codesHidden = !codesHidden if (codesHidden) { @@ -2190,6 +2199,8 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi private fun renderNotesFragment () { + vault.note?.forEach { if (!it.deleted) notes.add(it) } + sortBy = configData.getString("sort_by", io.SORT_LAST_EDITED)!! vault = io.vaultSorter(vault, sortBy) @@ -2205,7 +2216,7 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi killBottomSheet() - if (io.getNotes(io.getVault()).size == 0) { + if (notes.isEmpty()) { try { fragmentRoot.removeView(fragmentView) } catch (uninflated: UninitializedPropertyAccessException) { } fragmentView = inflater.inflate(R.layout.no_vault_data, null) fragmentRoot.addView(fragmentView) @@ -2220,8 +2231,7 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi fragmentRoot.addView(fragmentView) notes.clear() - for (encryptedNote in io.getNotes(vault)) - notes.add(io.decryptNote(encryptedNote)) + for (encryptedNote in io.getNotes(vault)) if (!encryptedNote.deleted) notes.add(io.decryptNote(encryptedNote)) notesRecycler = fragmentView.findViewById(R.id.notes_recycler) @@ -2289,6 +2299,8 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi private fun renderCardsFragment () { + vault.card?.forEach { if (!it.deleted) cards.add(it) } + sortBy = configData.getString("sort_by", io.SORT_LAST_EDITED)!! vault = io.vaultSorter(vault, sortBy) @@ -2303,20 +2315,19 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi } killBottomSheet() - if (vault.card.isNullOrEmpty()) { - try { fragmentRoot.removeView(fragmentView) } catch (uninflated: UninitializedPropertyAccessException) { } + if (cards.isEmpty()) { + try { fragmentRoot.removeView(fragmentView) } catch (_: UninitializedPropertyAccessException) { } fragmentView = inflater.inflate(R.layout.no_vault_data, null) fragmentRoot.addView(fragmentView) if (coldStart) fragmentView.startAnimation(loadAnimation(applicationContext, android.R.anim.fade_in)); } else { - try { fragmentRoot.removeView(fragmentView) } catch (uninflated: UninitializedPropertyAccessException) { } + try { fragmentRoot.removeView(fragmentView) } catch (_: UninitializedPropertyAccessException) { } fragmentView = inflater.inflate(R.layout.dashboard_fragment_cards, null) fragmentRoot.addView(fragmentView) cards.clear() - for (encryptedCard in io.getCards(vault)) - cards.add(io.decryptCard(encryptedCard)) + for (encryptedCard in io.getCards(vault)) if (!encryptedCard.deleted) cards.add(io.decryptCard(encryptedCard)) cardsRecycler = fragmentView.findViewById(R.id.cards_recycler) cardsRecycler.layoutManager = LinearLayoutManager(this) @@ -2703,6 +2714,7 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi name = decoded2faData?.label ?: decoded2faData?.issuer ?: decoded2faData?.account, notes = null, favorite = false, + deleted = false, tagId = null, loginData = IOUtilities.LoginData( username = decoded2faData?.account ?: decoded2faData?.label, @@ -2820,6 +2832,15 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi finish() } + private fun openDeletedItems () { + crypto.secureStartActivity ( + nextActivity = DeletedItems(), + nextActivityClassNameAsString = getString(R.string.title_activity_deleted_items), + keyring = keyring, + itemId = null + ) + } + private fun loginInfoDialog() { val builder = MaterialAlertDialogBuilder(this) builder.setCancelable(true) @@ -2841,6 +2862,7 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi val signOutButton = loginInfoBox.findViewById(R.id.signOutButton) val syncButton = loginInfoBox.findViewById(R.id.syncButton) + val deleteButton = loginInfoBox.findViewById(R.id.deleteButton) val sendFeedbackButton = loginInfoBox.findViewById(R.id.sendFeedbackButton) val keyspaceLogoHeader = loginInfoBox.findViewById(R.id.keyspaceLogoHeader) val settingsButton = loginInfoBox.findViewById(R.id.settingsButton) @@ -2848,6 +2870,11 @@ class Dashboard : AppCompatActivity(), NavigationView.OnNavigationItemSelectedLi val privacyPolicyButton = loginInfoBox.findViewById(R.id.privacyPolicyButton) val termsOfServiceButton = loginInfoBox.findViewById(R.id.termsOfServiceButton) + deleteButton.setOnClickListener { + dialog.dismiss() + openDeletedItems() + } + syncButton.setOnClickListener { dialog.dismiss() Toast.makeText(applicationContext, "Syncing vault", Toast.LENGTH_SHORT).show() diff --git a/app/src/main/kotlin/cloud/keyspace/android/DeletedItems.kt b/app/src/main/kotlin/cloud/keyspace/android/DeletedItems.kt new file mode 100644 index 0000000..8a4a9dd --- /dev/null +++ b/app/src/main/kotlin/cloud/keyspace/android/DeletedItems.kt @@ -0,0 +1,995 @@ +package cloud.keyspace.android + +import android.animation.Animator +import android.animation.AnimatorInflater +import android.animation.AnimatorSet +import android.annotation.SuppressLint +import android.app.ProgressDialog.show +import android.content.* +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.animation.AnimationUtils +import android.widget.* +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.cardview.widget.CardView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.startActivity +import androidx.core.graphics.drawable.DrawableCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.progressindicator.LinearProgressIndicator +import com.keyspace.keyspacemobile.NetworkUtilities +import com.nulabinc.zxcvbn.Zxcvbn +import com.yydcdut.markdown.MarkdownConfiguration +import com.yydcdut.markdown.MarkdownProcessor +import com.yydcdut.markdown.callback.OnTodoClickCallback +import com.yydcdut.markdown.syntax.text.TextFactory +import com.yydcdut.markdown.theme.Theme +import com.yydcdut.markdown.theme.ThemeDefault +import com.yydcdut.markdown.theme.ThemeDesert +import dev.turingcomplete.kotlinonetimepassword.GoogleAuthenticator +import java.util.* +import kotlin.concurrent.thread + +class DeletedItems : AppCompatActivity() { + + lateinit var crypto: CryptoUtilities + lateinit var network: NetworkUtilities + lateinit var misc: MiscUtilities + lateinit var io: IOUtilities + lateinit var keyring: CryptoUtilities.Keyring + lateinit var configData: SharedPreferences + lateinit var zxcvbn: Zxcvbn + + lateinit var vault: IOUtilities.Vault + lateinit var tags: MutableList + var deletedLogins: MutableList = mutableListOf() + var deletedNotes: MutableList = mutableListOf() + var deletedCards: MutableList = mutableListOf() + + lateinit var loginsAdapter: LoginsAdapter + lateinit var notesAdapter: NotesAdapter + lateinit var cardsAdapter: CardsAdapter + + lateinit var deletedLoginsRecycler: RecyclerView + lateinit var deletedLoginsLabel: TextView + lateinit var deletedNotesRecycler: RecyclerView + lateinit var deletedNotesLabel: TextView + lateinit var deletedCardsRecycler: RecyclerView + lateinit var deletedCardsLabel: TextView + + lateinit var dangerZoneLayout: LinearLayout + lateinit var deletedItemsGraphic: LinearLayout + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.deleted_items) + + crypto = CryptoUtilities(applicationContext, this) + misc = MiscUtilities (applicationContext) + + configData = getSharedPreferences (applicationContext.packageName + "_configuration_data", MODE_PRIVATE) + + try { + if (!this::keyring.isInitialized) { + keyring = crypto.receiveKeyringFromSecureIntent ( + currentActivityClassNameAsString = getString(R.string.title_activity_deleted_items), + intent = intent + ).first + } + } catch (themeSwitched: Exception) { + Toast.makeText(applicationContext, "Restarting app to apply theme", Toast.LENGTH_LONG).show() + startActivity(Intent(applicationContext, StartHere::class.java)) + finishAffinity() + finish() + return + } + + network = NetworkUtilities(applicationContext, this, keyring) + io = IOUtilities (applicationContext, this, keyring) + + val allowScreenshots = configData.getBoolean("allowScreenshots", false) + if (!allowScreenshots) window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + + val backButton: ImageView = findViewById(R.id.backButton) + backButton.setOnClickListener { onBackPressed() } + + zxcvbn = Zxcvbn() + + vault = io.getVault() + vault = io.vaultSorter(vault, io.SORT_LAST_EDITED) + + tags = mutableListOf() + + vault.tag?.forEach { tags.add(io.decryptTag(it)!!) } + vault.login?.forEach { if (it.deleted) deletedLogins.add(io.decryptLogin(it)) } + vault.note?.forEach { if (it.deleted) deletedNotes.add(io.decryptNote(it)) } + vault.card?.forEach { if (it.deleted) deletedCards.add(io.decryptCard(it)) } + + dangerZoneLayout = findViewById(R.id.dangerZoneLayout) + deletedItemsGraphic = findViewById(R.id.deletedItemsGraphic) + + deletedLoginsRecycler = findViewById(R.id.deletedLoginsRecycler) + deletedLoginsLabel = findViewById(R.id.deletedLoginsLabel) + + if (deletedLogins.isEmpty()) { + deletedLoginsRecycler.visibility = View.GONE + deletedLoginsLabel.visibility = View.GONE + } else { + deletedLoginsRecycler.visibility = View.VISIBLE + deletedLoginsLabel.visibility = View.VISIBLE + deletedLoginsRecycler.layoutManager = LinearLayoutManager(this@DeletedItems) + deletedLoginsLabel.text = deletedLoginsLabel.text.toString() + " (" + deletedLogins.size + ")" + loginsAdapter = LoginsAdapter(deletedLogins) + loginsAdapter.setHasStableIds(true) + deletedLoginsRecycler.adapter = loginsAdapter + deletedLoginsRecycler.setItemViewCacheSize(50) + LinearLayoutManager(applicationContext).apply { isAutoMeasureEnabled = false } + deletedLoginsRecycler.recycledViewPool.setMaxRecycledViews(0, 0) + loginsAdapter.notifyItemInserted(deletedLogins.size) + deletedLoginsRecycler.isNestedScrollingEnabled = false + } + + + deletedNotesRecycler = findViewById(R.id.deletedNotesRecycler) + deletedNotesLabel = findViewById(R.id.deletedNotesLabel) + + if (deletedNotes.isEmpty()) { + deletedNotesRecycler.visibility = View.GONE + deletedNotesLabel.visibility = View.GONE + } else { + deletedNotesRecycler.visibility = View.VISIBLE + deletedNotesLabel.visibility = View.VISIBLE + deletedNotesRecycler.layoutManager = LinearLayoutManager(this@DeletedItems) + deletedNotesLabel.text = deletedNotesLabel.text.toString() + " (" + deletedNotes.size + ")" + + notesAdapter = NotesAdapter(deletedNotes) + notesAdapter.setHasStableIds(true) + deletedNotesRecycler.adapter = notesAdapter + deletedNotesRecycler.setItemViewCacheSize(50) + LinearLayoutManager(applicationContext).apply { isAutoMeasureEnabled = false } + deletedNotesRecycler.recycledViewPool.setMaxRecycledViews(0, 0) + notesAdapter.notifyItemInserted(deletedNotes.size) + deletedNotesRecycler.isNestedScrollingEnabled = false + } + + + deletedCardsRecycler = findViewById(R.id.deletedCardsRecycler) + deletedCardsLabel = findViewById(R.id.deletedCardsLabel) + + if (deletedCards.isEmpty()) { + deletedCardsRecycler.visibility = View.GONE + deletedCardsLabel.visibility = View.GONE + } else { + deletedCardsRecycler.visibility = View.VISIBLE + deletedCardsLabel.visibility = View.VISIBLE + deletedCardsRecycler.layoutManager = LinearLayoutManager(this@DeletedItems) + deletedCardsLabel.text = deletedCardsLabel.text.toString() + " (" + deletedCards.size + ")" + + cardsAdapter = CardsAdapter(deletedCards) + cardsAdapter.setHasStableIds(true) + deletedCardsRecycler.adapter = cardsAdapter + deletedCardsRecycler.setItemViewCacheSize(50) + LinearLayoutManager(applicationContext).apply { isAutoMeasureEnabled = false } + deletedCardsRecycler.recycledViewPool.setMaxRecycledViews(0, 0) + cardsAdapter.notifyItemInserted(deletedCards.size) + deletedCardsRecycler.isNestedScrollingEnabled = false + } + + val deletePermanentlyButton: LinearLayout = findViewById(R.id.deletePermanentlyButton) + deletePermanentlyButton.setOnClickListener { + val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() + alertDialog.setTitle("Delete permanently") + alertDialog.setMessage("Would you like to permanently delete these items? This action is irreversible.") + + alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Delete") { dialog, _ -> + deletePermanently() + onBackPressed() + } + alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Go back") { dialog, _ -> dialog.dismiss() } + alertDialog.show() + } + + val restoreAllButton: LinearLayout = findViewById(R.id.restoreAllButton) + restoreAllButton.setOnClickListener { + val alertDialog: AlertDialog = MaterialAlertDialogBuilder(this).create() + alertDialog.setTitle("Restore all") + alertDialog.setMessage("Would you like to restore these items?") + + alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "Restore") { dialog, _ -> + restorePermanently() + onBackPressed() + } + alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "Go back") { dialog, _ -> dialog.dismiss() } + alertDialog.show() + } + + if (deletedLogins.isEmpty() && deletedNotes.isEmpty() && deletedCards.isEmpty()) { + dangerZoneLayout.visibility = View.GONE + deletedItemsGraphic.visibility = View.VISIBLE + + } else { + dangerZoneLayout.visibility = View.VISIBLE + deletedItemsGraphic.visibility = View.GONE + + } + + } + + override fun onBackPressed () { + crypto.secureStartActivity ( + nextActivity = Dashboard(), + nextActivityClassNameAsString = getString(R.string.title_activity_dashboard), + keyring = keyring, + itemId = null + ) + super.onBackPressed() + + } + + private fun deletePermanently () { + + val newLogins = mutableListOf() + val newNotes = mutableListOf() + val newCards = mutableListOf() + + vault.login?.forEach { if (!it.deleted) newLogins.add(it) } + vault.note?.forEach { if (!it.deleted) newNotes.add(it) } + vault.card?.forEach { if (!it.deleted) newCards.add(it) } + + if (deletedLogins.isNotEmpty()) deletedLogins.forEach { network.writeQueueTask (it.id!!, mode = network.MODE_DELETE) } + if (deletedNotes.isNotEmpty()) deletedNotes.forEach { network.writeQueueTask (it.id!!, mode = network.MODE_DELETE) } + if (deletedCards.isNotEmpty()) deletedCards.forEach { network.writeQueueTask (it.id!!, mode = network.MODE_DELETE) } + + io.writeVault ( + IOUtilities.Vault( + version = vault.version, + tag = vault.tag, + login = newLogins, + note = newNotes, + card = newCards + ) + ) + + } + + private fun restorePermanently () { + + val newLogins = mutableListOf() + val newNotes = mutableListOf() + val newCards = mutableListOf() + + vault.login?.forEach { + if (it.deleted) it.deleted = false + newLogins.add(it) + } + + vault.note?.forEach { + if (it.deleted) it.deleted = false + newNotes.add(it) + } + + vault.card?.forEach { + if (it.deleted) it.deleted = false + newCards.add(it) + } + + newLogins.forEach { network.writeQueueTask (it, mode = network.MODE_PUT) } + newNotes.forEach { network.writeQueueTask (it, mode = network.MODE_PUT) } + newCards.forEach { network.writeQueueTask (it, mode = network.MODE_PUT) } + + io.writeVault ( + IOUtilities.Vault( + version = vault.version, + tag = vault.tag, + login = newLogins, + note = newNotes, + card = newCards + ) + ) + + } + + inner class LoginsAdapter (private val logins: MutableList) : RecyclerView.Adapter() { + + lateinit var star: Drawable + lateinit var warning: Drawable + lateinit var mfa: Drawable + lateinit var circle: Drawable + lateinit var emailIcon: Drawable + lateinit var loginIcon: Drawable + lateinit var website: Drawable + lateinit var clipboard: ClipboardManager + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : ViewHolder { // create new views + val loginCard: View = LayoutInflater.from(parent.context).inflate(R.layout.login, parent, false) + loginCard.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + + return ViewHolder(loginCard) + } + + override fun onBindViewHolder(loginCard: ViewHolder, position: Int) { // binds the list items to a view + bindData (loginCard) + } + + override fun getItemId (position: Int): Long { + return logins[position].id.hashCode().toLong() + } + + override fun getItemCount(): Int { // return the number of the items in the list + return logins.size + } + + inner class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) { // Holds the views for adding it to image and text + val siteIcon: ImageView = itemView.findViewById(R.id.SiteIcon) + val siteName: TextView = itemView.findViewById(R.id.SiteName) + + val tagText: TextView = itemView.findViewById(R.id.TagText) + + val usernameText: TextView = itemView.findViewById(R.id.usernameText) + + val mfaProgress: LinearProgressIndicator = itemView.findViewById(R.id.mfaProgress) + val mfaText: TextView = itemView.findViewById(R.id.mfaText) + + val miscText: TextView = itemView.findViewById(R.id.MiscText) + + val loginInformation: LinearLayout = itemView.findViewById(R.id.LoginInformation) + + init { + mfaProgress.visibility = View.INVISIBLE + mfaText.text = "••• •••" + + siteIcon.invalidate() + siteIcon.refreshDrawableState() + + mfaText.invalidate() + mfaText.refreshDrawableState() + + mfaProgress.invalidate() + mfaProgress.refreshDrawableState() + + siteIcon.setColorFilter(siteName.currentTextColor) + + star = ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_24)!! + warning = ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_warning_24)!! + mfa = ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_time_24)!! + website = ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_website_24)!! + circle = ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_circle_24)!! + loginIcon = ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_account_circle_24)!! + emailIcon = ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_email_24)!! + + clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + } + } + + private fun bindData (loginCard: ViewHolder) { + val login = logins[loginCard.adapterPosition] + + loginCard.siteName.text = login.name + + loginCard.tagText.visibility = View.GONE + if (tags.size > 0) { + for (tag in tags) { + if (login.tagId == tag.id) { + loginCard.tagText.visibility = View.VISIBLE + loginCard.tagText.text = tag.name + try { + if (!tag.color.isNullOrEmpty()) { + DrawableCompat.setTint (circle, Color.parseColor(tag.color)) + DrawableCompat.setTintMode (circle, PorterDuff.Mode.SRC_IN) + loginCard.tagText.setCompoundDrawablesWithIntrinsicBounds (null, null, circle, null) + } + } catch (_: StringIndexOutOfBoundsException) { } catch (_: IllegalArgumentException) { } + break + } + } + } + + if (!login.loginData?.email.isNullOrEmpty()) { + loginCard.usernameText.text = login.loginData!!.email + loginCard.usernameText.setCompoundDrawablesRelativeWithIntrinsicBounds (emailIcon, null, null, null) + } else if (!login.loginData?.username.isNullOrEmpty()) { + loginCard.usernameText.text = login.loginData!!.username + loginCard.usernameText.setCompoundDrawablesRelativeWithIntrinsicBounds (loginIcon, null, null, null) + } else loginCard.usernameText.visibility = View.GONE + + loginCard.miscText.visibility = View.GONE + + if (login.loginData?.password.isNullOrEmpty()) { + loginCard.miscText.visibility = View.VISIBLE + loginCard.miscText.setCompoundDrawablesRelativeWithIntrinsicBounds (null, null, warning, null) + loginCard.miscText.text = "No password" + } + + if (login.loginData?.password.isNullOrEmpty() && !login.loginData?.totp?.secret.isNullOrEmpty()) { + loginCard.miscText.setCompoundDrawablesRelativeWithIntrinsicBounds (null, null, mfa, null) + if (login.loginData?.email == login.name) loginCard.usernameText.visibility = View.GONE + loginCard.miscText.text = "2FA only" + } + + if (login.favorite) { + loginCard.miscText.visibility = View.VISIBLE + loginCard.miscText.setCompoundDrawablesRelativeWithIntrinsicBounds (null, null, star, null) + loginCard.miscText.text = "" + } + + if (!login.loginData?.password.isNullOrEmpty()) { + thread { + val passwordStrength = zxcvbn.measure(login.loginData?.password.toString()) + if (passwordStrength.score <= 2) { + runOnUiThread { + loginCard.miscText.visibility = View.VISIBLE + loginCard.miscText.setCompoundDrawablesRelativeWithIntrinsicBounds (null, null, warning, null) + loginCard.miscText.text = "Weak password" + } + } + } + } + + if (login.name!!.lowercase().contains("keyspace")) { // Easter egg + if (login.loginData?.password!!.split("-").size == 12 || login.loginData.password.split(" ").size == 12) { + loginCard.miscText.visibility = View.VISIBLE + loginCard.miscText.setCompoundDrawablesRelativeWithIntrinsicBounds (null, null, null, null) + loginCard.miscText.text = "You were supposed to write them down! " + } + login.customFields!!.forEach { field -> + if (field.value.split(" ").size == 12 || field.value.split("-").size == 12) { + loginCard.miscText.visibility = View.VISIBLE + loginCard.miscText.setCompoundDrawablesRelativeWithIntrinsicBounds (null, null, null, null) + loginCard.miscText.text = "You were supposed to write them down! " + } + } + } + + if (login.loginData?.totp?.secret.isNullOrEmpty()) { + loginCard.mfaText.visibility = View.GONE + loginCard.mfaProgress.visibility = View.GONE + } + + if (login.iconFile != null) loginCard.siteIcon.setImageDrawable(misc.getSiteIcon(login.iconFile, loginCard.siteName.currentTextColor)) + else loginCard.siteIcon.setImageDrawable(DrawableCompat.wrap(website)) + + loginCard.loginInformation.setOnClickListener { + MaterialAlertDialogBuilder(this@DeletedItems) + .setTitle("Deleted login") + .setCancelable(true) + .setMessage("What would you like to do with \"${loginCard.siteName.text}\"?") + .setNeutralButton("Go back"){ _, _ -> } + .setPositiveButton("Delete"){ _, _ -> + val newLogins = mutableListOf() + vault.login?.forEach { if (it.id != login.id) newLogins.add(it) } + network.writeQueueTask (login.id!!, mode = network.MODE_DELETE) + io.writeVault ( + IOUtilities.Vault( + version = vault.version, + tag = vault.tag, + login = newLogins, + note = vault.note, + card = vault.card + ) + ) + deletedLogins.remove(login) + loginsAdapter.notifyDataSetChanged() + deletedLoginsLabel.text = "Deleted logins" + " (" + deletedLogins.size + ")" + + if (deletedLogins.isEmpty()) { + deletedLoginsRecycler.visibility = View.GONE + deletedLoginsLabel.visibility = View.GONE + } + + if (deletedLogins.isEmpty() && deletedNotes.isEmpty() && deletedCards.isEmpty()) { + dangerZoneLayout.visibility = View.GONE + deletedItemsGraphic.visibility = View.VISIBLE + } + + } + .setNegativeButton("Restore") {_, _ -> + val newLogins = mutableListOf() + lateinit var loginToSync: IOUtilities.Login + vault.login?.forEach { + if (it.id == login.id) + if (it.deleted) { + it.deleted = false + loginToSync = it + } + newLogins.add(it) + } + network.writeQueueTask (loginToSync, mode = network.MODE_PUT) + io.writeVault ( + IOUtilities.Vault( + version = vault.version, + tag = vault.tag, + login = newLogins, + note = vault.note, + card = vault.card + ) + ) + deletedLogins.remove(login) + loginsAdapter.notifyDataSetChanged() + deletedLoginsLabel.text = "Deleted logins" + " (" + deletedLogins.size + ")" + + if (deletedLogins.isEmpty()) { + deletedLoginsRecycler.visibility = View.GONE + deletedLoginsLabel.visibility = View.GONE + } + + if (deletedLogins.isEmpty() && deletedNotes.isEmpty() && deletedCards.isEmpty()) { + dangerZoneLayout.visibility = View.GONE + deletedItemsGraphic.visibility = View.VISIBLE + } + + } + .show() + } + + } + + } + + inner class NotesAdapter (private val notes: MutableList) : RecyclerView.Adapter() { + + lateinit var markdownProcessor: MarkdownProcessor + lateinit var calendar: Calendar + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : ViewHolder { // create new views + val noteCard: View = LayoutInflater.from(parent.context).inflate(R.layout.note, parent, false) + noteCard.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + + return ViewHolder(noteCard) + } + + @SuppressLint("UseCompatLoadingForDrawables") + override fun onBindViewHolder(noteCard: ViewHolder, position: Int) { // binds the list items to a view + bindData(noteCard) + } + + override fun getItemCount(): Int { // return the number of the items in the list + return notes.size + } + + override fun getItemId (position: Int): Long { + return notes[position].id.hashCode().toLong() + } + + inner class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) { // Holds the views for adding it to image and text + val note: com.yydcdut.markdown.MarkdownTextView = itemView.findViewById(R.id.Note) + val noteCardLayout: LinearLayout = itemView.findViewById(R.id.NoteCardLayout) + val date: TextView = itemView.findViewById(R.id.Date) + val line: View = itemView.findViewById(R.id.line) + + val tagText: TextView = itemView.findViewById(R.id.TagText) + val miscText: TextView = itemView.findViewById(R.id.MiscText) + + init { + + note.refreshDrawableState() + note.invalidate() + + noteCardLayout.refreshDrawableState() + noteCardLayout.invalidate() + + val theme: 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() + } + + calendar = Calendar.getInstance(Locale.getDefault()) + + val markdownConfig = MarkdownConfiguration.Builder(applicationContext) + .setTheme(theme) + .showLinkUnderline(true) + .setOnLinkClickCallback { _, link -> + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link))) + } + .setLinkFontColor(note.currentTextColor) + .setOnTodoClickCallback(object : OnTodoClickCallback { + override fun onTodoClicked(view: View?, line: String?, lineNumber: Int): CharSequence { + return "" + } + }) + .setDefaultImageSize(480, 240) + .build() + + markdownProcessor = MarkdownProcessor(this@DeletedItems) + markdownProcessor.factory(TextFactory.create()) + markdownProcessor.config(markdownConfig) + } + + } + + private fun bindData (noteCard: ViewHolder) { + val note = notes[noteCard.adapterPosition] + + noteCard.noteCardLayout.setOnClickListener { + crypto.secureStartActivity ( + nextActivity = AddNote(), + nextActivityClassNameAsString = getString(R.string.title_activity_add_note), + keyring = keyring, + itemId = note.id + ) + } + + noteCard.note.text = note.notes + noteCard.noteCardLayout.setBackgroundColor(0) + + if (!note.notes.isNullOrEmpty()) { + thread { + val parsedText = markdownProcessor.parse(note.notes) + runOnUiThread { + noteCard.note.text = parsedText + } + } + } + + if (note.favorite) { + noteCard.miscText.setCompoundDrawablesRelativeWithIntrinsicBounds(null,null, ContextCompat.getDrawable(applicationContext, R.drawable.ic_baseline_star_24),null) + noteCard.miscText.text = null + } else noteCard.miscText.visibility = View.GONE + + calendar.timeInMillis = note.dateModified?.times(1000L)!! + val dateAndTime = DateFormat.format("MMM dd, yyyy ⋅ hh:mm a", calendar).toString() + noteCard.date.text = dateAndTime + + noteCard.note.setBackgroundColor(0x00000000) + if (!note.color.isNullOrEmpty()) { + val noteColor = note.color + noteCard.noteCardLayout.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 + if (g >= 200 || b >= 200) { + noteCard.note.setTextColor (Color.BLACK) + noteCard.date .setTextColor (Color.BLACK) + noteCard.miscText.setTextColor (Color.BLACK) + noteCard.tagText.setTextColor (Color.BLACK) + noteCard.line.setBackgroundColor (Color.BLACK) + + val starIcon = DrawableCompat.wrap(getDrawable(R.drawable.ic_baseline_star_24)!!) + DrawableCompat.setTint(starIcon, Color.BLACK) + DrawableCompat.setTintMode(starIcon, PorterDuff.Mode.MULTIPLY) + noteCard.miscText.setCompoundDrawablesWithIntrinsicBounds(starIcon, null, null, null) + + val tagIcon = DrawableCompat.wrap(getDrawable(R.drawable.ic_baseline_circle_24)!!) + DrawableCompat.setTint(tagIcon, Color.BLACK) + DrawableCompat.setTintMode(tagIcon, PorterDuff.Mode.SRC_IN) + noteCard.tagText.setCompoundDrawablesWithIntrinsicBounds(null, null, tagIcon, null) + + } else { + noteCard.note.setTextColor(Color.WHITE) + noteCard.note.setTextColor(Color.WHITE) + noteCard.date.setTextColor(Color.WHITE) + noteCard.miscText.setTextColor(Color.WHITE) + noteCard.tagText.setTextColor(Color.WHITE) + noteCard.line.setBackgroundColor (Color.WHITE) + + val starIcon = DrawableCompat.wrap(getDrawable(R.drawable.ic_baseline_star_24)!!) + DrawableCompat.setTint(starIcon, Color.WHITE) + DrawableCompat.setTintMode(starIcon, PorterDuff.Mode.MULTIPLY) + noteCard.miscText.setCompoundDrawablesWithIntrinsicBounds(starIcon, null, null, null) + + val tagIcon = DrawableCompat.wrap(getDrawable(R.drawable.ic_baseline_circle_24)!!) + DrawableCompat.setTint(tagIcon, Color.WHITE) + DrawableCompat.setTintMode(tagIcon, PorterDuff.Mode.SRC_IN) + noteCard.tagText.setCompoundDrawablesWithIntrinsicBounds(null, null, tagIcon, null) + } + } + + noteCard.tagText.visibility = View.GONE + if (tags.size > 0) { + for (tag in tags) { + if (note.tagId == tag.id) { + noteCard.tagText.visibility = View.VISIBLE + noteCard.tagText.text = tag.name + try { + if (!tag.color.isNullOrEmpty()) { + val tagIcon = DrawableCompat.wrap(getDrawable(R.drawable.ic_baseline_circle_24)!!) + DrawableCompat.setTint(tagIcon, Color.parseColor(tag.color)) + DrawableCompat.setTintMode(tagIcon, PorterDuff.Mode.SRC_IN) + noteCard.tagText.setCompoundDrawablesWithIntrinsicBounds(null, null, tagIcon, null) + } + } catch (_: StringIndexOutOfBoundsException) { } catch (_: IllegalArgumentException) { } + break + } + } + } + + noteCard.noteCardLayout.setOnClickListener { + MaterialAlertDialogBuilder(this@DeletedItems) + .setTitle("Deleted note") + .setCancelable(true) + .setMessage("What would you like to do with this note?") + .setNeutralButton("Go back"){ _, _ -> } + .setPositiveButton("Delete"){ _, _ -> + val newNotes = mutableListOf() + vault.note?.forEach { if (it.id != note.id) newNotes.add(it) } + network.writeQueueTask (note.id!!, mode = network.MODE_DELETE) + io.writeVault ( + IOUtilities.Vault( + version = vault.version, + tag = vault.tag, + login = vault.login, + note = newNotes, + card = vault.card + ) + ) + deletedNotes.remove(note) + notesAdapter.notifyDataSetChanged() + deletedNotesLabel.text = "Deleted notes" + " (" + deletedNotes.size + ")" + + if (deletedNotes.isEmpty()) { + deletedNotesRecycler.visibility = View.GONE + deletedNotesLabel.visibility = View.GONE + } + + if (deletedLogins.isEmpty() && deletedNotes.isEmpty() && deletedCards.isEmpty()) { + dangerZoneLayout.visibility = View.GONE + deletedItemsGraphic.visibility = View.VISIBLE + } + + } + .setNegativeButton("Restore") {_, _ -> + val newNotes = mutableListOf() + lateinit var noteToSync: IOUtilities.Note + vault.note?.forEach { + if (it.id == note.id) + if (it.deleted) { + it.deleted = false + noteToSync = it + } + newNotes.add(it) + } + network.writeQueueTask (noteToSync, mode = network.MODE_PUT) + io.writeVault ( + IOUtilities.Vault( + version = vault.version, + tag = vault.tag, + login = vault.login, + note = newNotes, + card = vault.card + ) + ) + deletedNotes.remove(note) + notesAdapter.notifyDataSetChanged() + deletedNotesLabel.text = "Deleted notes" + " (" + deletedNotes.size + ")" + + if (deletedNotes.isEmpty()) { + deletedNotesRecycler.visibility = View.GONE + deletedNotesLabel.visibility = View.GONE + } + + if (deletedLogins.isEmpty() && deletedNotes.isEmpty() && deletedCards.isEmpty()) { + dangerZoneLayout.visibility = View.GONE + deletedItemsGraphic.visibility = View.VISIBLE + } + + } + .show() + } + + } + } + + inner class CardsAdapter (private val cards: MutableList) : RecyclerView.Adapter() { + + lateinit var setRightOut: AnimatorSet + lateinit var setLeftIn: AnimatorSet + lateinit var animatedContactless: AnimatedVectorDrawable + + lateinit var clipboard: ClipboardManager + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : ViewHolder { // create new views + val cardCard: View = LayoutInflater.from(parent.context).inflate(R.layout.card, parent, false) + cardCard.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + return ViewHolder(cardCard) + } + + @SuppressLint("ResourceType") + override fun onBindViewHolder(cardCard: ViewHolder, position: Int) { // binds the list items to a view + bindData (cardCard) + } + + override fun getItemId (position: Int): Long { + return cards[position].id.hashCode().toLong() + } + + override fun getItemCount(): Int { // return the number of the items in the list + return cards.size + } + + @SuppressLint("ResourceType", "ClickableViewAccessibility") + inner class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) { // Holds the views for adding it to image and text + + val cardsCardFrontLayout: ConstraintLayout = itemView.findViewById(R.id.CardsCardFrontLayout) + val cardsCardLayout: ConstraintLayout = itemView.findViewById(R.id.CardsCardLayout) + + val bankNameFront: TextView = itemView.findViewById(R.id.bankNameFront) + val bankLogoFront: ImageView = itemView.findViewById(R.id.bankLogoFront) + val rfidIcon: ImageView = itemView.findViewById(R.id.RfidIcon) + val cardNumber: TextView = itemView.findViewById(R.id.CardNumber) + val toDate: TextView = itemView.findViewById(R.id.toDate) + val toLabel: TextView = itemView.findViewById(R.id.toLabel) + val cardHolder: TextView = itemView.findViewById(R.id.CardHolder) + val paymentGateway: ImageView = itemView.findViewById(R.id.PaymentGateway) + + init { + + rfidIcon.invalidate() + rfidIcon.refreshDrawableState() + + setRightOut = AnimatorInflater.loadAnimator(applicationContext, R.anim.flip2) as AnimatorSet + setLeftIn = AnimatorInflater.loadAnimator(applicationContext, R.anim.flip1) as AnimatorSet + + clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + + animatedContactless = getDrawable(R.drawable.lessdistractingflickercontactless)?.mutate() as AnimatedVectorDrawable + + } + + } + + @SuppressLint("ClickableViewAccessibility", "ResourceType", "UseCompatLoadingForDrawables") + fun bindData (cardCard: ViewHolder) { + val card = cards[cardCard.adapterPosition] + + if (card.cardNumber?.length == 16) cardCard.cardNumber.text = card.cardNumber.replace("....".toRegex(), "$0 ") + else cardCard.cardNumber.text = card.cardNumber + + cardCard.toDate.text = card.expiry + cardCard.cardHolder.text = card.cardholderName + + cardCard.bankNameFront.text = card.name + cardCard.bankLogoFront.visibility = View.GONE + + + val cardColor = card.color + if (!card.color.isNullOrEmpty()) cardCard.cardsCardFrontLayout.backgroundTintList = ColorStateList.valueOf(Color.parseColor(cardColor)) + else cardCard.cardsCardFrontLayout.backgroundTintList = ColorStateList.valueOf(Color.DKGRAY) + + val intColor: Int = try { cardCard.cardsCardFrontLayout.backgroundTintList?.defaultColor!! } catch (_: NullPointerException) { 0 } + + 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) { + cardCard.rfidIcon.setColorFilter(Color.BLACK) + cardCard.bankNameFront.setTextColor (Color.BLACK) + cardCard.cardHolder.setTextColor (Color.BLACK) + cardCard.toDate.setTextColor (Color.BLACK) + cardCard.toLabel.setTextColor (Color.BLACK) + cardCard.cardNumber.setTextColor (Color.BLACK) + cardCard.cardNumber.setTextColor (Color.BLACK) + cardCard.bankLogoFront.setColorFilter(Color.BLACK) + cardCard.paymentGateway.setColorFilter(Color.BLACK) + } else { + cardCard.rfidIcon.setColorFilter(Color.WHITE) + cardCard.bankNameFront.setTextColor (Color.WHITE) + cardCard.cardHolder .setTextColor (Color.WHITE) + cardCard.toDate .setTextColor (Color.WHITE) + cardCard.toLabel.setTextColor (Color.WHITE) + cardCard.cardNumber.setTextColor (Color.WHITE) + cardCard.cardNumber.setTextColor (Color.WHITE) + cardCard.rfidIcon.foregroundTintList = ColorStateList.valueOf(Color.WHITE) + cardCard.bankLogoFront.setColorFilter(Color.WHITE) + cardCard.paymentGateway.setColorFilter(Color.WHITE) + } + + val paymentGateway = misc.getPaymentGateway(card.cardNumber.toString()) + var bankLogo = if (card.iconFile != null) misc.getSiteIcon(card.iconFile, cardCard.cardNumber.currentTextColor) else null + + var gatewayLogo = if (paymentGateway != null) misc.getSiteIcon(paymentGateway, cardCard.cardNumber.currentTextColor) else null + + thread { + if (bankLogo != null && card.iconFile != "bank") { + runOnUiThread { + cardCard.bankLogoFront.visibility = View.VISIBLE + cardCard.bankLogoFront.setImageDrawable(DrawableCompat.wrap(bankLogo)) + } + } else cardCard.bankLogoFront.visibility = View.GONE + + if (paymentGateway != null) { + if (gatewayLogo != null) runOnUiThread { cardCard.paymentGateway.setImageDrawable(gatewayLogo) } + else cardCard.paymentGateway.visibility = View.GONE + } else cardCard.paymentGateway.visibility = View.GONE + + } + + cardCard.rfidIcon.visibility = View.GONE + if (card.rfid == true) { + cardCard.rfidIcon.visibility = View.VISIBLE + cardCard.rfidIcon.setImageDrawable(animatedContactless) + } + + cardCard.cardsCardLayout.setOnClickListener { + MaterialAlertDialogBuilder(this@DeletedItems) + .setTitle("Deleted card") + .setCancelable(true) + .setMessage("What would you like to do with \"${cardCard.bankNameFront.text}\"?") + .setNeutralButton("Go back"){ _, _ -> } + .setPositiveButton("Delete"){ _, _ -> + val newCards = mutableListOf() + vault.card?.forEach { if (it.id != card.id) newCards.add(it) } + network.writeQueueTask (card.id!!, mode = network.MODE_DELETE) + io.writeVault ( + IOUtilities.Vault( + version = vault.version, + tag = vault.tag, + login = vault.login, + note = vault.note, + card = newCards + ) + ) + deletedCards.remove(card) + cardsAdapter.notifyDataSetChanged() + deletedCardsLabel.text = "Deleted cards" + " (" + deletedCards.size + ")" + + if (deletedCards.isEmpty()) { + deletedCardsRecycler.visibility = View.GONE + deletedCardsLabel.visibility = View.GONE + } + + if (deletedLogins.isEmpty() && deletedNotes.isEmpty() && deletedCards.isEmpty()) { + dangerZoneLayout.visibility = View.GONE + deletedItemsGraphic.visibility = View.VISIBLE + } + + } + .setNegativeButton("Restore") {_, _ -> + val newCards = mutableListOf() + lateinit var cardToSync: IOUtilities.Card + vault.card?.forEach { + if (it.id == card.id) + if (it.deleted) { + it.deleted = false + cardToSync = it + } + newCards.add(it) + } + network.writeQueueTask (cardToSync, mode = network.MODE_PUT) + io.writeVault ( + IOUtilities.Vault( + version = vault.version, + tag = vault.tag, + login = vault.login, + note = vault.note, + card = newCards + ) + ) + deletedCards.remove(card) + cardsAdapter.notifyDataSetChanged() + deletedCardsLabel.text = "Deleted cards" + " (" + deletedCards.size + ")" + + if (deletedCards.isEmpty()) { + deletedCardsRecycler.visibility = View.GONE + deletedCardsLabel.visibility = View.GONE + } + + if (deletedLogins.isEmpty() && deletedNotes.isEmpty() && deletedCards.isEmpty()) { + dangerZoneLayout.visibility = View.GONE + deletedItemsGraphic.visibility = View.VISIBLE + } + + } + .show() + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/cloud/keyspace/android/DeveloperOptions.kt b/app/src/main/kotlin/cloud/keyspace/android/DeveloperOptions.kt index e01f8e9..511fa72 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/DeveloperOptions.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/DeveloperOptions.kt @@ -2,10 +2,7 @@ package cloud.keyspace.android import android.os.Bundle import android.util.Log -import android.widget.EditText -import android.widget.RadioGroup -import android.widget.TextView -import android.widget.Toast +import android.widget.* import androidx.appcompat.app.AppCompatActivity import com.google.android.material.button.MaterialButton import com.goterl.lazysodium.LazySodiumAndroid @@ -42,6 +39,9 @@ class DeveloperOptions : AppCompatActivity() { val blake2bWordsInput = findViewById(R.id.blake2bWordsInput) val blake2bHashButton = findViewById(R.id.blake2bHashButton) + val backButton: ImageView = findViewById(R.id.backButton) + backButton.setOnClickListener { onBackPressed() } + blake2bHashButton.setOnClickListener { val bip39Seed: ByteArray? = crypto.wordsToSeed(blake2bWordsInput.text.toString().toCharArray(), blake2bPassphraseInput.text.toString().toCharArray()) diff --git a/app/src/main/kotlin/cloud/keyspace/android/IOUtilities.kt b/app/src/main/kotlin/cloud/keyspace/android/IOUtilities.kt index 9749243..abc56ae 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/IOUtilities.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/IOUtilities.kt @@ -41,6 +41,7 @@ class IOUtilities( val file = File(applicationContext.cacheDir, filename!!) + @JsonIgnoreProperties(ignoreUnknown = true) data class Tag ( val id: String, val name: String, @@ -58,22 +59,26 @@ class IOUtilities( val card: MutableList?, ) + @JsonIgnoreProperties(ignoreUnknown = true) data class CustomField ( var name: String?, var value: String, val hidden: Boolean ) + @JsonIgnoreProperties(ignoreUnknown = true) data class Password ( val password: String, val created: Long ) + @JsonIgnoreProperties(ignoreUnknown = true) data class Totp ( val secret: String?, val backupCodes: MutableList? ) + @JsonIgnoreProperties(ignoreUnknown = true) data class LoginData ( val username: String?, val password: String?, @@ -84,6 +89,7 @@ class IOUtilities( val siteUrls: MutableList? ) + @JsonIgnoreProperties(ignoreUnknown = true) data class Login ( val id: String?, val organizationId: String?, @@ -91,6 +97,7 @@ class IOUtilities( val name: String?, val notes: String?, val favorite: Boolean, + var deleted: Boolean, val tagId: String?, val loginData: LoginData?, val dateCreated: Long?, @@ -100,6 +107,7 @@ class IOUtilities( val customFields: MutableList? ) + @JsonIgnoreProperties(ignoreUnknown = true) data class Note ( val id: String?, val organizationId: String?, @@ -110,9 +118,11 @@ class IOUtilities( val notes: String?, val color: String?, val favorite: Boolean, + var deleted: Boolean, val tagId: String?, ) + @JsonIgnoreProperties(ignoreUnknown = true) data class Card ( val id: String?, val organizationId: String?, @@ -122,6 +132,7 @@ class IOUtilities( val expiry: String?, val securityCode: String?, val favorite: Boolean, + var deleted: Boolean, val cardholderName: String?, val name: String?, val pin: String?, @@ -336,6 +347,7 @@ class IOUtilities( name = try { crypto.kfsDecrypt (login.name, keyring.XCHACHA_POLY1305_KEY!!)} catch (_: Exception) { null } as String?, notes = try { crypto.kfsDecrypt (login.notes, keyring.XCHACHA_POLY1305_KEY!!)} catch (_: Exception) { null } as String?, favorite = login.favorite, + deleted= login.deleted, tagId = login.tagId, loginData = LoginData ( username = try { crypto.kfsDecrypt (login.loginData?.username, keyring.XCHACHA_POLY1305_KEY!!)} catch (_: Exception) { null } as String?, @@ -985,6 +997,7 @@ class IOUtilities( notes = try { crypto.kfsDecrypt (note.notes, keyring.XCHACHA_POLY1305_KEY!!)} catch (_: Exception) {null } as String?, color = try { crypto.kfsDecrypt (note.color, keyring.XCHACHA_POLY1305_KEY!!)} catch (_: Exception) {null } as String?, favorite = note.favorite, + deleted= note.deleted, tagId = note.tagId, dateCreated = note.dateCreated, dateModified = note.dateModified, @@ -1034,6 +1047,7 @@ class IOUtilities( pin = try {crypto.kfsDecrypt (card.pin.toString(), keyring.XCHACHA_POLY1305_KEY!!)} catch (_: Exception) { null} as String?, securityCode = try {crypto.kfsDecrypt (card.securityCode.toString(), keyring.XCHACHA_POLY1305_KEY!!)} catch (_: Exception) { null} as String?, favorite = card.favorite, + deleted= card.deleted, tagId = card.tagId, dateCreated = card.dateCreated, dateModified = card.dateModified, @@ -1168,6 +1182,7 @@ class IOUtilities( name = try { crypto.kfsEncrypt (login.name, keyring.XCHACHA_POLY1305_KEY!!) } catch (_: Exception) { null }, notes = try { crypto.kfsEncrypt (login.notes, keyring.XCHACHA_POLY1305_KEY!!) } catch (_: Exception) { null }, favorite = login.favorite, + deleted= login.deleted, tagId = login.tagId, loginData = LoginData ( username = try { crypto.kfsEncrypt (login.loginData?.username, keyring.XCHACHA_POLY1305_KEY!!) } catch (_: Exception) { null }, @@ -1197,6 +1212,7 @@ class IOUtilities( notes = try { crypto.kfsEncrypt (note.notes, keyring.XCHACHA_POLY1305_KEY!!) } catch (_: Exception) { null }, color = try { crypto.kfsEncrypt (note.color, keyring.XCHACHA_POLY1305_KEY!!) } catch (_: Exception) { null }, favorite = note.favorite, + deleted= note.deleted, tagId = note.tagId, dateCreated = note.dateCreated, dateModified = note.dateModified, @@ -1233,6 +1249,7 @@ class IOUtilities( pin = try { crypto.kfsEncrypt (card.pin.toString(), keyring.XCHACHA_POLY1305_KEY!!) } catch (_: Exception) { null }, securityCode = try { crypto.kfsEncrypt (card.securityCode.toString(), keyring.XCHACHA_POLY1305_KEY!!) } catch (_: Exception) { null }, favorite = card.favorite, + deleted= card.deleted, tagId = card.tagId, dateCreated = card.dateCreated, dateModified = card.dateModified, diff --git a/app/src/main/kotlin/cloud/keyspace/android/Keyspace.kt b/app/src/main/kotlin/cloud/keyspace/android/Keyspace.kt new file mode 100644 index 0000000..c4d7fdd --- /dev/null +++ b/app/src/main/kotlin/cloud/keyspace/android/Keyspace.kt @@ -0,0 +1,39 @@ +package cloud.keyspace.android + +import android.app.Application +import android.content.Context +import org.acra.config.dialog +import org.acra.config.mailSender +import org.acra.data.StringFormat +import org.acra.ktx.initAcra +import java.time.Instant +import java.time.format.DateTimeFormatter + +class Keyspace : Application() { + override fun attachBaseContext(base: Context) { + super.attachBaseContext(base) + initAcra { + buildConfigClass = BuildConfig::class.java + reportFormat = StringFormat.KEY_VALUE_LIST + + dialog { + text = getString(R.string.crash_send_logs_description) + title = " " + getString(R.string.app_name) + positiveButtonText = getString(R.string.crash_send_positive_button_text) + negativeButtonText = getString(R.string.exit) + resIcon = R.drawable.keyspace + resTheme = android.R.style.Theme_Material_Dialog + } + + mailSender { + mailTo = getString(R.string.support_email) + reportAsFile = true + reportFileName = "android_bug_report_${DateTimeFormatter.ISO_INSTANT.format(Instant.now())}.txt" + subject = getString(R.string.crash_send_logs_email_subject) + body = getString(R.string.crash_send_logs_email_body) + } + + } + + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cloud/keyspace/android/MiscUtilities.kt b/app/src/main/kotlin/cloud/keyspace/android/MiscUtilities.kt index 64647b9..931e636 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/MiscUtilities.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/MiscUtilities.kt @@ -36,32 +36,39 @@ class MiscUtilities (applicationContext: Context) { fun getPaymentGateway(cardNumber: String): String? { try { if (cardNumber.startsWith("4") || - cardNumber.startsWith("4026") || - cardNumber.startsWith("411750") || - cardNumber.startsWith("4508") || - cardNumber.startsWith("4913") || - cardNumber.startsWith("4917") || - cardNumber.startsWith("4844")) { + cardNumber.startsWith("40") || + cardNumber.startsWith("411") || + cardNumber.startsWith("45") || + cardNumber.startsWith("49") || + cardNumber.startsWith("49") || + cardNumber.startsWith("48") + ) { return "visa" } else if ( (cardNumber.take(4)).toInt() in 2221..2720 || (cardNumber.take(2)).toInt() in 51..55 || - (cardNumber.take(4)).toInt() in 5100..5399) { + (cardNumber.take(4)).toInt() in 5100..5399 || + cardNumber.startsWith("67") + ) { return "mastercard" } else if ( (cardNumber.take(4)).toInt() in 622126..622925 || (cardNumber.take(3)).toInt() in 644..649 || - cardNumber.startsWith("66")) { + cardNumber.startsWith("66") || + cardNumber.startsWith("601") + ) { return "discover" } else if ( cardNumber.startsWith("60") || cardNumber.startsWith("6521") || cardNumber.startsWith("6522") || - cardNumber.startsWith("50")) { + cardNumber.startsWith("50") + ) { return "rupay" } else if ( cardNumber.startsWith("34") || - cardNumber.startsWith("37")) { + cardNumber.startsWith("37") + ) { return "americanExpress" } else { return null diff --git a/app/src/main/kotlin/cloud/keyspace/android/Settings.kt b/app/src/main/kotlin/cloud/keyspace/android/Settings.kt index edc97fd..3f192a3 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/Settings.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/Settings.kt @@ -27,6 +27,9 @@ class Settings : AppCompatActivity() { val configData = getSharedPreferences (applicationContext.packageName + "_configuration_data", MODE_PRIVATE) + val backButton: ImageView = findViewById(R.id.backButton) + backButton.setOnClickListener { onBackPressed() } + val keyspaceAccountPicture: ImageView = findViewById(R.id.keyspaceAccountPicture) keyspaceAccountPicture.setImageDrawable(MiscUtilities(applicationContext).generateProfilePicture(configData.getString("userEmail", null)!!)) @@ -59,14 +62,14 @@ class Settings : AppCompatActivity() { val strongBoxIcon: ImageView = findViewById(R.id.strongBoxTypeIcon) if (applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)) { // Check if strongbox / hardware keystore exists - strongBoxText.text = "Keyspace is encrypting your keys and tokens using tamper-resistant Strongbox hardware on your phone." + strongBoxText.text = "Keys are encrypted using tamper-resistant Strongbox hardware." strongBoxIcon.setImageDrawable(getDrawable(R.drawable.ic_baseline_chip_24)) } else { if (applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_HARDWARE_KEYSTORE)) { - strongBoxText.text = "Your phone doesn't contain Strongbox hardware. Using Hardware Abstraction Layer (HAL) based Keystore." + strongBoxText.text = "Keys are encrypted using Hardware Abstraction Layer (HAL) Keystore." strongBoxIcon.setImageDrawable(getDrawable(R.drawable.ic_baseline_code_24)) } else { - strongBoxText.text = "Your phone doesn't contain strongbox hardware. Using container-based Keystore." + strongBoxText.text = "Keys are encrypted using container-based Keystore." strongBoxIcon.setImageDrawable(getDrawable(R.drawable.ic_baseline_insert_drive_file_24)) } } diff --git a/app/src/main/kotlin/cloud/keyspace/android/StartHere.kt b/app/src/main/kotlin/cloud/keyspace/android/StartHere.kt index f3892f1..8e19bff 100644 --- a/app/src/main/kotlin/cloud/keyspace/android/StartHere.kt +++ b/app/src/main/kotlin/cloud/keyspace/android/StartHere.kt @@ -61,8 +61,6 @@ import kotlinx.coroutines.* import java.io.File import java.util.* import java.util.concurrent.Executor -import kotlin.system.exitProcess - private lateinit var _supportFragmentManager: FragmentManager @@ -375,15 +373,10 @@ class StartHere : AppCompatActivity() { loadingScreenFragmentView = inflater.inflate(R.layout.loading_screen, container, false) loadContent() - try { - generateCryptoObjects() - // logger() - if (mode == MODE_CREATE_ACCOUNT) createAccount() - else if (mode == MODE_SIGN_IN) signIn() + generateCryptoObjects() - } catch (unknownError: Exception) { - showCryptographyErrorDialog() - } + if (mode == MODE_CREATE_ACCOUNT) createAccount() + else if (mode == MODE_SIGN_IN) signIn() return loadingScreenFragmentView @@ -439,49 +432,39 @@ class StartHere : AppCompatActivity() { delay (500) setKeygen() - try { - generateCryptoObjects() - - keygenToSend() - delay(500) - - CoroutineScope(Dispatchers.IO).launch { - kotlin.runCatching { - val vaultData = network.grabLatestVaultFromBackend (signedToken) - withContext(Dispatchers.Main) { // used to run synchronous Kotlin functions like `suspend fun foo()` - sendToReceive() - - io.writeVault(vaultData) - - delay(500) - receiveToKeystore() - storeToKeyring() - delay (1000) - - keystoreToTick() - delay (3000) - - startPermissions() - } - }.onFailure { - when (it) { - is NetworkUtilities.IncorrectCredentialsException -> { - withContext(Dispatchers.Main) { - showIncorrectCredentialsDialog() - } + generateCryptoObjects() + keygenToSend() + delay(500) + + CoroutineScope(Dispatchers.IO).launch { + kotlin.runCatching { + val vaultData = network.grabLatestVaultFromBackend (signedToken) + withContext(Dispatchers.Main) { // used to run synchronous Kotlin functions like `suspend fun foo()` + sendToReceive() + io.writeVault(vaultData) + delay(500) + receiveToKeystore() + storeToKeyring() + delay (1000) + keystoreToTick() + delay (3000) + startPermissions() + } + }.onFailure { + when (it) { + is NetworkUtilities.IncorrectCredentialsException -> { + withContext(Dispatchers.Main) { + showIncorrectCredentialsDialog() } - is NetworkError -> { - withContext(Dispatchers.Main) { - showNetworkErrorDialog() - } + } + is NetworkError -> { + withContext(Dispatchers.Main) { + showNetworkErrorDialog() } - else -> throw it } + else -> throw it } } - - } catch (unknownError: Exception) { - showCryptographyErrorDialog () } } @@ -499,54 +482,44 @@ class StartHere : AppCompatActivity() { delay (500) setKeygen() - try { - generateCryptoObjects() + generateCryptoObjects() + keygenToSend() - keygenToSend() - CoroutineScope(Dispatchers.IO).launch { - kotlin.runCatching { - val createAccountResponse: NetworkUtilities.SignupResponse = network.sendSignupRequest( - NetworkUtilities.SignupParameters( - email = email, - public_key = publicKey.toHexString(), - signed_token = signedToken - ) - )!! - - if (createAccountResponse.status != network.RESPONSE_SUCCESS) throw NetworkUtilities.AccountExistsException() else { - withContext(Dispatchers.Main) { // used to run synchronous Kotlin functions like `suspend fun foo()` - delay(1000) - - receiveToKeystore() - storeToKeyring() - delay (1000) - - keystoreToTick() - delay (3000) - - startPermissions() - } + CoroutineScope(Dispatchers.IO).launch { + kotlin.runCatching { + val createAccountResponse: NetworkUtilities.SignupResponse = network.sendSignupRequest( + NetworkUtilities.SignupParameters( + email = email, + public_key = publicKey.toHexString(), + signed_token = signedToken + ) + )!! + if (createAccountResponse.status != network.RESPONSE_SUCCESS) throw NetworkUtilities.AccountExistsException() else { + withContext(Dispatchers.Main) { // used to run synchronous Kotlin functions like `suspend fun foo()` + delay(1000) + receiveToKeystore() + storeToKeyring() + delay (1000) + keystoreToTick() + delay (3000) + startPermissions() } - - }.onFailure { - when (it) { - is NetworkUtilities.AccountExistsException -> { - withContext(Dispatchers.Main) { - showDuplicateAccountDialog() - } + } + }.onFailure { + when (it) { + is NetworkUtilities.AccountExistsException -> { + withContext(Dispatchers.Main) { + showDuplicateAccountDialog() } - is NetworkError -> { - withContext(Dispatchers.Main) { - showNetworkErrorDialog() - } + } + is NetworkError -> { + withContext(Dispatchers.Main) { + showNetworkErrorDialog() } - else -> throw it } + else -> throw it } } - - } catch (unknownError: Exception) { - showCryptographyErrorDialog () } } diff --git a/app/src/main/res/drawable/illustration_deleted_items.xml b/app/src/main/res/drawable/illustration_deleted_items.xml new file mode 100644 index 0000000..2533309 --- /dev/null +++ b/app/src/main/res/drawable/illustration_deleted_items.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/card.xml b/app/src/main/res/layout/card.xml index 1abb09d..4e6037d 100644 --- a/app/src/main/res/layout/card.xml +++ b/app/src/main/res/layout/card.xml @@ -10,14 +10,14 @@ + android:maxHeight="285dp" + android:minWidth="435dp" + android:minHeight="285dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/developer_options.xml b/app/src/main/res/layout/developer_options.xml index 5a9e2b9..82c5328 100644 --- a/app/src/main/res/layout/developer_options.xml +++ b/app/src/main/res/layout/developer_options.xml @@ -20,7 +20,15 @@ android:layout_height="match_parent" android:paddingBottom="50dp" android:orientation="vertical" - android:paddingTop="105dp"> + android:paddingTop="15dp"> + + + android:textSize="35sp" /> + + @@ -143,15 +162,13 @@ android:orientation="vertical"> + android:layout_height="wrap_content" + android:paddingVertical="3.5dp" + android:textSize="20sp" + android:paddingHorizontal="5dp" + android:text="Test Argon2i" /> @@ -280,15 +298,13 @@ android:orientation="vertical"> + android:layout_height="wrap_content" + android:paddingVertical="3.5dp" + android:textSize="20sp" + android:paddingHorizontal="5dp" + android:text="Test BIP39" /> diff --git a/app/src/main/res/layout/edit_card.xml b/app/src/main/res/layout/edit_card.xml index aa57895..c0d99ad 100644 --- a/app/src/main/res/layout/edit_card.xml +++ b/app/src/main/res/layout/edit_card.xml @@ -211,7 +211,7 @@ android:digits="0123456789 " android:inputType="phone" android:letterSpacing="0.05" - android:maxLength="19" /> + android:maxLength="22" /> diff --git a/app/src/main/res/layout/edit_login.xml b/app/src/main/res/layout/edit_login.xml index a0f2ae8..ec53fd3 100644 --- a/app/src/main/res/layout/edit_login.xml +++ b/app/src/main/res/layout/edit_login.xml @@ -216,15 +216,6 @@ android:inputType="textFilter" /> - - - + + + android:layout_height="wrap_content"> - - - + - + + - - + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - \ No newline at end of file + android:layout_weight="1" + android:drawablePadding="5dp" + android:ellipsize="marquee" + android:fadingEdge="horizontal" + android:gravity="center|end" + android:marqueeRepeatLimit="marquee_forever" + android:scrollbars="none" + android:scrollHorizontally="true" + android:singleLine="true" + android:text="Tag" + android:textSize="14sp" + app:drawableEndCompat="@drawable/ic_baseline_circle_24" /> + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings.xml b/app/src/main/res/layout/settings.xml index 9ed4cd6..00f3faf 100644 --- a/app/src/main/res/layout/settings.xml +++ b/app/src/main/res/layout/settings.xml @@ -20,12 +20,21 @@ android:layout_height="match_parent" android:paddingBottom="50dp" android:orientation="vertical" - android:paddingTop="55dp"> + android:paddingTop="15dp" > + + + android:textSize="35sp" />