diff options
author | Aditya Wasan <adityawasan55@gmail.com> | 2021-02-19 13:57:57 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-19 08:27:57 +0000 |
commit | d17ff0d9251bd2ad84778535dabe4884a4cf9d76 (patch) | |
tree | 57c2cfd031632e8687c038bd35ff2b39f61f6d77 | |
parent | 92ece7dbb5607258bcf954963009bf1f9411ab07 (diff) |
Parse extra content into key value pairs (#1321)
* ui: add skeleton recyclerview to parse extra content
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* ui: add recyclerview and update PasswordEntry to create map of key-value pairs
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* password-entry: When key-value pair is not correctly formed, display it as Extra Content
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* Fix formatting
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* bug: update otp code on main thread
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* Add complete string if key-value pair cannot be formed
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* test: add a few tests for key-value parsing logic
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* prefs: remove SHOW_EXTRA_CONTENT from shared preferences
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* Update CHANGELOG.md
* Cleanup and refactor
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* PasswordEntryTest: silence nullability warning
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* PasswordEntry: simplify constructor
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* PasswordEntry: annotate test-enablement visibility
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* Reintroduce the catch-all field
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* update parsing logic
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* add one more test case
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* Add missing newlines
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* Remove unnecessary scrollview
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* rv: do not return if hasExtraContent is false
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* Don't anchor RV to bottom
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
11 files changed, 354 insertions, 236 deletions
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index f831e10b..c21a1678 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -36,6 +36,9 @@ <option name="IF_RPAREN_ON_NEW_LINE" value="false" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> </JetCodeStyleSettings> + <editorconfig> + <option name="ENABLED" value="false" /> + </editorconfig> <codeStyleSettings language="JAVA"> <option name="METHOD_ANNOTATION_WRAP" value="0" /> <option name="FIELD_ANNOTATION_WRAP" value="0" /> diff --git a/CHANGELOG.md b/CHANGELOG.md index cb811fc8..4d497f1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. - Suggest users to re-clone repository when it is deemed to be broken - Allow doing a merge instead of a rebase when pulling or syncing - Add support for manually providing TOTP parameters +- Parse extra content as individual fields ### Fixed diff --git a/app/src/main/java/dev/msfjarvis/aps/data/password/FieldItem.kt b/app/src/main/java/dev/msfjarvis/aps/data/password/FieldItem.kt new file mode 100644 index 00000000..b77ab5e3 --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/data/password/FieldItem.kt @@ -0,0 +1,27 @@ +package dev.msfjarvis.aps.data.password + +class FieldItem(val key: String, val value: String, val action: ActionType) { + enum class ActionType { + COPY, HIDE + } + + enum class ItemType(val type: String) { + USERNAME("Username"), PASSWORD("Password"), OTP("OTP") + } + + companion object { + + // Extra helper methods + fun createOtpField(otp: String): FieldItem { + return FieldItem(ItemType.OTP.type, otp, ActionType.COPY) + } + + fun createPasswordField(password: String): FieldItem { + return FieldItem(ItemType.PASSWORD.type, password, ActionType.HIDE) + } + + fun createUsernameField(username: String): FieldItem { + return FieldItem(ItemType.USERNAME.type, username, ActionType.COPY) + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt b/app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt index 3a6d9e2c..576a051b 100644 --- a/app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt +++ b/app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt @@ -4,12 +4,12 @@ */ package dev.msfjarvis.aps.data.password +import androidx.annotation.VisibleForTesting import com.github.michaelbull.result.get import dev.msfjarvis.aps.util.totp.Otp import dev.msfjarvis.aps.util.totp.TotpFinder import dev.msfjarvis.aps.util.totp.UriTotpFinder import java.io.ByteArrayOutputStream -import java.io.UnsupportedEncodingException import java.util.Date /** @@ -20,20 +20,28 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot val password: String val username: String? + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val digits: String + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val totpSecret: String? val totpPeriod: Long + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val totpAlgorithm: String - var extraContent: String - private set + val extraContent: String + val extraContentWithoutAuthData: String + val extraContentMap: Map<String, String> - @Throws(UnsupportedEncodingException::class) - constructor(os: ByteArrayOutputStream) : this(os.toString("UTF-8"), UriTotpFinder()) + constructor(os: ByteArrayOutputStream) : this(os.toString(Charsets.UTF_8.name()), UriTotpFinder()) init { val (foundPassword, passContent) = findAndStripPassword(content.split("\n".toRegex())) password = foundPassword extraContent = passContent.joinToString("\n") + extraContentWithoutAuthData = generateExtraContentWithoutAuthData() + extraContentMap = generateExtraContentPairs() username = findUsername() digits = findOtpDigits(content) totpSecret = findTotpSecret(content) @@ -45,6 +53,10 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot return extraContent.isNotEmpty() } + fun hasExtraContentWithoutAuthData(): Boolean { + return extraContentWithoutAuthData.isNotEmpty() + } + fun hasTotp(): Boolean { return totpSecret != null } @@ -59,23 +71,63 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot return Otp.calculateCode(totpSecret, Date().time / (1000 * totpPeriod), totpAlgorithm, digits).get() } - val extraContentWithoutAuthData by lazy(LazyThreadSafetyMode.NONE) { + private fun generateExtraContentWithoutAuthData(): String { var foundUsername = false - extraContent.splitToSequence("\n").filter { line -> - return@filter when { - USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } && !foundUsername -> { - foundUsername = true - false - } - line.startsWith("otpauth://", ignoreCase = true) || - line.startsWith("totp:", ignoreCase = true) -> { - false - } - else -> { - true + return extraContent + .lineSequence() + .filter { line -> + return@filter when { + USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } && !foundUsername -> { + foundUsername = true + false + } + line.startsWith("otpauth://", ignoreCase = true) || + line.startsWith("totp:", ignoreCase = true) -> { + false + } + else -> { + true + } } + }.joinToString(separator = "\n") + } + + private fun generateExtraContentPairs(): Map<String, String> { + fun MutableMap<String, String>.putOrAppend(key: String, value: String) { + if (value.isEmpty()) return + val existing = this[key] + this[key] = if (existing == null) { + value + } else { + "$existing\n$value" + } + } + + val items = mutableMapOf<String, String>() + // Take extraContentWithoutAuthData and onEach line perform the following tasks + extraContentWithoutAuthData.lines().forEach { line -> + // Split the line on ':' and save all the parts into an array + // "ABC : DEF:GHI" --> ["ABC", "DEF", "GHI"] + val splitArray = line.split(":") + // Take the first element of the array. This will be the key for the key-value pair. + // ["ABC ", " DEF", "GHI"] -> key = "ABC" + val key = splitArray.first().trimEnd() + // Remove the first element from the array and join the rest of the string again with ':' as separator. + // ["ABC ", " DEF", "GHI"] -> value = "DEF:GHI" + val value = splitArray.drop(1).joinToString(":").trimStart() + + if (key.isNotEmpty() && value.isNotEmpty()) { + // If both key and value are not empty, we can form a pair with this so add it to the map. + // key = "ABC", value = "DEF:GHI" + items[key] = value + } else { + // If either key or value is empty, we were not able to form proper key-value pair. + // So append the original line into an "EXTRA CONTENT" map entry + items.putOrAppend(EXTRA_CONTENT, line) } - }.joinToString(separator = "\n") + } + + return items } private fun findUsername(): String? { @@ -118,6 +170,9 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot companion object { + private const val EXTRA_CONTENT = "Extra Content" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val USERNAME_FIELDS = arrayOf( "login:", "username:", @@ -127,9 +182,10 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot "name:", "handle:", "id:", - "identity:" + "identity:", ) + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val PASSWORD_FIELDS = arrayOf( "password:", "secret:", diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/adapters/FieldItemAdapter.kt b/app/src/main/java/dev/msfjarvis/aps/ui/adapters/FieldItemAdapter.kt new file mode 100644 index 00000000..5ecc2888 --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/adapters/FieldItemAdapter.kt @@ -0,0 +1,85 @@ +package dev.msfjarvis.aps.ui.adapters + +import android.text.method.PasswordTransformationMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.textfield.TextInputLayout +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.data.password.FieldItem +import dev.msfjarvis.aps.databinding.ItemFieldBinding + +class FieldItemAdapter( + private var fieldItemList: List<FieldItem>, + private val showPassword: Boolean, + private val copyTextToClipBoard: (text: String?) -> Unit, +) : RecyclerView.Adapter<FieldItemAdapter.FieldItemViewHolder>() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldItemViewHolder { + val binding = ItemFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return FieldItemViewHolder(binding.root, binding) + } + + override fun onBindViewHolder(holder: FieldItemViewHolder, position: Int) { + holder.bind(fieldItemList[position], showPassword, copyTextToClipBoard) + } + + override fun getItemCount(): Int { + return fieldItemList.size + } + + fun updateOTPCode(code: String) { + var otpItemPosition = -1; + fieldItemList = fieldItemList.mapIndexed { position, item -> + if (item.key.equals(FieldItem.ItemType.OTP.type, true)) { + otpItemPosition = position + return@mapIndexed FieldItem.createOtpField(code) + } + + return@mapIndexed item + } + + notifyItemChanged(otpItemPosition) + } + + fun updateItems(itemList: List<FieldItem>) { + fieldItemList = itemList + notifyDataSetChanged() + } + + class FieldItemViewHolder(itemView: View, val binding: ItemFieldBinding) : + RecyclerView.ViewHolder(itemView) { + + fun bind(fieldItem: FieldItem, showPassword: Boolean, copyTextToClipBoard: (String?) -> Unit) { + with(binding) { + itemText.hint = fieldItem.key + itemTextContainer.hint = fieldItem.key + itemText.setText(fieldItem.value) + + when (fieldItem.action) { + FieldItem.ActionType.COPY -> { + itemTextContainer.apply { + endIconDrawable = ContextCompat.getDrawable(itemView.context, R.drawable.ic_content_copy) + endIconMode = TextInputLayout.END_ICON_CUSTOM + setEndIconOnClickListener { copyTextToClipBoard(itemText.text.toString()) } + } + } + FieldItem.ActionType.HIDE -> { + itemTextContainer.apply { + endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE + setOnClickListener { copyTextToClipBoard(itemText.text.toString()) } + } + itemText.apply { + if (!showPassword) { + transformationMethod = PasswordTransformationMethod.getInstance() + } + setOnClickListener { copyTextToClipBoard(itemText.text.toString()) } + } + } + } + } + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt index 8e08b039..88f76161 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt @@ -6,9 +6,7 @@ package dev.msfjarvis.aps.ui.crypto import android.content.Intent -import android.graphics.Typeface import android.os.Bundle -import android.text.method.PasswordTransformationMethod import android.view.Menu import android.view.MenuItem import android.view.View @@ -19,8 +17,10 @@ import com.github.ajalt.timberkt.e import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.runCatching import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.data.password.FieldItem import dev.msfjarvis.aps.data.password.PasswordEntry import dev.msfjarvis.aps.databinding.DecryptLayoutBinding +import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter import dev.msfjarvis.aps.util.extensions.viewBinding import dev.msfjarvis.aps.util.settings.PreferenceKeys import java.io.ByteArrayOutputStream @@ -172,80 +172,56 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound { startAutoDismissTimer() runCatching { val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true) - val showExtraContent = settings.getBoolean(PreferenceKeys.SHOW_EXTRA_CONTENT, true) - val monoTypeface = Typeface.createFromAsset(assets, "fonts/sourcecodepro.ttf") val entry = PasswordEntry(outputStream) + val items = arrayListOf<FieldItem>() + val adapter = FieldItemAdapter(emptyList(), showPassword) { text -> + copyTextToClipboard(text) + } + + if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) { + copyPasswordToClipboard(entry.password) + } passwordEntry = entry invalidateOptionsMenu() - with(binding) { - if (entry.password.isEmpty()) { - passwordTextContainer.visibility = View.GONE - } else { - passwordTextContainer.visibility = View.VISIBLE - passwordText.typeface = monoTypeface - passwordText.setText(entry.password) - if (!showPassword) { - passwordText.transformationMethod = PasswordTransformationMethod.getInstance() - } - passwordTextContainer.setOnClickListener { copyPasswordToClipboard(entry.password) } - passwordText.setOnClickListener { copyPasswordToClipboard(entry.password) } - } - - if (entry.hasExtraContent()) { - if (entry.extraContentWithoutAuthData.isNotEmpty()) { - extraContentContainer.visibility = View.VISIBLE - extraContent.typeface = monoTypeface - extraContent.setText(entry.extraContentWithoutAuthData) - if (!showExtraContent) { - extraContent.transformationMethod = PasswordTransformationMethod.getInstance() - } - extraContentContainer.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutAuthData) } - extraContent.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutAuthData) } - } + if (entry.password.isNotEmpty()) { + items.add(FieldItem.createPasswordField(entry.password)) + } - if (entry.hasUsername()) { - usernameText.typeface = monoTypeface - usernameText.setText(entry.username) - usernameTextContainer.setEndIconOnClickListener { copyTextToClipboard(entry.username) } - usernameTextContainer.visibility = View.VISIBLE - } else { - usernameTextContainer.visibility = View.GONE + if (entry.hasTotp()) { + launch(Dispatchers.IO) { + // Calculate the actual remaining time for the first pass + // then return to the standard 30 second affair. + val remainingTime = + entry.totpPeriod - (System.currentTimeMillis() % entry.totpPeriod) + withContext(Dispatchers.Main) { + val code = entry.calculateTotpCode() ?: "Error" + items.add(FieldItem.createOtpField(code)) } - - if (entry.hasTotp()) { - otpTextContainer.visibility = View.VISIBLE - otpTextContainer.setEndIconOnClickListener { - copyTextToClipboard( - otpText.text.toString(), - snackbarTextRes = R.string.clipboard_otp_copied_text - ) - } - launch(Dispatchers.IO) { - // Calculate the actual remaining time for the first pass - // then return to the standard 30 second affair. - val remainingTime = entry.totpPeriod - (System.currentTimeMillis() % entry.totpPeriod) - withContext(Dispatchers.Main) { - otpText.setText(entry.calculateTotpCode() - ?: "Error") - } - delay(remainingTime.seconds) - repeat(Int.MAX_VALUE) { - val code = entry.calculateTotpCode() ?: "Error" - withContext(Dispatchers.Main) { - otpText.setText(code) - } - delay(30.seconds) - } + delay(remainingTime.seconds) + repeat(Int.MAX_VALUE) { + val code = entry.calculateTotpCode() ?: "Error" + withContext(Dispatchers.Main) { + adapter.updateOTPCode(code) } + delay(30.seconds) } } } - if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) { - copyPasswordToClipboard(entry.password) + if (!entry.username.isNullOrEmpty()) { + items.add(FieldItem.createUsernameField(entry.username)) } + + if (entry.hasExtraContentWithoutAuthData()) { + entry.extraContentMap.forEach { (key, value) -> + items.add(FieldItem(key, value, FieldItem.ActionType.COPY)) + } + } + + binding.recyclerView.adapter = adapter + adapter.updateItems(items) }.onFailure { e -> e(e) } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt index 8d2536d0..922eff46 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt @@ -103,11 +103,6 @@ class PasswordSettings(private val activity: FragmentActivity) : SettingsProvide summaryRes = R.string.show_password_pref_summary defaultValue = true } - checkBox(PreferenceKeys.SHOW_EXTRA_CONTENT) { - titleRes = R.string.show_extra_content_pref_title - summaryRes = R.string.show_extra_content_pref_summary - defaultValue = true - } checkBox(PreferenceKeys.COPY_ON_DECRYPT) { titleRes = R.string.pref_copy_title summaryRes = R.string.pref_copy_summary diff --git a/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt b/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt index 084b70c2..708e147f 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt +++ b/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt @@ -62,7 +62,6 @@ object PreferenceKeys { const val REPOSITORY_INITIALIZED = "repository_initialized" const val REPO_CHANGED = "repo_changed" const val SEARCH_ON_START = "search_on_start" - const val SHOW_EXTRA_CONTENT = "show_extra_content" @Deprecated( message = "Use SHOW_HIDDEN_CONTENTS instead", diff --git a/app/src/main/res/layout/decrypt_layout.xml b/app/src/main/res/layout/decrypt_layout.xml index 8d16ff77..e314b345 100644 --- a/app/src/main/res/layout/decrypt_layout.xml +++ b/app/src/main/res/layout/decrypt_layout.xml @@ -3,156 +3,73 @@ ~ SPDX-License-Identifier: GPL-3.0-only --> -<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_width="fill_parent" + android:layout_height="wrap_content" android:orientation="vertical" + android:padding="16dp" tools:context=".ui.crypto.DecryptActivity"> - <androidx.constraintlayout.widget.ConstraintLayout - android:layout_width="fill_parent" + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/password_category" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical" - android:padding="16dp"> - - <androidx.appcompat.widget.AppCompatTextView - android:id="@+id/password_category" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - android:layout_marginStart="16dp" - android:textColor="?android:attr/textColor" - android:textIsSelectable="false" - android:textSize="18sp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:text="CATEGORY HERE" /> - - <androidx.appcompat.widget.AppCompatTextView - android:id="@+id/password_file" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/activity_horizontal_margin" - android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium" - android:textColor="?attr/colorSecondary" - android:textSize="24sp" - android:textStyle="bold" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/password_category" - tools:text="PASSWORD FILE NAME HERE" /> - - <androidx.appcompat.widget.AppCompatTextView - android:id="@+id/password_last_changed" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - android:layout_marginStart="16dp" - android:textColor="?android:attr/textColor" - android:textIsSelectable="false" - android:textSize="18sp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/password_file" - tools:text="LAST CHANGED HERE" /> - - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/divider" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:layout_marginBottom="16dp" - android:src="@drawable/divider" - app:layout_constraintTop_toBottomOf="@id/password_last_changed" - tools:ignore="ContentDescription" /> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/password_text_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:hint="@string/password" - android:visibility="gone" - app:endIconMode="password_toggle" - app:layout_constraintTop_toBottomOf="@id/divider" - tools:visibility="visible"> - - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/password_text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:editable="false" - android:fontFamily="@font/sourcecodepro" - android:textIsSelectable="true" - tools:text="p@55w0rd!" /> - - </com.google.android.material.textfield.TextInputLayout> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/otp_text_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:hint="@string/otp" - android:visibility="gone" - app:endIconDrawable="@drawable/ic_content_copy" - app:endIconMode="custom" - app:layout_constraintTop_toBottomOf="@id/password_text_container" - tools:visibility="visible"> - - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/otp_text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:editable="false" - android:fontFamily="@font/sourcecodepro" - android:textIsSelectable="true" - tools:text="123456" /> - - </com.google.android.material.textfield.TextInputLayout> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/username_text_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:hint="@string/username" - android:visibility="gone" - app:endIconDrawable="@drawable/ic_content_copy" - app:endIconMode="custom" - app:layout_constraintTop_toBottomOf="@id/otp_text_container" - tools:visibility="visible"> - - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/username_text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:editable="false" - android:textIsSelectable="true" - tools:text="totally_real_user@example.com" /> - </com.google.android.material.textfield.TextInputLayout> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/extra_content_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:hint="@string/extra_content" - android:visibility="gone" - app:endIconMode="password_toggle" - app:layout_constraintTop_toBottomOf="@id/username_text_container" - tools:visibility="visible"> - - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/extra_content" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:editable="false" - android:textIsSelectable="true" - tools:text="lots of extra content that will surely fill this \n up well" /> - </com.google.android.material.textfield.TextInputLayout> - - </androidx.constraintlayout.widget.ConstraintLayout> + android:layout_gravity="center_vertical" + android:layout_marginStart="16dp" + android:textColor="?android:attr/textColor" + android:textIsSelectable="false" + android:textSize="18sp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="CATEGORY HERE" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/password_file" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/activity_horizontal_margin" + android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium" + android:textColor="?attr/colorSecondary" + android:textSize="24sp" + android:textStyle="bold" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/password_category" + tools:text="PASSWORD FILE NAME HERE" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/password_last_changed" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="16dp" + android:textColor="?android:attr/textColor" + android:textIsSelectable="false" + android:textSize="18sp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/password_file" + tools:text="LAST CHANGED HERE" /> + + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/divider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:layout_marginBottom="16dp" + android:src="@drawable/divider" + app:layout_constraintTop_toBottomOf="@id/password_last_changed" + tools:ignore="ContentDescription" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:layout_constraintTop_toBottomOf="@id/divider" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + tools:listitem="@layout/item_field" /> -</ScrollView> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_field.xml b/app/src/main/res/layout/item_field.xml new file mode 100644 index 00000000..8c98bd05 --- /dev/null +++ b/app/src/main/res/layout/item_field.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/item_text_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + tools:visibility="visible" + tools:hint="@string/password"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/item_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:editable="false" + android:fontFamily="@font/sourcecodepro" + android:textIsSelectable="true" + tools:text="p@55w0rd!" /> + + </com.google.android.material.textfield.TextInputLayout> + +</LinearLayout> diff --git a/app/src/test/java/dev/msfjarvis/aps/data/password/PasswordEntryTest.kt b/app/src/test/java/dev/msfjarvis/aps/data/password/PasswordEntryTest.kt index a627dd7a..aab7d1d2 100644 --- a/app/src/test/java/dev/msfjarvis/aps/data/password/PasswordEntryTest.kt +++ b/app/src/test/java/dev/msfjarvis/aps/data/password/PasswordEntryTest.kt @@ -50,6 +50,38 @@ class PasswordEntryTest { assertEquals("", makeEntry("").extraContent) } + @Test fun parseExtraContentWithoutAuth() { + var entry = makeEntry("username: abc\npassword: abc\ntest: abcdef") + assertEquals(1, entry.extraContentMap.size) + assertTrue(entry.extraContentMap.containsKey("test")) + assertEquals("abcdef", entry.extraContentMap["test"]) + + entry = makeEntry("username: abc\npassword: abc\ntest: :abcdef:") + assertEquals(1, entry.extraContentMap.size) + assertTrue(entry.extraContentMap.containsKey("test")) + assertEquals(":abcdef:", entry.extraContentMap["test"]) + + entry = makeEntry("username: abc\npassword: abc\ntest : ::abc:def::") + assertEquals(1, entry.extraContentMap.size) + assertTrue(entry.extraContentMap.containsKey("test")) + assertEquals("::abc:def::", entry.extraContentMap["test"]) + + entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\ntest2: ghijkl") + assertEquals(2, entry.extraContentMap.size) + assertTrue(entry.extraContentMap.containsKey("test2")) + assertEquals("ghijkl", entry.extraContentMap["test2"]) + + entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\n: ghijkl\n mnopqr:") + assertEquals(2, entry.extraContentMap.size) + assertTrue(entry.extraContentMap.containsKey("Extra Content")) + assertEquals(": ghijkl\n mnopqr:", entry.extraContentMap["Extra Content"]) + + entry = makeEntry("username: abc\npassword: abc\n:\n\n") + assertEquals(1, entry.extraContentMap.size) + assertTrue(entry.extraContentMap.containsKey("Extra Content")) + assertEquals(":", entry.extraContentMap["Extra Content"]) + } + @Test fun testGetUsername() { for (field in PasswordEntry.USERNAME_FIELDS) { assertEquals("username", makeEntry("\n$field username").username) @@ -133,7 +165,7 @@ class PasswordEntryTest { // This implementation is hardcoded for the URI above. val testFinder = object : TotpFinder { - override fun findSecret(content: String): String? { + override fun findSecret(content: String): String { return "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ" } |