aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/dev/msfjarvis/aps/ui/autofill
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2020-12-05 06:07:18 +0530
committerGitHub <noreply@github.com>2020-12-05 06:07:18 +0530
commit5e66d99c852ea67a88b650c03b0e8d55e83eccde (patch)
treeaee323a08253fe7b863976eff92a1bd412bd1430 /app/src/main/java/dev/msfjarvis/aps/ui/autofill
parent8eb55f18a11d6b2f155c7c9d48ca833313ee13db (diff)
downloadHardenedPass-5e66d99c852ea67a88b650c03b0e8d55e83eccde.tar.gz
HardenedPass-5e66d99c852ea67a88b650c03b0e8d55e83eccde.zip
Refactor package structure (#1233)
* idea: default test runner to Gradle * Kick off package structure revamp * Reparent all classes under dev.msfjarvis.aps Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/ui/autofill')
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt245
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillFilterActivity.kt220
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillPublisherChangedActivity.kt114
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillSaveActivity.kt149
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/autofill/PasswordViewHolder.kt16
5 files changed, 744 insertions, 0 deletions
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt
new file mode 100644
index 00000000..5d6fb886
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt
@@ -0,0 +1,245 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package dev.msfjarvis.aps.ui.autofill
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.os.Build
+import android.os.Bundle
+import android.view.autofill.AutofillManager
+import android.widget.Toast
+import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import com.github.ajalt.timberkt.d
+import com.github.ajalt.timberkt.e
+import com.github.michaelbull.result.getOrElse
+import com.github.michaelbull.result.onFailure
+import com.github.michaelbull.result.onSuccess
+import com.github.michaelbull.result.runCatching
+import com.github.androidpasswordstore.autofillparser.AutofillAction
+import com.github.androidpasswordstore.autofillparser.Credentials
+import dev.msfjarvis.aps.util.autofill.AutofillPreferences
+import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder
+import dev.msfjarvis.aps.util.autofill.DirectoryStructure
+import dev.msfjarvis.aps.data.password.PasswordEntry
+import dev.msfjarvis.aps.util.extensions.OPENPGP_PROVIDER
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import me.msfjarvis.openpgpktx.util.OpenPgpApi
+import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
+import org.openintents.openpgp.IOpenPgpService2
+import org.openintents.openpgp.OpenPgpError
+
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope {
+
+ companion object {
+
+ private const val EXTRA_FILE_PATH = "dev.msfjarvis.aps.autofill.oreo.EXTRA_FILE_PATH"
+ private const val EXTRA_SEARCH_ACTION =
+ "dev.msfjarvis.aps.autofill.oreo.EXTRA_SEARCH_ACTION"
+
+ private var decryptFileRequestCode = 1
+
+ fun makeDecryptFileIntent(file: File, forwardedExtras: Bundle, context: Context): Intent {
+ return Intent(context, AutofillDecryptActivity::class.java).apply {
+ putExtras(forwardedExtras)
+ putExtra(EXTRA_SEARCH_ACTION, true)
+ putExtra(EXTRA_FILE_PATH, file.absolutePath)
+ }
+ }
+
+ fun makeDecryptFileIntentSender(file: File, context: Context): IntentSender {
+ val intent = Intent(context, AutofillDecryptActivity::class.java).apply {
+ putExtra(EXTRA_SEARCH_ACTION, false)
+ putExtra(EXTRA_FILE_PATH, file.absolutePath)
+ }
+ return PendingIntent.getActivity(
+ context,
+ decryptFileRequestCode++,
+ intent,
+ PendingIntent.FLAG_CANCEL_CURRENT
+ ).intentSender
+ }
+ }
+
+ private val decryptInteractionRequiredAction = registerForActivityResult(StartIntentSenderForResult()) { result ->
+ if (continueAfterUserInteraction != null) {
+ val data = result.data
+ if (result.resultCode == RESULT_OK && data != null) {
+ continueAfterUserInteraction?.resume(data)
+ } else {
+ continueAfterUserInteraction?.resumeWithException(Exception("OpenPgpApi ACTION_DECRYPT_VERIFY failed to continue after user interaction"))
+ }
+ continueAfterUserInteraction = null
+ }
+ }
+
+ private var continueAfterUserInteraction: Continuation<Intent>? = null
+ private lateinit var directoryStructure: DirectoryStructure
+
+ override val coroutineContext
+ get() = Dispatchers.IO + SupervisorJob()
+
+ override fun onStart() {
+ super.onStart()
+ val filePath = intent?.getStringExtra(EXTRA_FILE_PATH) ?: run {
+ e { "AutofillDecryptActivity started without EXTRA_FILE_PATH" }
+ finish()
+ return
+ }
+ val clientState = intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) ?: run {
+ e { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" }
+ finish()
+ return
+ }
+ val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!!
+ val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match
+ directoryStructure = AutofillPreferences.directoryStructure(this)
+ d { action.toString() }
+ launch {
+ val credentials = decryptCredential(File(filePath))
+ if (credentials == null) {
+ setResult(RESULT_CANCELED)
+ } else {
+ val fillInDataset =
+ AutofillResponseBuilder.makeFillInDataset(
+ this@AutofillDecryptActivity,
+ credentials,
+ clientState,
+ action
+ )
+ withContext(Dispatchers.Main) {
+ setResult(RESULT_OK, Intent().apply {
+ putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset)
+ })
+ }
+ }
+ withContext(Dispatchers.Main) {
+ finish()
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ coroutineContext.cancelChildren()
+ }
+
+ private suspend fun executeOpenPgpApi(
+ data: Intent,
+ input: InputStream,
+ output: OutputStream
+ ): Intent? {
+ var openPgpServiceConnection: OpenPgpServiceConnection? = null
+ val openPgpService = suspendCoroutine<IOpenPgpService2> { cont ->
+ openPgpServiceConnection = OpenPgpServiceConnection(
+ this,
+ OPENPGP_PROVIDER,
+ object : OpenPgpServiceConnection.OnBound {
+ override fun onBound(service: IOpenPgpService2) {
+ cont.resume(service)
+ }
+
+ override fun onError(e: Exception) {
+ cont.resumeWithException(e)
+ }
+ }).also { it.bindToService() }
+ }
+ return OpenPgpApi(this, openPgpService).executeApi(data, input, output).also {
+ openPgpServiceConnection?.unbindFromService()
+ }
+ }
+
+ private suspend fun decryptCredential(
+ file: File,
+ resumeIntent: Intent? = null
+ ): Credentials? {
+ val command = resumeIntent ?: Intent().apply {
+ action = OpenPgpApi.ACTION_DECRYPT_VERIFY
+ }
+ runCatching {
+ file.inputStream()
+ }.onFailure { e ->
+ e(e) { "File to decrypt not found" }
+ return null
+ }.onSuccess { encryptedInput ->
+ val decryptedOutput = ByteArrayOutputStream()
+ runCatching {
+ executeOpenPgpApi(command, encryptedInput, decryptedOutput)
+ }.onFailure { e ->
+ e(e) { "OpenPgpApi ACTION_DECRYPT_VERIFY failed" }
+ return null
+ }.onSuccess { result ->
+ return when (val resultCode =
+ result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
+ OpenPgpApi.RESULT_CODE_SUCCESS -> {
+ runCatching {
+ val entry = withContext(Dispatchers.IO) {
+ @Suppress("BlockingMethodInNonBlockingContext")
+ (PasswordEntry(decryptedOutput))
+ }
+ AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure)
+ }.getOrElse { e ->
+ e(e) { "Failed to parse password entry" }
+ return null
+ }
+ }
+ OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
+ val pendingIntent: PendingIntent =
+ result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!!
+ runCatching {
+ val intentToResume = withContext(Dispatchers.Main) {
+ suspendCoroutine<Intent> { cont ->
+ continueAfterUserInteraction = cont
+ decryptInteractionRequiredAction.launch(IntentSenderRequest.Builder(pendingIntent.intentSender).build())
+ }
+ }
+ decryptCredential(file, intentToResume)
+ }.getOrElse { e ->
+ e(e) { "OpenPgpApi ACTION_DECRYPT_VERIFY failed with user interaction" }
+ return null
+ }
+ }
+ OpenPgpApi.RESULT_CODE_ERROR -> {
+ val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR)
+ if (error != null) {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(
+ applicationContext,
+ "Error from OpenKeyChain: ${error.message}",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ e { "OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}" }
+ }
+ null
+ }
+ else -> {
+ e { "Unrecognized OpenPgpApi result: $resultCode" }
+ null
+ }
+ }
+ }
+ }
+ return null
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillFilterActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillFilterActivity.kt
new file mode 100644
index 00000000..fe19a636
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillFilterActivity.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package dev.msfjarvis.aps.ui.autofill
+
+import android.annotation.TargetApi
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.view.autofill.AutofillManager
+import android.widget.TextView
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.text.bold
+import androidx.core.text.buildSpannedString
+import androidx.core.text.underline
+import androidx.core.widget.addTextChangedListener
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.github.ajalt.timberkt.e
+import com.github.androidpasswordstore.autofillparser.FormOrigin
+import dev.msfjarvis.aps.R
+import dev.msfjarvis.aps.util.autofill.AutofillMatcher
+import dev.msfjarvis.aps.util.autofill.AutofillPreferences
+import dev.msfjarvis.aps.util.autofill.DirectoryStructure
+import dev.msfjarvis.aps.databinding.ActivityOreoAutofillFilterBinding
+import dev.msfjarvis.aps.data.password.PasswordItem
+import dev.msfjarvis.aps.util.extensions.viewBinding
+import dev.msfjarvis.aps.util.viewmodel.FilterMode
+import dev.msfjarvis.aps.util.viewmodel.ListMode
+import dev.msfjarvis.aps.util.viewmodel.SearchMode
+import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryAdapter
+import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel
+
+@TargetApi(Build.VERSION_CODES.O)
+class AutofillFilterView : AppCompatActivity() {
+
+ companion object {
+
+ private const val HEIGHT_PERCENTAGE = 0.9
+ private const val WIDTH_PERCENTAGE = 0.75
+
+ private const val EXTRA_FORM_ORIGIN_WEB =
+ "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_WEB"
+ private const val EXTRA_FORM_ORIGIN_APP =
+ "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_APP"
+ private var matchAndDecryptFileRequestCode = 1
+
+ fun makeMatchAndDecryptFileIntentSender(
+ context: Context,
+ formOrigin: FormOrigin
+ ): IntentSender {
+ val intent = Intent(context, AutofillFilterView::class.java).apply {
+ when (formOrigin) {
+ is FormOrigin.Web -> putExtra(EXTRA_FORM_ORIGIN_WEB, formOrigin.identifier)
+ is FormOrigin.App -> putExtra(EXTRA_FORM_ORIGIN_APP, formOrigin.identifier)
+ }
+ }
+ return PendingIntent.getActivity(
+ context,
+ matchAndDecryptFileRequestCode++,
+ intent,
+ PendingIntent.FLAG_CANCEL_CURRENT
+ ).intentSender
+ }
+ }
+
+ private lateinit var formOrigin: FormOrigin
+ private lateinit var directoryStructure: DirectoryStructure
+ private val binding by viewBinding(ActivityOreoAutofillFilterBinding::inflate)
+
+ private val model: SearchableRepositoryViewModel by viewModels {
+ ViewModelProvider.AndroidViewModelFactory(application)
+ }
+
+ private val decryptAction = registerForActivityResult(StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) {
+ setResult(RESULT_OK, result.data)
+ }
+ finish()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+ setFinishOnTouchOutside(true)
+
+ val params = window.attributes
+ params.height = (HEIGHT_PERCENTAGE * resources.displayMetrics.heightPixels).toInt()
+ params.width = (WIDTH_PERCENTAGE * resources.displayMetrics.widthPixels).toInt()
+ window.attributes = params
+
+ if (intent?.hasExtra(AutofillManager.EXTRA_CLIENT_STATE) != true) {
+ e { "AutofillFilterActivity started without EXTRA_CLIENT_STATE" }
+ finish()
+ return
+ }
+ formOrigin = when {
+ intent?.hasExtra(EXTRA_FORM_ORIGIN_WEB) == true -> {
+ FormOrigin.Web(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_WEB)!!)
+ }
+ intent?.hasExtra(EXTRA_FORM_ORIGIN_APP) == true -> {
+ FormOrigin.App(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_APP)!!)
+ }
+ else -> {
+ e { "AutofillFilterActivity started without EXTRA_FORM_ORIGIN_WEB or EXTRA_FORM_ORIGIN_APP" }
+ finish()
+ return
+ }
+ }
+ directoryStructure = AutofillPreferences.directoryStructure(this)
+
+ supportActionBar?.hide()
+ bindUI()
+ updateSearch()
+ setResult(RESULT_CANCELED)
+ }
+
+ private fun bindUI() {
+ with(binding) {
+ rvPassword.apply {
+ adapter = SearchableRepositoryAdapter(
+ R.layout.oreo_autofill_filter_row,
+ ::PasswordViewHolder
+ ) { item ->
+ val file = item.file.relativeTo(item.rootDir)
+ val pathToIdentifier = directoryStructure.getPathToIdentifierFor(file)
+ val identifier = directoryStructure.getIdentifierFor(file)
+ val accountPart = directoryStructure.getAccountPartFor(file)
+ check(identifier != null || accountPart != null) { "At least one of identifier and accountPart should always be non-null" }
+ title.text = if (identifier != null) {
+ buildSpannedString {
+ if (pathToIdentifier != null)
+ append("$pathToIdentifier/")
+ bold { underline { append(identifier) } }
+ }
+ } else {
+ accountPart
+ }
+ subtitle.apply {
+ if (identifier != null && accountPart != null) {
+ text = accountPart
+ visibility = View.VISIBLE
+ } else {
+ visibility = View.GONE
+ }
+ }
+ }.onItemClicked { _, item ->
+ decryptAndFill(item)
+ }
+ layoutManager = LinearLayoutManager(context)
+ }
+ search.apply {
+ val initialSearch =
+ formOrigin.getPrettyIdentifier(applicationContext, untrusted = false)
+ setText(initialSearch, TextView.BufferType.EDITABLE)
+ addTextChangedListener { updateSearch() }
+ }
+ origin.text = buildSpannedString {
+ append(getString(R.string.oreo_autofill_select_and_fill_into))
+ append("\n")
+ bold {
+ append(formOrigin.getPrettyIdentifier(applicationContext, untrusted = true))
+ }
+ }
+ strictDomainSearch.apply {
+ visibility = if (formOrigin is FormOrigin.Web) View.VISIBLE else View.GONE
+ isChecked = formOrigin is FormOrigin.Web
+ setOnCheckedChangeListener { _, _ -> updateSearch() }
+ }
+ shouldMatch.text = getString(
+ R.string.oreo_autofill_match_with,
+ formOrigin.getPrettyIdentifier(applicationContext)
+ )
+ model.searchResult.observe(this@AutofillFilterView) { result ->
+ val list = result.passwordItems
+ (rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) {
+ rvPassword.scrollToPosition(0)
+ }
+ // Switch RecyclerView out for a "no results" message if the new list is empty and
+ // the message is not yet shown (and vice versa).
+ if ((list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) ||
+ (list.isNotEmpty() && rvPasswordSwitcher.nextView.id == rvPassword.id)
+ ) {
+ rvPasswordSwitcher.showNext()
+ }
+ }
+ }
+ }
+
+ private fun updateSearch() {
+ model.search(
+ binding.search.text.toString().trim(),
+ filterMode = if (binding.strictDomainSearch.isChecked) FilterMode.StrictDomain else FilterMode.Fuzzy,
+ searchMode = SearchMode.RecursivelyInSubdirectories,
+ listMode = ListMode.FilesOnly
+ )
+ }
+
+ private fun decryptAndFill(item: PasswordItem) {
+ if (binding.shouldClear.isChecked) AutofillMatcher.clearMatchesFor(applicationContext, formOrigin)
+ if (binding.shouldMatch.isChecked) AutofillMatcher.addMatchFor(
+ applicationContext,
+ formOrigin,
+ item.file
+ )
+ // intent?.extras? is checked to be non-null in onCreate
+ decryptAction.launch(AutofillDecryptActivity.makeDecryptFileIntent(
+ item.file,
+ intent!!.extras!!,
+ this
+ ))
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillPublisherChangedActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillPublisherChangedActivity.kt
new file mode 100644
index 00000000..a8edabbe
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillPublisherChangedActivity.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package dev.msfjarvis.aps.ui.autofill
+
+import android.annotation.TargetApi
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.service.autofill.FillResponse
+import android.text.format.DateUtils
+import android.view.View
+import android.view.autofill.AutofillManager
+import androidx.appcompat.app.AppCompatActivity
+import com.github.ajalt.timberkt.e
+import com.github.androidpasswordstore.autofillparser.FormOrigin
+import com.github.androidpasswordstore.autofillparser.computeCertificatesHash
+import com.github.michaelbull.result.onFailure
+import com.github.michaelbull.result.runCatching
+import dev.msfjarvis.aps.R
+import dev.msfjarvis.aps.util.autofill.AutofillMatcher
+import dev.msfjarvis.aps.util.autofill.AutofillPublisherChangedException
+import dev.msfjarvis.aps.databinding.ActivityOreoAutofillPublisherChangedBinding
+import dev.msfjarvis.aps.util.extensions.viewBinding
+
+@TargetApi(Build.VERSION_CODES.O)
+class AutofillPublisherChangedActivity : AppCompatActivity() {
+
+ companion object {
+
+ private const val EXTRA_APP_PACKAGE =
+ "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_APP_PACKAGE"
+ private const val EXTRA_FILL_RESPONSE_AFTER_RESET =
+ "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FILL_RESPONSE_AFTER_RESET"
+ private var publisherChangedRequestCode = 1
+
+ fun makePublisherChangedIntentSender(
+ context: Context,
+ publisherChangedException: AutofillPublisherChangedException,
+ fillResponseAfterReset: FillResponse?,
+ ): IntentSender {
+ val intent = Intent(context, AutofillPublisherChangedActivity::class.java).apply {
+ putExtra(EXTRA_APP_PACKAGE, publisherChangedException.formOrigin.identifier)
+ putExtra(EXTRA_FILL_RESPONSE_AFTER_RESET, fillResponseAfterReset)
+ }
+ return PendingIntent.getActivity(
+ context, publisherChangedRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT
+ ).intentSender
+ }
+ }
+
+ private lateinit var appPackage: String
+ private val binding by viewBinding(ActivityOreoAutofillPublisherChangedBinding::inflate)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+ setFinishOnTouchOutside(true)
+
+ appPackage = intent.getStringExtra(EXTRA_APP_PACKAGE) ?: run {
+ e { "AutofillPublisherChangedActivity started without EXTRA_PACKAGE_NAME" }
+ finish()
+ return
+ }
+ supportActionBar?.hide()
+ showPackageInfo()
+ with(binding) {
+ okButton.setOnClickListener { finish() }
+ advancedButton.setOnClickListener {
+ advancedButton.visibility = View.GONE
+ warningAppAdvancedInfo.visibility = View.VISIBLE
+ resetButton.visibility = View.VISIBLE
+ }
+ resetButton.setOnClickListener {
+ AutofillMatcher.clearMatchesFor(this@AutofillPublisherChangedActivity, FormOrigin.App(appPackage))
+ val fillResponse = intent.getParcelableExtra<FillResponse>(EXTRA_FILL_RESPONSE_AFTER_RESET)
+ setResult(RESULT_OK, Intent().apply {
+ putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse)
+ })
+ finish()
+ }
+ }
+ }
+
+ private fun showPackageInfo() {
+ runCatching {
+ with(binding) {
+ val packageInfo =
+ packageManager.getPackageInfo(appPackage, PackageManager.GET_META_DATA)
+ val installTime = DateUtils.getRelativeTimeSpanString(packageInfo.firstInstallTime)
+ warningAppInstallDate.text =
+ getString(R.string.oreo_autofill_warning_publisher_install_time, installTime)
+ val appInfo =
+ packageManager.getApplicationInfo(appPackage, PackageManager.GET_META_DATA)
+ warningAppName.text = "“${packageManager.getApplicationLabel(appInfo)}”"
+
+ val currentHash = computeCertificatesHash(this@AutofillPublisherChangedActivity, appPackage)
+ warningAppAdvancedInfo.text = getString(
+ R.string.oreo_autofill_warning_publisher_advanced_info_template,
+ appPackage,
+ currentHash
+ )
+ }
+ }.onFailure { e ->
+ e(e) { "Failed to retrieve package info for $appPackage" }
+ finish()
+ }
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillSaveActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillSaveActivity.kt
new file mode 100644
index 00000000..46234c4f
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillSaveActivity.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package dev.msfjarvis.aps.ui.autofill
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.os.Build
+import android.os.Bundle
+import android.view.autofill.AutofillManager
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.os.bundleOf
+import com.github.ajalt.timberkt.e
+import com.github.androidpasswordstore.autofillparser.AutofillAction
+import com.github.androidpasswordstore.autofillparser.Credentials
+import com.github.androidpasswordstore.autofillparser.FormOrigin
+import dev.msfjarvis.aps.util.autofill.AutofillMatcher
+import dev.msfjarvis.aps.util.autofill.AutofillPreferences
+import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder
+import dev.msfjarvis.aps.ui.crypto.PasswordCreationActivity
+import dev.msfjarvis.aps.data.repo.PasswordRepository
+import java.io.File
+
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillSaveActivity : AppCompatActivity() {
+
+ companion object {
+
+ private const val EXTRA_FOLDER_NAME =
+ "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FOLDER_NAME"
+ private const val EXTRA_PASSWORD = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_PASSWORD"
+ private const val EXTRA_NAME = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_NAME"
+ private const val EXTRA_SHOULD_MATCH_APP =
+ "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP"
+ private const val EXTRA_SHOULD_MATCH_WEB =
+ "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_WEB"
+ private const val EXTRA_GENERATE_PASSWORD =
+ "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_GENERATE_PASSWORD"
+
+ private var saveRequestCode = 1
+
+ fun makeSaveIntentSender(
+ context: Context,
+ credentials: Credentials?,
+ formOrigin: FormOrigin
+ ): IntentSender {
+ val identifier = formOrigin.getPrettyIdentifier(context, untrusted = false)
+ // Prevent directory traversals
+ val sanitizedIdentifier = identifier.replace('\\', '_')
+ .replace('/', '_')
+ .trimStart('.')
+ .takeUnless { it.isBlank() } ?: formOrigin.identifier
+ val directoryStructure = AutofillPreferences.directoryStructure(context)
+ val folderName = directoryStructure.getSaveFolderName(
+ sanitizedIdentifier = sanitizedIdentifier,
+ username = credentials?.username
+ )
+ val fileName = directoryStructure.getSaveFileName(username = credentials?.username, identifier = identifier)
+ val intent = Intent(context, AutofillSaveActivity::class.java).apply {
+ putExtras(
+ bundleOf(
+ EXTRA_FOLDER_NAME to folderName,
+ EXTRA_NAME to fileName,
+ EXTRA_PASSWORD to credentials?.password,
+ EXTRA_SHOULD_MATCH_APP to formOrigin.identifier.takeIf { formOrigin is FormOrigin.App },
+ EXTRA_SHOULD_MATCH_WEB to formOrigin.identifier.takeIf { formOrigin is FormOrigin.Web },
+ EXTRA_GENERATE_PASSWORD to (credentials == null)
+ )
+ )
+ }
+ return PendingIntent.getActivity(
+ context,
+ saveRequestCode++,
+ intent,
+ PendingIntent.FLAG_CANCEL_CURRENT
+ ).intentSender
+ }
+ }
+
+ private val formOrigin by lazy(LazyThreadSafetyMode.NONE) {
+ val shouldMatchApp: String? = intent.getStringExtra(EXTRA_SHOULD_MATCH_APP)
+ val shouldMatchWeb: String? = intent.getStringExtra(EXTRA_SHOULD_MATCH_WEB)
+ if (shouldMatchApp != null && shouldMatchWeb == null) {
+ FormOrigin.App(shouldMatchApp)
+ } else if (shouldMatchApp == null && shouldMatchWeb != null) {
+ FormOrigin.Web(shouldMatchWeb)
+ } else {
+ null
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val repo = PasswordRepository.getRepositoryDirectory()
+ val saveIntent = Intent(this, PasswordCreationActivity::class.java).apply {
+ putExtras(
+ bundleOf(
+ "REPO_PATH" to repo.absolutePath,
+ "FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath,
+ PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
+ PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
+ PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
+ )
+ )
+ }
+ registerForActivityResult(StartActivityForResult()) { result ->
+ val data = result.data
+ if (result.resultCode == RESULT_OK && data != null) {
+ val createdPath = data.getStringExtra("CREATED_FILE")!!
+ formOrigin?.let {
+ AutofillMatcher.addMatchFor(this, it, File(createdPath))
+ }
+ val password = data.getStringExtra("PASSWORD")
+ val resultIntent = if (password != null) {
+ // Password was generated and should be filled into a form.
+ val username = data.getStringExtra("USERNAME")
+ val clientState =
+ intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) ?: run {
+ e { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" }
+ finish()
+ return@registerForActivityResult
+ }
+ val credentials = Credentials(username, password, null)
+ val fillInDataset = AutofillResponseBuilder.makeFillInDataset(
+ this,
+ credentials,
+ clientState,
+ AutofillAction.Generate
+ )
+ Intent().apply {
+ putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset)
+ }
+ } else {
+ // Password was extracted from a form, there is nothing to fill.
+ Intent()
+ }
+ setResult(RESULT_OK, resultIntent)
+ } else {
+ setResult(RESULT_CANCELED)
+ }
+ finish()
+ }.launch(saveIntent)
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/PasswordViewHolder.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/PasswordViewHolder.kt
new file mode 100644
index 00000000..e2fbaf76
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/PasswordViewHolder.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package dev.msfjarvis.aps.ui.autofill
+
+import android.view.View
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import dev.msfjarvis.aps.R
+
+class PasswordViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+
+ val title: TextView = itemView.findViewById(R.id.title)
+ val subtitle: TextView = itemView.findViewById(R.id.subtitle)
+}