aboutsummaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
authorAditya Wasan <adityawasan55@gmail.com>2021-02-19 13:57:57 +0530
committerGitHub <noreply@github.com>2021-02-19 08:27:57 +0000
commitd17ff0d9251bd2ad84778535dabe4884a4cf9d76 (patch)
tree57c2cfd031632e8687c038bd35ff2b39f61f6d77 /app/src/main
parent92ece7dbb5607258bcf954963009bf1f9411ab07 (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>
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/data/password/FieldItem.kt27
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt96
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/adapters/FieldItemAdapter.kt85
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt102
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt5
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt1
-rw-r--r--app/src/main/res/layout/decrypt_layout.xml209
-rw-r--r--app/src/main/res/layout/item_field.xml27
8 files changed, 317 insertions, 235 deletions
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>