diff options
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/ui/autofill')
5 files changed, 461 insertions, 504 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 index 94274c37..ab1f402d 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt @@ -51,195 +51,184 @@ import org.openintents.openpgp.OpenPgpError @RequiresApi(Build.VERSION_CODES.O) class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope { - companion object { + 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 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 + 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 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 + 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 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 + private var continueAfterUserInteraction: Continuation<Intent>? = null + private lateinit var directoryStructure: DirectoryStructure - override val coroutineContext - get() = Dispatchers.IO + SupervisorJob() + 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 + 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 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() - } + 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() - } + 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) - } + 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() - } + 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" } + 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 { 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 - } + } + .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)) } - 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 + 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 - } + } + } + 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 index ffff8bbd..cf833a19 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillFilterActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillFilterActivity.kt @@ -41,180 +41,164 @@ 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 + 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() + } + + 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) + 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 + 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 + 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)!!) } - 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 - } + intent?.hasExtra(EXTRA_FORM_ORIGIN_APP) == true -> { + FormOrigin.App(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_APP)!!) } - 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() } + 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" } - 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() + 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 - )) - } + } + + 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 index faf1f1c0..bea2bcd3 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillPublisherChangedActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillPublisherChangedActivity.kt @@ -31,84 +31,83 @@ import dev.msfjarvis.aps.util.extensions.viewBinding @TargetApi(Build.VERSION_CODES.O) class AutofillPublisherChangedActivity : AppCompatActivity() { - companion object { + 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 + 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 + 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) + 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) + 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() - } + 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)}”" + 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() - } + 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 index f307d481..dfec29be 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillSaveActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillSaveActivity.kt @@ -29,121 +29,106 @@ import java.io.File @RequiresApi(Build.VERSION_CODES.O) class AutofillSaveActivity : AppCompatActivity() { - companion object { + 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 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 + 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 + 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) ) - 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 + ) } + 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 - } + 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) + 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 { - setResult(RESULT_CANCELED) + // Password was extracted from a form, there is nothing to fill. + Intent() } - finish() - }.launch(saveIntent) - } + 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 index 2b708591..eacd49c3 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/PasswordViewHolder.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/PasswordViewHolder.kt @@ -11,6 +11,6 @@ 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) + val title: TextView = itemView.findViewById(R.id.title) + val subtitle: TextView = itemView.findViewById(R.id.subtitle) } |
