all: reformat with Spotless again

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2021-05-15 12:56:49 +05:30
parent 2fcb285e27
commit 7e2eb2425e
No known key found for this signature in database
GPG key ID: 366D7BBAD1031E80
108 changed files with 1385 additions and 473 deletions

View file

@ -115,7 +115,10 @@ class MigrationsTest {
putBoolean(PreferenceKeys.CLEAR_CLIPBOARD_20X, true) putBoolean(PreferenceKeys.CLEAR_CLIPBOARD_20X, true)
} }
runMigrations(context) runMigrations(context)
assertEquals(true, context.sharedPrefs.getBoolean(PreferenceKeys.CLEAR_CLIPBOARD_HISTORY, false)) assertEquals(
true,
context.sharedPrefs.getBoolean(PreferenceKeys.CLEAR_CLIPBOARD_HISTORY, false)
)
assertFalse(context.sharedPrefs.contains(PreferenceKeys.CLEAR_CLIPBOARD_20X)) assertFalse(context.sharedPrefs.contains(PreferenceKeys.CLEAR_CLIPBOARD_20X))
} }
} }

View file

@ -32,7 +32,9 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
if (BuildConfig.ENABLE_DEBUG_FEATURES || prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false)) { if (BuildConfig.ENABLE_DEBUG_FEATURES ||
prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false)
) {
plant(DebugTree()) plant(DebugTree())
StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build()) StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build()) StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())

View file

@ -199,8 +199,11 @@ object PasswordRepository {
fun getFilesList(path: File?): ArrayList<File> { fun getFilesList(path: File?): ArrayList<File> {
if (path == null || !path.exists()) return ArrayList() if (path == null || !path.exists()) return ArrayList()
val directories = (path.listFiles(FileFilter { pathname -> pathname.isDirectory }) ?: emptyArray()).toList() val directories =
val files = (path.listFiles(FileFilter { pathname -> pathname.extension == "gpg" }) ?: emptyArray()).toList() (path.listFiles(FileFilter { pathname -> pathname.isDirectory }) ?: emptyArray()).toList()
val files =
(path.listFiles(FileFilter { pathname -> pathname.extension == "gpg" }) ?: emptyArray())
.toList()
val items = ArrayList<File>() val items = ArrayList<File>()
items.addAll(directories) items.addAll(directories)
@ -216,7 +219,11 @@ object PasswordRepository {
* @return a list of password items * @return a list of password items
*/ */
@JvmStatic @JvmStatic
fun getPasswords(path: File, rootDir: File, sortOrder: PasswordSortOrder): ArrayList<PasswordItem> { fun getPasswords(
path: File,
rootDir: File,
sortOrder: PasswordSortOrder
): ArrayList<PasswordItem> {
// We need to recover the passwords then parse the files // We need to recover the passwords then parse the files
val passList = getFilesList(path).also { it.sortBy { f -> f.name } } val passList = getFilesList(path).also { it.sortBy { f -> f.name } }
val passwordList = ArrayList<PasswordItem>() val passwordList = ArrayList<PasswordItem>()

View file

@ -55,7 +55,8 @@ class FieldItemAdapter(
notifyDataSetChanged() notifyDataSetChanged()
} }
class FieldItemViewHolder(itemView: View, val binding: ItemFieldBinding) : RecyclerView.ViewHolder(itemView) { class FieldItemViewHolder(itemView: View, val binding: ItemFieldBinding) :
RecyclerView.ViewHolder(itemView) {
fun bind(fieldItem: FieldItem, showPassword: Boolean, copyTextToClipBoard: (String?) -> Unit) { fun bind(fieldItem: FieldItem, showPassword: Boolean, copyTextToClipBoard: (String?) -> Unit) {
with(binding) { with(binding) {
@ -66,7 +67,8 @@ class FieldItemAdapter(
when (fieldItem.action) { when (fieldItem.action) {
FieldItem.ActionType.COPY -> { FieldItem.ActionType.COPY -> {
itemTextContainer.apply { itemTextContainer.apply {
endIconDrawable = ContextCompat.getDrawable(itemView.context, R.drawable.ic_content_copy) endIconDrawable =
ContextCompat.getDrawable(itemView.context, R.drawable.ic_content_copy)
endIconMode = TextInputLayout.END_ICON_CUSTOM endIconMode = TextInputLayout.END_ICON_CUSTOM
setEndIconOnClickListener { copyTextToClipBoard(itemText.text.toString()) } setEndIconOnClickListener { copyTextToClipBoard(itemText.text.toString()) }
} }

View file

@ -35,7 +35,9 @@ open class PasswordItemRecyclerAdapter :
return super.onItemClicked(listener) as PasswordItemRecyclerAdapter return super.onItemClicked(listener) as PasswordItemRecyclerAdapter
} }
override fun onSelectionChanged(listener: (selection: Selection<String>) -> Unit): PasswordItemRecyclerAdapter { override fun onSelectionChanged(
listener: (selection: Selection<String>) -> Unit
): PasswordItemRecyclerAdapter {
return super.onSelectionChanged(listener) as PasswordItemRecyclerAdapter return super.onSelectionChanged(listener) as PasswordItemRecyclerAdapter
} }
@ -59,7 +61,8 @@ open class PasswordItemRecyclerAdapter :
name.text = spannable name.text = spannable
if (item.type == PasswordItem.TYPE_CATEGORY) { if (item.type == PasswordItem.TYPE_CATEGORY) {
folderIndicator.visibility = View.VISIBLE folderIndicator.visibility = View.VISIBLE
val count = item.file.listFiles { path -> path.isDirectory || path.extension == "gpg" }?.size ?: 0 val count =
item.file.listFiles { path -> path.isDirectory || path.extension == "gpg" }?.size ?: 0
childCount.visibility = if (count > 0) View.VISIBLE else View.GONE childCount.visibility = if (count > 0) View.VISIBLE else View.GONE
childCount.text = "$count" childCount.text = "$count"
} else { } else {
@ -74,7 +77,8 @@ open class PasswordItemRecyclerAdapter :
} }
} }
class PasswordItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<String>() { class PasswordItemDetailsLookup(private val recyclerView: RecyclerView) :
ItemDetailsLookup<String>() {
override fun getItemDetails(event: MotionEvent): ItemDetails<String>? { override fun getItemDetails(event: MotionEvent): ItemDetails<String>? {
val view = recyclerView.findChildViewUnder(event.x, event.y) ?: return null val view = recyclerView.findChildViewUnder(event.x, event.y) ?: return null

View file

@ -73,7 +73,12 @@ class AutofillDecryptActivity : AppCompatActivity() {
putExtra(EXTRA_SEARCH_ACTION, false) putExtra(EXTRA_SEARCH_ACTION, false)
putExtra(EXTRA_FILE_PATH, file.absolutePath) putExtra(EXTRA_FILE_PATH, file.absolutePath)
} }
return PendingIntent.getActivity(context, decryptFileRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT) return PendingIntent.getActivity(
context,
decryptFileRequestCode++,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
)
.intentSender .intentSender
} }
} }
@ -124,9 +129,17 @@ class AutofillDecryptActivity : AppCompatActivity() {
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
} else { } else {
val fillInDataset = val fillInDataset =
AutofillResponseBuilder.makeFillInDataset(this@AutofillDecryptActivity, credentials, clientState, action) AutofillResponseBuilder.makeFillInDataset(
this@AutofillDecryptActivity,
credentials,
clientState,
action
)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
setResult(RESULT_OK, Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }) setResult(
RESULT_OK,
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }
)
} }
} }
withContext(Dispatchers.Main) { finish() } withContext(Dispatchers.Main) { finish() }
@ -137,7 +150,11 @@ class AutofillDecryptActivity : AppCompatActivity() {
super.onDestroy() super.onDestroy()
} }
private suspend fun executeOpenPgpApi(data: Intent, input: InputStream, output: OutputStream): Intent? { private suspend fun executeOpenPgpApi(
data: Intent,
input: InputStream,
output: OutputStream
): Intent? {
var openPgpServiceConnection: OpenPgpServiceConnection? = null var openPgpServiceConnection: OpenPgpServiceConnection? = null
val openPgpService = val openPgpService =
suspendCoroutine<IOpenPgpService2> { cont -> suspendCoroutine<IOpenPgpService2> { cont ->
@ -177,7 +194,9 @@ class AutofillDecryptActivity : AppCompatActivity() {
return null return null
} }
.onSuccess { result -> .onSuccess { result ->
return when (val resultCode = result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { return when (val resultCode =
result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)
) {
OpenPgpApi.RESULT_CODE_SUCCESS -> { OpenPgpApi.RESULT_CODE_SUCCESS -> {
runCatching { runCatching {
val entry = val entry =
@ -185,7 +204,12 @@ class AutofillDecryptActivity : AppCompatActivity() {
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
passwordEntryFactory.create(lifecycleScope, decryptedOutput.toByteArray()) passwordEntryFactory.create(lifecycleScope, decryptedOutput.toByteArray())
} }
AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure) AutofillPreferences.credentialsFromStoreEntry(
this,
file,
entry,
directoryStructure
)
} }
.getOrElse { e -> .getOrElse { e ->
e(e) { "Failed to parse password entry" } e(e) { "Failed to parse password entry" }
@ -193,7 +217,8 @@ class AutofillDecryptActivity : AppCompatActivity() {
} }
} }
OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
val pendingIntent: PendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!! val pendingIntent: PendingIntent =
result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!!
runCatching { runCatching {
val intentToResume = val intentToResume =
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -215,10 +240,16 @@ class AutofillDecryptActivity : AppCompatActivity() {
val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR) val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR)
if (error != null) { if (error != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(applicationContext, "Error from OpenKeyChain: ${error.message}", Toast.LENGTH_LONG) Toast.makeText(
applicationContext,
"Error from OpenKeyChain: ${error.message}",
Toast.LENGTH_LONG
)
.show() .show()
} }
e { "OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}" } e {
"OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}"
}
} }
null null
} }

View file

@ -46,11 +46,16 @@ class AutofillFilterView : AppCompatActivity() {
private const val HEIGHT_PERCENTAGE = 0.9 private const val HEIGHT_PERCENTAGE = 0.9
private const val WIDTH_PERCENTAGE = 0.75 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_WEB =
private const val EXTRA_FORM_ORIGIN_APP = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_APP" "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 private var matchAndDecryptFileRequestCode = 1
fun makeMatchAndDecryptFileIntentSender(context: Context, formOrigin: FormOrigin): IntentSender { fun makeMatchAndDecryptFileIntentSender(
context: Context,
formOrigin: FormOrigin
): IntentSender {
val intent = val intent =
Intent(context, AutofillFilterView::class.java).apply { Intent(context, AutofillFilterView::class.java).apply {
when (formOrigin) { when (formOrigin) {
@ -108,7 +113,9 @@ class AutofillFilterView : AppCompatActivity() {
FormOrigin.App(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_APP)!!) FormOrigin.App(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_APP)!!)
} }
else -> { else -> {
e { "AutofillFilterActivity started without EXTRA_FORM_ORIGIN_WEB or EXTRA_FORM_ORIGIN_APP" } e {
"AutofillFilterActivity started without EXTRA_FORM_ORIGIN_WEB or EXTRA_FORM_ORIGIN_APP"
}
finish() finish()
return return
} }
@ -125,7 +132,8 @@ class AutofillFilterView : AppCompatActivity() {
with(binding) { with(binding) {
rvPassword.apply { rvPassword.apply {
adapter = adapter =
SearchableRepositoryAdapter(R.layout.oreo_autofill_filter_row, ::PasswordViewHolder) { item -> SearchableRepositoryAdapter(R.layout.oreo_autofill_filter_row, ::PasswordViewHolder) {
item ->
val file = item.file.relativeTo(item.rootDir) val file = item.file.relativeTo(item.rootDir)
val pathToIdentifier = directoryStructure.getPathToIdentifierFor(file) val pathToIdentifier = directoryStructure.getPathToIdentifierFor(file)
val identifier = directoryStructure.getIdentifierFor(file) val identifier = directoryStructure.getIdentifierFor(file)
@ -171,10 +179,15 @@ class AutofillFilterView : AppCompatActivity() {
setOnCheckedChangeListener { _, _ -> updateSearch() } setOnCheckedChangeListener { _, _ -> updateSearch() }
} }
shouldMatch.text = shouldMatch.text =
getString(R.string.oreo_autofill_match_with, formOrigin.getPrettyIdentifier(applicationContext)) getString(
R.string.oreo_autofill_match_with,
formOrigin.getPrettyIdentifier(applicationContext)
)
model.searchResult.observe(this@AutofillFilterView) { result -> model.searchResult.observe(this@AutofillFilterView) { result ->
val list = result.passwordItems val list = result.passwordItems
(rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) { rvPassword.scrollToPosition(0) } (rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) {
rvPassword.scrollToPosition(0)
}
// Switch RecyclerView out for a "no results" message if the new list is empty and // Switch RecyclerView out for a "no results" message if the new list is empty and
// the message is not yet shown (and vice versa). // the message is not yet shown (and vice versa).
if ((list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) || if ((list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) ||
@ -189,16 +202,21 @@ class AutofillFilterView : AppCompatActivity() {
private fun updateSearch() { private fun updateSearch() {
model.search( model.search(
binding.search.text.toString().trim(), binding.search.text.toString().trim(),
filterMode = if (binding.strictDomainSearch.isChecked) FilterMode.StrictDomain else FilterMode.Fuzzy, filterMode =
if (binding.strictDomainSearch.isChecked) FilterMode.StrictDomain else FilterMode.Fuzzy,
searchMode = SearchMode.RecursivelyInSubdirectories, searchMode = SearchMode.RecursivelyInSubdirectories,
listMode = ListMode.FilesOnly listMode = ListMode.FilesOnly
) )
} }
private fun decryptAndFill(item: PasswordItem) { private fun decryptAndFill(item: PasswordItem) {
if (binding.shouldClear.isChecked) AutofillMatcher.clearMatchesFor(applicationContext, formOrigin) if (binding.shouldClear.isChecked)
if (binding.shouldMatch.isChecked) AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file) AutofillMatcher.clearMatchesFor(applicationContext, formOrigin)
if (binding.shouldMatch.isChecked)
AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file)
// intent?.extras? is checked to be non-null in onCreate // intent?.extras? is checked to be non-null in onCreate
decryptAction.launch(AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this)) decryptAction.launch(
AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this)
)
} }
} }

View file

@ -83,9 +83,15 @@ class AutofillPublisherChangedActivity : AppCompatActivity() {
resetButton.visibility = View.VISIBLE resetButton.visibility = View.VISIBLE
} }
resetButton.setOnClickListener { resetButton.setOnClickListener {
AutofillMatcher.clearMatchesFor(this@AutofillPublisherChangedActivity, FormOrigin.App(appPackage)) AutofillMatcher.clearMatchesFor(
this@AutofillPublisherChangedActivity,
FormOrigin.App(appPackage)
)
val fillResponse = intent.getParcelableExtra<FillResponse>(EXTRA_FILL_RESPONSE_AFTER_RESET) val fillResponse = intent.getParcelableExtra<FillResponse>(EXTRA_FILL_RESPONSE_AFTER_RESET)
setResult(RESULT_OK, Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) }) setResult(
RESULT_OK,
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) }
)
finish() finish()
} }
} }
@ -96,13 +102,18 @@ class AutofillPublisherChangedActivity : AppCompatActivity() {
with(binding) { with(binding) {
val packageInfo = packageManager.getPackageInfo(appPackage, PackageManager.GET_META_DATA) val packageInfo = packageManager.getPackageInfo(appPackage, PackageManager.GET_META_DATA)
val installTime = DateUtils.getRelativeTimeSpanString(packageInfo.firstInstallTime) val installTime = DateUtils.getRelativeTimeSpanString(packageInfo.firstInstallTime)
warningAppInstallDate.text = getString(R.string.oreo_autofill_warning_publisher_install_time, installTime) warningAppInstallDate.text =
getString(R.string.oreo_autofill_warning_publisher_install_time, installTime)
val appInfo = packageManager.getApplicationInfo(appPackage, PackageManager.GET_META_DATA) val appInfo = packageManager.getApplicationInfo(appPackage, PackageManager.GET_META_DATA)
warningAppName.text = "${packageManager.getApplicationLabel(appInfo)}" warningAppName.text = "${packageManager.getApplicationLabel(appInfo)}"
val currentHash = computeCertificatesHash(this@AutofillPublisherChangedActivity, appPackage) val currentHash = computeCertificatesHash(this@AutofillPublisherChangedActivity, appPackage)
warningAppAdvancedInfo.text = warningAppAdvancedInfo.text =
getString(R.string.oreo_autofill_warning_publisher_advanced_info_template, appPackage, currentHash) getString(
R.string.oreo_autofill_warning_publisher_advanced_info_template,
appPackage,
currentHash
)
} }
} }
.onFailure { e -> .onFailure { e ->

View file

@ -34,13 +34,20 @@ class AutofillSaveActivity : AppCompatActivity() {
private const val EXTRA_FOLDER_NAME = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FOLDER_NAME" 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_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_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_APP =
private const val EXTRA_SHOULD_MATCH_WEB = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_WEB" "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP"
private const val EXTRA_GENERATE_PASSWORD = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_GENERATE_PASSWORD" 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 { fun makeSaveIntentSender(
context: Context,
credentials: Credentials?,
formOrigin: FormOrigin
): IntentSender {
val identifier = formOrigin.getPrettyIdentifier(context, untrusted = false) val identifier = formOrigin.getPrettyIdentifier(context, untrusted = false)
// Prevent directory traversals // Prevent directory traversals
val sanitizedIdentifier = val sanitizedIdentifier =
@ -52,7 +59,11 @@ class AutofillSaveActivity : AppCompatActivity() {
sanitizedIdentifier = sanitizedIdentifier, sanitizedIdentifier = sanitizedIdentifier,
username = credentials?.username username = credentials?.username
) )
val fileName = directoryStructure.getSaveFileName(username = credentials?.username, identifier = identifier) val fileName =
directoryStructure.getSaveFileName(
username = credentials?.username,
identifier = identifier
)
val intent = val intent =
Intent(context, AutofillSaveActivity::class.java).apply { Intent(context, AutofillSaveActivity::class.java).apply {
putExtras( putExtras(
@ -60,13 +71,20 @@ class AutofillSaveActivity : AppCompatActivity() {
EXTRA_FOLDER_NAME to folderName, EXTRA_FOLDER_NAME to folderName,
EXTRA_NAME to fileName, EXTRA_NAME to fileName,
EXTRA_PASSWORD to credentials?.password, EXTRA_PASSWORD to credentials?.password,
EXTRA_SHOULD_MATCH_APP to formOrigin.identifier.takeIf { formOrigin is FormOrigin.App }, EXTRA_SHOULD_MATCH_APP to
EXTRA_SHOULD_MATCH_WEB to formOrigin.identifier.takeIf { formOrigin is FormOrigin.Web }, 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) EXTRA_GENERATE_PASSWORD to (credentials == null)
) )
) )
} }
return PendingIntent.getActivity(context, saveRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT) return PendingIntent.getActivity(
context,
saveRequestCode++,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
)
.intentSender .intentSender
} }
} }
@ -94,7 +112,8 @@ class AutofillSaveActivity : AppCompatActivity() {
"FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath, "FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath,
PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME), PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD), PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to
intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
) )
) )
} }
@ -117,8 +136,15 @@ class AutofillSaveActivity : AppCompatActivity() {
} }
val credentials = Credentials(username, password, null) val credentials = Credentials(username, password, null)
val fillInDataset = val fillInDataset =
AutofillResponseBuilder.makeFillInDataset(this, credentials, clientState, AutofillAction.Generate) AutofillResponseBuilder.makeFillInDataset(
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) } this,
credentials,
clientState,
AutofillAction.Generate
)
Intent().apply {
putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset)
}
} else { } else {
// Password was extracted from a form, there is nothing to fill. // Password was extracted from a form, there is nothing to fill.
Intent() Intent()

View file

@ -157,7 +157,10 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
return return
} else { } else {
previousListener = null previousListener = null
serviceConnection = OpenPgpServiceConnection(this, OPENPGP_PROVIDER, onBoundListener).also { it.bindToService() } serviceConnection =
OpenPgpServiceConnection(this, OPENPGP_PROVIDER, onBoundListener).also {
it.bindToService()
}
} }
} }
@ -250,7 +253,10 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
fun getParentPath(fullPath: String, repositoryPath: String): String { fun getParentPath(fullPath: String, repositoryPath: String): String {
val relativePath = getRelativePath(fullPath, repositoryPath) val relativePath = getRelativePath(fullPath, repositoryPath)
val index = relativePath.lastIndexOf("/") val index = relativePath.lastIndexOf("/")
return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace("/+".toRegex(), "/") return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace(
"/+".toRegex(),
"/"
)
} }
/** /path/to/store/social/facebook.gpg -> social/facebook */ /** /path/to/store/social/facebook.gpg -> social/facebook */

View file

@ -44,7 +44,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
private val binding by viewBinding(DecryptLayoutBinding::inflate) private val binding by viewBinding(DecryptLayoutBinding::inflate)
@Inject lateinit var passwordEntryFactory: PasswordEntryFactory @Inject lateinit var passwordEntryFactory: PasswordEntryFactory
private val relativeParentPath by lazy(LazyThreadSafetyMode.NONE) { getParentPath(fullPath, repoPath) } private val relativeParentPath by lazy(LazyThreadSafetyMode.NONE) {
getParentPath(fullPath, repoPath)
}
private var passwordEntry: PasswordEntry? = null private var passwordEntry: PasswordEntry? = null
private val userInteractionRequiredResult = private val userInteractionRequiredResult =
@ -136,7 +138,10 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
intent.putExtra("REPO_PATH", repoPath) intent.putExtra("REPO_PATH", repoPath)
intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name) intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name)
intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password) intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password)
intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContentWithoutAuthData) intent.putExtra(
PasswordCreationActivity.EXTRA_EXTRA_CONTENT,
passwordEntry?.extraContentWithoutAuthData
)
intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true) intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true)
startActivity(intent) startActivity(intent)
finish() finish()
@ -150,7 +155,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
type = "text/plain" type = "text/plain"
} }
// Always show a picker to give the user a chance to cancel // Always show a picker to give the user a chance to cancel
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to))) startActivity(
Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to))
)
} }
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
@ -166,7 +173,10 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO) { checkNotNull(api).executeApi(data, inputStream, outputStream) } val result =
withContext(Dispatchers.IO) {
checkNotNull(api).executeApi(data, inputStream, outputStream)
}
when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
OpenPgpApi.RESULT_CODE_SUCCESS -> { OpenPgpApi.RESULT_CODE_SUCCESS -> {
startAutoDismissTimer() startAutoDismissTimer()
@ -174,7 +184,8 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true) val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true)
val entry = passwordEntryFactory.create(lifecycleScope, outputStream.toByteArray()) val entry = passwordEntryFactory.create(lifecycleScope, outputStream.toByteArray())
val items = arrayListOf<FieldItem>() val items = arrayListOf<FieldItem>()
val adapter = FieldItemAdapter(emptyList(), showPassword) { text -> copyTextToClipboard(text) } val adapter =
FieldItemAdapter(emptyList(), showPassword) { text -> copyTextToClipboard(text) }
if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) { if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) {
copyPasswordToClipboard(entry.password) copyPasswordToClipboard(entry.password)
@ -190,7 +201,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
if (entry.hasTotp()) { if (entry.hasTotp()) {
launch { launch {
items.add(FieldItem.createOtpField(entry.totp.value)) items.add(FieldItem.createOtpField(entry.totp.value))
entry.totp.collect { code -> withContext(Dispatchers.Main) { adapter.updateOTPCode(code) } } entry.totp.collect { code ->
withContext(Dispatchers.Main) { adapter.updateOTPCode(code) }
}
} }
} }
@ -198,7 +211,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
items.add(FieldItem.createUsernameField(entry.username!!)) items.add(FieldItem.createUsernameField(entry.username!!))
} }
entry.extraContent.forEach { (key, value) -> items.add(FieldItem(key, value, FieldItem.ActionType.COPY)) } entry.extraContent.forEach { (key, value) ->
items.add(FieldItem(key, value, FieldItem.ActionType.COPY))
}
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
adapter.updateItems(items) adapter.updateItems(items)

View file

@ -55,7 +55,9 @@ class GetKeyIdsActivity : BasePgpActivity() {
OpenPgpApi.RESULT_CODE_SUCCESS -> { OpenPgpApi.RESULT_CODE_SUCCESS -> {
runCatching { runCatching {
val ids = val ids =
result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)?.map { OpenPgpUtils.convertKeyIdToHex(it) } result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)?.map {
OpenPgpUtils.convertKeyIdToHex(it)
}
?: emptyList() ?: emptyList()
val keyResult = Intent().putExtra(OpenPgpApi.EXTRA_KEY_IDS, ids.toTypedArray()) val keyResult = Intent().putExtra(OpenPgpApi.EXTRA_KEY_IDS, ids.toTypedArray())
setResult(RESULT_OK, keyResult) setResult(RESULT_OK, keyResult)

View file

@ -63,14 +63,24 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
private val binding by viewBinding(PasswordCreationActivityBinding::inflate) private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
@Inject lateinit var passwordEntryFactory: PasswordEntryFactory @Inject lateinit var passwordEntryFactory: PasswordEntryFactory
private val suggestedName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) } private val suggestedName by lazy(LazyThreadSafetyMode.NONE) {
private val suggestedPass by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_PASSWORD) } intent.getStringExtra(EXTRA_FILE_NAME)
private val suggestedExtra by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_EXTRA_CONTENT) } }
private val suggestedPass by lazy(LazyThreadSafetyMode.NONE) {
intent.getStringExtra(EXTRA_PASSWORD)
}
private val suggestedExtra by lazy(LazyThreadSafetyMode.NONE) {
intent.getStringExtra(EXTRA_EXTRA_CONTENT)
}
private val shouldGeneratePassword by lazy(LazyThreadSafetyMode.NONE) { private val shouldGeneratePassword by lazy(LazyThreadSafetyMode.NONE) {
intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
} }
private val editing by lazy(LazyThreadSafetyMode.NONE) { intent.getBooleanExtra(EXTRA_EDITING, false) } private val editing by lazy(LazyThreadSafetyMode.NONE) {
private val oldFileName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) } intent.getBooleanExtra(EXTRA_EDITING, false)
}
private val oldFileName by lazy(LazyThreadSafetyMode.NONE) {
intent.getStringExtra(EXTRA_FILE_NAME)
}
private var oldCategory: String? = null private var oldCategory: String? = null
private var copy: Boolean = false private var copy: Boolean = false
private var encryptionIntent: Intent = Intent() private var encryptionIntent: Intent = Intent()
@ -99,7 +109,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data) val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data)
val contents = "${intentResult.contents}\n" val contents = "${intentResult.contents}\n"
val currentExtras = binding.extraContent.text.toString() val currentExtras = binding.extraContent.text.toString()
if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') binding.extraContent.append("\n$contents") if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
binding.extraContent.append("\n$contents")
else binding.extraContent.append(contents) else binding.extraContent.append(contents)
snackbar(message = getString(R.string.otp_import_success)) snackbar(message = getString(R.string.otp_import_success))
} else { } else {
@ -113,18 +124,27 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds ->
lifecycleScope.launch { lifecycleScope.launch {
val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id") val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id")
withContext(Dispatchers.IO) { gpgIdentifierFile.writeText((keyIds + "").joinToString("\n")) } withContext(Dispatchers.IO) {
gpgIdentifierFile.writeText((keyIds + "").joinToString("\n"))
}
commitChange( commitChange(
getString( getString(
R.string.git_commit_gpg_id, R.string.git_commit_gpg_id,
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) getLongName(
gpgIdentifierFile.parentFile!!.absolutePath,
repoPath,
gpgIdentifierFile.name
)
) )
) )
.onSuccess { encrypt(encryptionIntent) } .onSuccess { encrypt(encryptionIntent) }
} }
} }
} else { } else {
snackbar(message = getString(R.string.gpg_key_select_mandatory), length = Snackbar.LENGTH_LONG) snackbar(
message = getString(R.string.gpg_key_select_mandatory),
length = Snackbar.LENGTH_LONG
)
} }
} }
@ -148,24 +168,31 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
bindToOpenKeychain(this) bindToOpenKeychain(this)
title = if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title) title =
if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title)
with(binding) { with(binding) {
setContentView(root) setContentView(root)
generatePassword.setOnClickListener { generatePassword() } generatePassword.setOnClickListener { generatePassword() }
otpImportButton.setOnClickListener { otpImportButton.setOnClickListener {
supportFragmentManager.setFragmentResultListener(OTP_RESULT_REQUEST_KEY, this@PasswordCreationActivity) { supportFragmentManager.setFragmentResultListener(
requestKey, OTP_RESULT_REQUEST_KEY,
bundle -> this@PasswordCreationActivity
) { requestKey, bundle ->
if (requestKey == OTP_RESULT_REQUEST_KEY) { if (requestKey == OTP_RESULT_REQUEST_KEY) {
val contents = bundle.getString(RESULT) val contents = bundle.getString(RESULT)
val currentExtras = binding.extraContent.text.toString() val currentExtras = binding.extraContent.text.toString()
if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') binding.extraContent.append("\n$contents") if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
binding.extraContent.append("\n$contents")
else binding.extraContent.append(contents) else binding.extraContent.append(contents)
} }
} }
val hasCamera = packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) == true val hasCamera = packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) == true
if (hasCamera) { if (hasCamera) {
val items = arrayOf(getString(R.string.otp_import_qr_code), getString(R.string.otp_import_manual_entry)) val items =
arrayOf(
getString(R.string.otp_import_qr_code),
getString(R.string.otp_import_manual_entry)
)
MaterialAlertDialogBuilder(this@PasswordCreationActivity) MaterialAlertDialogBuilder(this@PasswordCreationActivity)
.setItems(items) { _, index -> .setItems(items) { _, index ->
when (index) { when (index) {
@ -209,7 +236,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
// in the encrypted extras. This only makes sense if the directory structure is // in the encrypted extras. This only makes sense if the directory structure is
// FileBased. // FileBased.
if (suggestedName == null && if (suggestedName == null &&
AutofillPreferences.directoryStructure(this@PasswordCreationActivity) == DirectoryStructure.FileBased AutofillPreferences.directoryStructure(this@PasswordCreationActivity) ==
DirectoryStructure.FileBased
) { ) {
encryptUsername.apply { encryptUsername.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
@ -226,7 +254,10 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
// User wants to disable username encryption, so we extract the // User wants to disable username encryption, so we extract the
// username from the encrypted extras and use it as the filename. // username from the encrypted extras and use it as the filename.
val entry = val entry =
passwordEntryFactory.create(lifecycleScope, "PASSWORD\n${extraContent.text}".encodeToByteArray()) passwordEntryFactory.create(
lifecycleScope,
"PASSWORD\n${extraContent.text}".encodeToByteArray()
)
val username = entry.username val username = entry.username
// username should not be null here by the logic in // username should not be null here by the logic in
@ -239,7 +270,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
} }
} }
} }
listOf(filename, extraContent).forEach { it.doOnTextChanged { _, _, _, _ -> updateViewState() } } listOf(filename, extraContent).forEach {
it.doOnTextChanged { _, _, _, _ -> updateViewState() }
}
} }
suggestedPass?.let { suggestedPass?.let {
password.setText(it) password.setText(it)
@ -279,21 +312,29 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
} }
private fun generatePassword() { private fun generatePassword() {
supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) { requestKey, bundle -> supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) {
requestKey,
bundle ->
if (requestKey == PASSWORD_RESULT_REQUEST_KEY) { if (requestKey == PASSWORD_RESULT_REQUEST_KEY) {
binding.password.setText(bundle.getString(RESULT)) binding.password.setText(bundle.getString(RESULT))
} }
} }
when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) { when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) {
KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator") KEY_PWGEN_TYPE_CLASSIC ->
KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment().show(supportFragmentManager, "xkpwgenerator") PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
KEY_PWGEN_TYPE_XKPASSWD ->
XkPasswordGeneratorDialogFragment().show(supportFragmentManager, "xkpwgenerator")
} }
} }
private fun updateViewState() = private fun updateViewState() =
with(binding) { with(binding) {
// Use PasswordEntry to parse extras for username // Use PasswordEntry to parse extras for username
val entry = passwordEntryFactory.create(lifecycleScope, "PLACEHOLDER\n${extraContent.text}".encodeToByteArray()) val entry =
passwordEntryFactory.create(
lifecycleScope,
"PLACEHOLDER\n${extraContent.text}".encodeToByteArray()
)
encryptUsername.apply { encryptUsername.apply {
if (visibility != View.VISIBLE) return@apply if (visibility != View.VISIBLE) return@apply
val hasUsernameInFileName = filename.text.toString().isNotBlank() val hasUsernameInFileName = filename.text.toString().isNotBlank()
@ -354,14 +395,18 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
} }
} }
if (gpgIdentifiers.isEmpty()) { if (gpgIdentifiers.isEmpty()) {
gpgKeySelectAction.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)) gpgKeySelectAction.launch(
Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)
)
return@with return@with
} }
val keyIds = gpgIdentifiers.filterIsInstance<GpgIdentifier.KeyId>().map { it.id }.toLongArray() val keyIds =
gpgIdentifiers.filterIsInstance<GpgIdentifier.KeyId>().map { it.id }.toLongArray()
if (keyIds.isNotEmpty()) { if (keyIds.isNotEmpty()) {
encryptionIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIds) encryptionIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIds)
} }
val userIds = gpgIdentifiers.filterIsInstance<GpgIdentifier.UserId>().map { it.email }.toTypedArray() val userIds =
gpgIdentifiers.filterIsInstance<GpgIdentifier.UserId>().map { it.email }.toTypedArray()
if (userIds.isNotEmpty()) { if (userIds.isNotEmpty()) {
encryptionIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds) encryptionIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds)
} }
@ -396,7 +441,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
val result = val result =
withContext(Dispatchers.IO) { checkNotNull(api).executeApi(encryptionIntent, inputStream, outputStream) } withContext(Dispatchers.IO) {
checkNotNull(api).executeApi(encryptionIntent, inputStream, outputStream)
}
when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
OpenPgpApi.RESULT_CODE_SUCCESS -> { OpenPgpApi.RESULT_CODE_SUCCESS -> {
runCatching { runCatching {
@ -405,7 +452,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
// Additionally, if we were editing and the incoming and outgoing // Additionally, if we were editing and the incoming and outgoing
// filenames differ, it means we renamed. Ensure that the target // filenames differ, it means we renamed. Ensure that the target
// doesn't already exist to prevent an accidental overwrite. // doesn't already exist to prevent an accidental overwrite.
if ((!editing || (editing && suggestedName != file.nameWithoutExtension)) && file.exists()) { if ((!editing || (editing && suggestedName != file.nameWithoutExtension)) &&
file.exists()
) {
snackbar(message = getString(R.string.password_creation_duplicate_error)) snackbar(message = getString(R.string.password_creation_duplicate_error))
return@runCatching return@runCatching
} }
@ -415,7 +464,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
return@runCatching return@runCatching
} }
withContext(Dispatchers.IO) { file.outputStream().use { it.write(outputStream.toByteArray()) } } withContext(Dispatchers.IO) {
file.outputStream().use { it.write(outputStream.toByteArray()) }
}
// associate the new password name with the last name's timestamp in // associate the new password name with the last name's timestamp in
// history // history
@ -432,7 +483,10 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
val returnIntent = Intent() val returnIntent = Intent()
returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path) returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path)
returnIntent.putExtra(RETURN_EXTRA_NAME, editName) returnIntent.putExtra(RETURN_EXTRA_NAME, editName)
returnIntent.putExtra(RETURN_EXTRA_LONG_NAME, getLongName(fullPath, repoPath, editName)) returnIntent.putExtra(
RETURN_EXTRA_LONG_NAME,
getLongName(fullPath, repoPath, editName)
)
if (shouldGeneratePassword) { if (shouldGeneratePassword) {
val directoryStructure = AutofillPreferences.directoryStructure(applicationContext) val directoryStructure = AutofillPreferences.directoryStructure(applicationContext)
@ -442,13 +496,18 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
returnIntent.putExtra(RETURN_EXTRA_USERNAME, username) returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
} }
if (directoryInputLayout.isVisible && directoryInputLayout.isEnabled && oldFileName != null) { if (directoryInputLayout.isVisible &&
directoryInputLayout.isEnabled &&
oldFileName != null
) {
val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg") val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg")
if (oldFile.path != file.path && !oldFile.delete()) { if (oldFile.path != file.path && !oldFile.delete()) {
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
MaterialAlertDialogBuilder(this@PasswordCreationActivity) MaterialAlertDialogBuilder(this@PasswordCreationActivity)
.setTitle(R.string.password_creation_file_fail_title) .setTitle(R.string.password_creation_file_fail_title)
.setMessage(getString(R.string.password_creation_file_delete_fail_message, oldFileName)) .setMessage(
getString(R.string.password_creation_file_delete_fail_message, oldFileName)
)
.setCancelable(false) .setCancelable(false)
.setPositiveButton(android.R.string.ok) { _, _ -> finish() } .setPositiveButton(android.R.string.ok) { _, _ -> finish() }
.show() .show()
@ -456,9 +515,12 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
} }
} }
val commitMessageRes = if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text val commitMessageRes =
if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text
lifecycleScope.launch { lifecycleScope.launch {
commitChange(resources.getString(commitMessageRes, getLongName(fullPath, repoPath, editName))) commitChange(
resources.getString(commitMessageRes, getLongName(fullPath, repoPath, editName))
)
.onSuccess { .onSuccess {
setResult(RESULT_OK, returnIntent) setResult(RESULT_OK, returnIntent)
finish() finish()

View file

@ -52,7 +52,11 @@ private constructor(
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
if (savedInstanceState != null) dismiss() if (savedInstanceState != null) dismiss()
return layoutInflater.inflate(R.layout.basic_bottom_sheet, container, false) return layoutInflater.inflate(R.layout.basic_bottom_sheet, container, false)
} }
@ -85,7 +89,9 @@ private constructor(
} }
if (negativeButtonClickListener != null) { if (negativeButtonClickListener != null) {
binding.bottomSheetCancelButton.isVisible = true binding.bottomSheetCancelButton.isVisible = true
negativeButtonLabel?.let { buttonLbl -> binding.bottomSheetCancelButton.text = buttonLbl } negativeButtonLabel?.let { buttonLbl ->
binding.bottomSheetCancelButton.text = buttonLbl
}
binding.bottomSheetCancelButton.setOnClickListener { binding.bottomSheetCancelButton.setOnClickListener {
negativeButtonClickListener.onClick(it) negativeButtonClickListener.onClick(it)
dismiss() dismiss()
@ -95,7 +101,9 @@ private constructor(
} }
) )
val gradientDrawable = val gradientDrawable =
GradientDrawable().apply { setColor(requireContext().resolveAttribute(android.R.attr.windowBackground)) } GradientDrawable().apply {
setColor(requireContext().resolveAttribute(android.R.attr.windowBackground))
}
view.background = gradientDrawable view.background = gradientDrawable
} }
@ -133,13 +141,19 @@ private constructor(
return this return this
} }
fun setPositiveButtonClickListener(buttonLabel: String? = null, listener: View.OnClickListener): Builder { fun setPositiveButtonClickListener(
buttonLabel: String? = null,
listener: View.OnClickListener
): Builder {
this.positiveButtonClickListener = listener this.positiveButtonClickListener = listener
this.positiveButtonLabel = buttonLabel this.positiveButtonLabel = buttonLabel
return this return this
} }
fun setNegativeButtonClickListener(buttonLabel: String? = null, listener: View.OnClickListener): Builder { fun setNegativeButtonClickListener(
buttonLabel: String? = null,
listener: View.OnClickListener
): Builder {
this.negativeButtonClickListener = listener this.negativeButtonClickListener = listener
this.negativeButtonLabel = buttonLabel this.negativeButtonLabel = buttonLabel
return this return this

View file

@ -37,7 +37,11 @@ class ItemCreationBottomSheet : BottomSheetDialogFragment() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
if (savedInstanceState != null) dismiss() if (savedInstanceState != null) dismiss()
return inflater.inflate(R.layout.item_create_sheet, container, false) return inflater.inflate(R.layout.item_create_sheet, container, false)
} }
@ -67,7 +71,9 @@ class ItemCreationBottomSheet : BottomSheetDialogFragment() {
} }
) )
val gradientDrawable = val gradientDrawable =
GradientDrawable().apply { setColor(requireContext().resolveAttribute(android.R.attr.windowBackground)) } GradientDrawable().apply {
setColor(requireContext().resolveAttribute(android.R.attr.windowBackground))
}
view.background = gradientDrawable view.background = gradientDrawable
} }

View file

@ -36,7 +36,10 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
val callingActivity = requireActivity() val callingActivity = requireActivity()
val binding = FragmentPwgenBinding.inflate(layoutInflater) val binding = FragmentPwgenBinding.inflate(layoutInflater)
val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf") val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf")
val prefs = requireActivity().applicationContext.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) val prefs =
requireActivity()
.applicationContext
.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
builder.setView(binding.root) builder.setView(binding.root)
@ -65,7 +68,9 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
.apply { .apply {
setOnShowListener { setOnShowListener {
generate(binding.passwordText) generate(binding.passwordText)
getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { generate(binding.passwordText) } getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
generate(binding.passwordText)
}
} }
} }
} }

View file

@ -49,7 +49,9 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
binding.xkNumWords.setText(prefs.getString(PREF_KEY_NUM_WORDS, DEFAULT_NUMBER_OF_WORDS)) binding.xkNumWords.setText(prefs.getString(PREF_KEY_NUM_WORDS, DEFAULT_NUMBER_OF_WORDS))
binding.xkSeparator.setText(prefs.getString(PREF_KEY_SEPARATOR, DEFAULT_WORD_SEPARATOR)) binding.xkSeparator.setText(prefs.getString(PREF_KEY_SEPARATOR, DEFAULT_WORD_SEPARATOR))
binding.xkNumberSymbolMask.setText(prefs.getString(PREF_KEY_EXTRA_SYMBOLS_MASK, DEFAULT_EXTRA_SYMBOLS_MASK)) binding.xkNumberSymbolMask.setText(
prefs.getString(PREF_KEY_EXTRA_SYMBOLS_MASK, DEFAULT_EXTRA_SYMBOLS_MASK)
)
binding.xkPasswordText.typeface = monoTypeface binding.xkPasswordText.typeface = monoTypeface
@ -85,8 +87,12 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
.setMinimumWordLength(DEFAULT_MIN_WORD_LENGTH) .setMinimumWordLength(DEFAULT_MIN_WORD_LENGTH)
.setMaximumWordLength(DEFAULT_MAX_WORD_LENGTH) .setMaximumWordLength(DEFAULT_MAX_WORD_LENGTH)
.setSeparator(binding.xkSeparator.text.toString()) .setSeparator(binding.xkSeparator.text.toString())
.appendNumbers(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_DIGIT }) .appendNumbers(
.appendSymbols(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_SYMBOL }) binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_DIGIT }
)
.appendSymbols(
binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_SYMBOL }
)
.setCapitalization(CapsType.valueOf(binding.xkCapType.selectedItem.toString())) .setCapitalization(CapsType.valueOf(binding.xkCapType.selectedItem.toString()))
.create() .create()
.fold( .fold(

View file

@ -24,7 +24,10 @@ class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
passwordList = SelectFolderFragment() passwordList = SelectFolderFragment()
val args = Bundle() val args = Bundle()
args.putString(PasswordStore.REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath) args.putString(
PasswordStore.REQUEST_ARG_PATH,
PasswordRepository.getRepositoryDirectory().absolutePath
)
passwordList.arguments = args passwordList.arguments = args
@ -32,7 +35,9 @@ class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.commit { replace(R.id.pgp_handler_linearlayout, passwordList, PASSWORD_FRAGMENT_TAG) } supportFragmentManager.commit {
replace(R.id.pgp_handler_linearlayout, passwordList, PASSWORD_FRAGMENT_TAG)
}
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View file

@ -35,7 +35,10 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.fab.hide() binding.fab.hide()
recyclerAdapter = PasswordItemRecyclerAdapter().onItemClicked { _, item -> listener.onFragmentInteraction(item) } recyclerAdapter =
PasswordItemRecyclerAdapter().onItemClicked { _, item ->
listener.onFragmentInteraction(item)
}
binding.passRecycler.apply { binding.passRecycler.apply {
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())
itemAnimator = null itemAnimator = null
@ -47,7 +50,9 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH)) val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH))
model.navigateTo(File(path), listMode = ListMode.DirectoriesOnly, pushPreviousLocation = false) model.navigateTo(File(path), listMode = ListMode.DirectoriesOnly, pushPreviousLocation = false)
model.searchResult.observe(viewLifecycleOwner) { result -> recyclerAdapter.submitList(result.passwordItems) } model.searchResult.observe(viewLifecycleOwner) { result ->
recyclerAdapter.submitList(result.passwordItems)
}
} }
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
@ -58,12 +63,16 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
override fun onFragmentInteraction(item: PasswordItem) { override fun onFragmentInteraction(item: PasswordItem) {
if (item.type == PasswordItem.TYPE_CATEGORY) { if (item.type == PasswordItem.TYPE_CATEGORY) {
model.navigateTo(item.file, listMode = ListMode.DirectoriesOnly) model.navigateTo(item.file, listMode = ListMode.DirectoriesOnly)
(requireActivity() as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true) (requireActivity() as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(
true
)
} }
} }
} }
} }
.onFailure { throw ClassCastException("$context must implement OnFragmentInteractionListener") } .onFailure {
throw ClassCastException("$context must implement OnFragmentInteractionListener")
}
} }
val currentDir: File val currentDir: File

View file

@ -116,7 +116,8 @@ abstract class BaseGitActivity : ContinuationContainerActivity() {
"Your local repository appears to be an incomplete Git clone, please delete and re-clone from settings" "Your local repository appears to be an incomplete Git clone, please delete and re-clone from settings"
) )
} }
err is TransportException && err.disconnectReason == DisconnectReason.HOST_KEY_NOT_VERIFIABLE -> { err is TransportException &&
err.disconnectReason == DisconnectReason.HOST_KEY_NOT_VERIFIABLE -> {
SSHException( SSHException(
DisconnectReason.HOST_KEY_NOT_VERIFIABLE, DisconnectReason.HOST_KEY_NOT_VERIFIABLE,
"WARNING: The remote host key has changed. If this is expected, please go to Git server settings and clear the saved host key." "WARNING: The remote host key has changed. If this is expected, please go to Git server settings and clear the saved host key."
@ -135,7 +136,9 @@ abstract class BaseGitActivity : ContinuationContainerActivity() {
private fun isExplicitlyUserInitiatedError(throwable: Throwable): Boolean { private fun isExplicitlyUserInitiatedError(throwable: Throwable): Boolean {
var cause: Throwable? = throwable var cause: Throwable? = throwable
while (cause != null) { while (cause != null) {
if (cause is SSHException && cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER) return true if (cause is SSHException && cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER
)
return true
cause = cause.cause cause = cause.cause
} }
return false return false
@ -154,7 +157,8 @@ abstract class BaseGitActivity : ContinuationContainerActivity() {
while ((rootCause is org.eclipse.jgit.errors.TransportException || while ((rootCause is org.eclipse.jgit.errors.TransportException ||
rootCause is org.eclipse.jgit.api.errors.TransportException || rootCause is org.eclipse.jgit.api.errors.TransportException ||
rootCause is org.eclipse.jgit.api.errors.InvalidRemoteException || rootCause is org.eclipse.jgit.api.errors.InvalidRemoteException ||
(rootCause is UserAuthException && rootCause.message == "Exhausted available authentication methods"))) { (rootCause is UserAuthException &&
rootCause.message == "Exhausted available authentication methods"))) {
rootCause = rootCause.cause ?: break rootCause = rootCause.cause ?: break
} }
return rootCause return rootCause

View file

@ -55,7 +55,12 @@ class GitConfigActivity : BaseGitActivity() {
} else { } else {
GitSettings.authorEmail = email GitSettings.authorEmail = email
GitSettings.authorName = name GitSettings.authorName = name
Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show() Snackbar.make(
binding.root,
getString(R.string.git_server_config_save_success),
Snackbar.LENGTH_SHORT
)
.show()
Handler(Looper.getMainLooper()).postDelayed(500) { finish() } Handler(Looper.getMainLooper()).postDelayed(500) { finish() }
} }
} }
@ -77,7 +82,8 @@ class GitConfigActivity : BaseGitActivity() {
if (repo != null) { if (repo != null) {
binding.gitHeadStatus.text = headStatusMsg(repo) binding.gitHeadStatus.text = headStatusMsg(repo)
// enable the abort button only if we're rebasing or merging // enable the abort button only if we're rebasing or merging
val needsAbort = repo.repositoryState.isRebasing || repo.repositoryState == RepositoryState.MERGING val needsAbort =
repo.repositoryState.isRebasing || repo.repositoryState == RepositoryState.MERGING
binding.gitAbortRebase.isEnabled = needsAbort binding.gitAbortRebase.isEnabled = needsAbort
binding.gitAbortRebase.alpha = if (needsAbort) 1.0f else 0.5f binding.gitAbortRebase.alpha = if (needsAbort) 1.0f else 0.5f
} }

View file

@ -89,7 +89,12 @@ class GitServerConfigActivity : BaseGitActivity() {
binding.clearHostKeyButton.isVisible = GitSettings.hasSavedHostKey() binding.clearHostKeyButton.isVisible = GitSettings.hasSavedHostKey()
binding.clearHostKeyButton.setOnClickListener { binding.clearHostKeyButton.setOnClickListener {
GitSettings.clearSavedHostKey() GitSettings.clearSavedHostKey()
Snackbar.make(binding.root, getString(R.string.clear_saved_host_key_success), Snackbar.LENGTH_LONG).show() Snackbar.make(
binding.root,
getString(R.string.clear_saved_host_key_success),
Snackbar.LENGTH_LONG
)
.show()
it.isVisible = false it.isVisible = false
} }
binding.saveButton.setOnClickListener { binding.saveButton.setOnClickListener {
@ -102,7 +107,9 @@ class GitServerConfigActivity : BaseGitActivity() {
BasicBottomSheet.Builder(this) BasicBottomSheet.Builder(this)
.setTitleRes(R.string.https_scheme_with_port_title) .setTitleRes(R.string.https_scheme_with_port_title)
.setMessageRes(R.string.https_scheme_with_port_message) .setMessageRes(R.string.https_scheme_with_port_message)
.setPositiveButtonClickListener { binding.serverUrl.setText(newUrl.replace(PORT_REGEX, "/")) } .setPositiveButtonClickListener {
binding.serverUrl.setText(newUrl.replace(PORT_REGEX, "/"))
}
.build() .build()
.show(supportFragmentManager, "SSH_SCHEME_WARNING") .show(supportFragmentManager, "SSH_SCHEME_WARNING")
return@setOnClickListener return@setOnClickListener
@ -110,7 +117,9 @@ class GitServerConfigActivity : BaseGitActivity() {
BasicBottomSheet.Builder(this) BasicBottomSheet.Builder(this)
.setTitleRes(R.string.ssh_scheme_needed_title) .setTitleRes(R.string.ssh_scheme_needed_title)
.setMessageRes(R.string.ssh_scheme_needed_message) .setMessageRes(R.string.ssh_scheme_needed_message)
.setPositiveButtonClickListener { @Suppress("SetTextI18n") binding.serverUrl.setText("ssh://$newUrl") } .setPositiveButtonClickListener {
@Suppress("SetTextI18n") binding.serverUrl.setText("ssh://$newUrl")
}
.build() .build()
.show(supportFragmentManager, "SSH_SCHEME_WARNING") .show(supportFragmentManager, "SSH_SCHEME_WARNING")
return@setOnClickListener return@setOnClickListener
@ -133,7 +142,12 @@ class GitServerConfigActivity : BaseGitActivity() {
) )
) { ) {
GitSettings.UpdateConnectionSettingsResult.FailedToParseUrl -> { GitSettings.UpdateConnectionSettingsResult.FailedToParseUrl -> {
Snackbar.make(binding.root, getString(R.string.git_server_config_save_error), Snackbar.LENGTH_LONG).show() Snackbar.make(
binding.root,
getString(R.string.git_server_config_save_error),
Snackbar.LENGTH_LONG
)
.show()
} }
is GitSettings.UpdateConnectionSettingsResult.MissingUsername -> { is GitSettings.UpdateConnectionSettingsResult.MissingUsername -> {
when (updateResult.newProtocol) { when (updateResult.newProtocol) {
@ -154,9 +168,14 @@ class GitServerConfigActivity : BaseGitActivity() {
} }
} }
GitSettings.UpdateConnectionSettingsResult.Valid -> { GitSettings.UpdateConnectionSettingsResult.Valid -> {
if (isClone && PasswordRepository.getRepository(null) == null) PasswordRepository.initialize() if (isClone && PasswordRepository.getRepository(null) == null)
PasswordRepository.initialize()
if (!isClone) { if (!isClone) {
Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT) Snackbar.make(
binding.root,
getString(R.string.git_server_config_save_success),
Snackbar.LENGTH_SHORT
)
.show() .show()
Handler(Looper.getMainLooper()).postDelayed(500) { finish() } Handler(Looper.getMainLooper()).postDelayed(500) { finish() }
} else { } else {
@ -206,7 +225,9 @@ class GitServerConfigActivity : BaseGitActivity() {
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory()) val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory())
val localDirFiles = localDir.listFiles() ?: emptyArray() val localDirFiles = localDir.listFiles() ?: emptyArray()
// Warn if non-empty folder unless it's a just-initialized store that has just a .git folder // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder
if (localDir.exists() && localDirFiles.isNotEmpty() && !(localDirFiles.size == 1 && localDirFiles[0].name == ".git") if (localDir.exists() &&
localDirFiles.isNotEmpty() &&
!(localDirFiles.size == 1 && localDirFiles[0].name == ".git")
) { ) {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_delete_title) .setTitle(R.string.dialog_delete_title)
@ -269,7 +290,9 @@ class GitServerConfigActivity : BaseGitActivity() {
private val PORT_REGEX = ":[0-9]{1,5}/".toRegex() private val PORT_REGEX = ":[0-9]{1,5}/".toRegex()
fun createCloneIntent(context: Context): Intent { fun createCloneIntent(context: Context): Intent {
return Intent(context, GitServerConfigActivity::class.java).apply { putExtra("cloning", true) } return Intent(context, GitServerConfigActivity::class.java).apply {
putExtra("cloning", true)
}
} }
} }
} }

View file

@ -45,7 +45,8 @@ class GitLogAdapter : RecyclerView.Adapter<GitLogAdapter.ViewHolder>() {
override fun getItemCount() = model.size override fun getItemCount() = model.size
class ViewHolder(private val binding: GitLogRowLayoutBinding) : RecyclerView.ViewHolder(binding.root) { class ViewHolder(private val binding: GitLogRowLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(commit: GitCommit) = fun bind(commit: GitCommit) =
with(binding) { with(binding) {

View file

@ -24,7 +24,9 @@ class CloneFragment : Fragment(R.layout.fragment_clone) {
private val binding by viewBinding(FragmentCloneBinding::bind) private val binding by viewBinding(FragmentCloneBinding::bind)
private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } private val settings by lazy(LazyThreadSafetyMode.NONE) {
requireActivity().applicationContext.sharedPrefs
}
private val cloneAction = private val cloneAction =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->

View file

@ -32,7 +32,9 @@ import me.msfjarvis.openpgpktx.util.OpenPgpApi
class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) { class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) {
private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } private val settings by lazy(LazyThreadSafetyMode.NONE) {
requireActivity().applicationContext.sharedPrefs
}
private val binding by viewBinding(FragmentKeySelectionBinding::bind) private val binding by viewBinding(FragmentKeySelectionBinding::bind)
private val gpgKeySelectAction = private val gpgKeySelectAction =
@ -45,13 +47,17 @@ class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) {
gpgIdentifierFile.writeText((keyIds + "").joinToString("\n")) gpgIdentifierFile.writeText((keyIds + "").joinToString("\n"))
} }
settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) }
requireActivity().commitChange(getString(R.string.git_commit_gpg_id, getString(R.string.app_name))) requireActivity()
.commitChange(getString(R.string.git_commit_gpg_id, getString(R.string.app_name)))
} }
} }
finish() finish()
} else { } else {
requireActivity() requireActivity()
.snackbar(message = getString(R.string.gpg_key_select_mandatory), length = Snackbar.LENGTH_LONG) .snackbar(
message = getString(R.string.gpg_key_select_mandatory),
length = Snackbar.LENGTH_LONG
)
} }
} }

View file

@ -35,7 +35,9 @@ import java.io.File
class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) { class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } private val settings by lazy(LazyThreadSafetyMode.NONE) {
requireActivity().applicationContext.sharedPrefs
}
private val directorySelectIntent by lazy(LazyThreadSafetyMode.NONE) { private val directorySelectIntent by lazy(LazyThreadSafetyMode.NONE) {
Intent(requireContext(), DirectorySelectionActivity::class.java) Intent(requireContext(), DirectorySelectionActivity::class.java)
} }
@ -65,7 +67,9 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
externalDirectorySelectAction.launch(directorySelectIntent) externalDirectorySelectAction.launch(directorySelectIntent)
} }
private val repositoryUsePermGrantedAction = createPermGrantedAction { initializeRepositoryInfo() } private val repositoryUsePermGrantedAction = createPermGrantedAction {
initializeRepositoryInfo()
}
private val repositoryChangePermGrantedAction = createPermGrantedAction { private val repositoryChangePermGrantedAction = createPermGrantedAction {
repositoryInitAction.launch(directorySelectIntent) repositoryInitAction.launch(directorySelectIntent)
@ -132,7 +136,12 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
dir.isDirectory && // The directory, is really a directory dir.isDirectory && // The directory, is really a directory
dir.listFilesRecursively().isNotEmpty() && // The directory contains files dir.listFilesRecursively().isNotEmpty() && // The directory contains files
// The directory contains a non-zero number of password files // The directory contains a non-zero number of password files
PasswordRepository.getPasswords(dir, PasswordRepository.getRepositoryDirectory(), sortOrder).isNotEmpty() PasswordRepository.getPasswords(
dir,
PasswordRepository.getRepositoryDirectory(),
sortOrder
)
.isNotEmpty()
) { ) {
PasswordRepository.closeRepository() PasswordRepository.closeRepository()
return true return true

View file

@ -27,6 +27,8 @@ class WelcomeFragment : Fragment(R.layout.fragment_welcome) {
binding.letsGo.setOnClickListener { binding.letsGo.setOnClickListener {
parentFragmentManager.performTransactionWithBackStack(CloneFragment.newInstance()) parentFragmentManager.performTransactionWithBackStack(CloneFragment.newInstance())
} }
binding.settingsButton.setOnClickListener { startActivity(Intent(requireContext(), SettingsActivity::class.java)) } binding.settingsButton.setOnClickListener {
startActivity(Intent(requireContext(), SettingsActivity::class.java))
}
} }
} }

View file

@ -77,8 +77,12 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
settings = requireContext().sharedPrefs settings = requireContext().sharedPrefs
initializePasswordList() initializePasswordList()
binding.fab.setOnClickListener { ItemCreationBottomSheet().show(childFragmentManager, "BOTTOM_SHEET") } binding.fab.setOnClickListener {
childFragmentManager.setFragmentResultListener(ITEM_CREATION_REQUEST_KEY, viewLifecycleOwner) { _, bundle -> ItemCreationBottomSheet().show(childFragmentManager, "BOTTOM_SHEET")
}
childFragmentManager.setFragmentResultListener(ITEM_CREATION_REQUEST_KEY, viewLifecycleOwner) {
_,
bundle ->
when (bundle.getString(ACTION_KEY)) { when (bundle.getString(ACTION_KEY)) {
ACTION_FOLDER -> requireStore().createFolder() ACTION_FOLDER -> requireStore().createFolder()
ACTION_PASSWORD -> requireStore().createPassword() ACTION_PASSWORD -> requireStore().createPassword()
@ -88,7 +92,8 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
private fun initializePasswordList() { private fun initializePasswordList() {
val gitDir = File(PasswordRepository.getRepositoryDirectory(), ".git") val gitDir = File(PasswordRepository.getRepositoryDirectory(), ".git")
val hasGitDir = gitDir.exists() && gitDir.isDirectory && (gitDir.listFiles()?.isNotEmpty() == true) val hasGitDir =
gitDir.exists() && gitDir.isDirectory && (gitDir.listFiles()?.isNotEmpty() == true)
binding.swipeRefresher.setOnRefreshListener { binding.swipeRefresher.setOnRefreshListener {
if (!hasGitDir) { if (!hasGitDir) {
requireStore().refreshPasswordList() requireStore().refreshPasswordList()
@ -118,7 +123,9 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
binding.swipeRefresher.isRefreshing = false binding.swipeRefresher.isRefreshing = false
refreshPasswordList() refreshPasswordList()
}, },
failure = { err -> promptOnErrorHandler(err) { binding.swipeRefresher.isRefreshing = false } }, failure = { err ->
promptOnErrorHandler(err) { binding.swipeRefresher.isRefreshing = false }
},
) )
} }
} }
@ -135,10 +142,16 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
binding.swipeRefresher.isEnabled = selection.isEmpty binding.swipeRefresher.isEnabled = selection.isEmpty
if (actionMode == null) if (actionMode == null)
actionMode = requireStore().startSupportActionMode(actionModeCallback) ?: return@onSelectionChanged actionMode =
requireStore().startSupportActionMode(actionModeCallback) ?: return@onSelectionChanged
if (!selection.isEmpty) { if (!selection.isEmpty) {
actionMode!!.title = resources.getQuantityString(R.plurals.delete_title, selection.size(), selection.size()) actionMode!!.title =
resources.getQuantityString(
R.plurals.delete_title,
selection.size(),
selection.size()
)
actionMode!!.invalidate() actionMode!!.invalidate()
} else { } else {
actionMode!!.finish() actionMode!!.finish()
@ -171,14 +184,18 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
recyclerView.scrollToPosition(0) recyclerView.scrollToPosition(0)
} }
scrollTarget != null -> { scrollTarget != null -> {
scrollTarget?.let { recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it)) } scrollTarget?.let {
recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it))
}
scrollTarget = null scrollTarget = null
} }
else -> { else -> {
// When the result is not filtered and there is a saved scroll position for // When the result is not filtered and there is a saved scroll position for
// it, // it,
// we try to restore it. // we try to restore it.
recyclerViewStateToRestore?.let { recyclerView.layoutManager!!.onRestoreInstanceState(it) } recyclerViewStateToRestore?.let {
recyclerView.layoutManager!!.onRestoreInstanceState(it)
}
recyclerViewStateToRestore = null recyclerViewStateToRestore = null
} }
} }
@ -201,7 +218,8 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
// but may be called multiple times if the mode is invalidated. // but may be called multiple times if the mode is invalidated.
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val selectedItems = recyclerAdapter.getSelectedItems() val selectedItems = recyclerAdapter.getSelectedItems()
menu.findItem(R.id.menu_edit_password).isVisible = selectedItems.all { it.type == PasswordItem.TYPE_CATEGORY } menu.findItem(R.id.menu_edit_password).isVisible =
selectedItems.all { it.type == PasswordItem.TYPE_CATEGORY }
menu.findItem(R.id.menu_pin_password).isVisible = menu.findItem(R.id.menu_pin_password).isVisible =
selectedItems.size == 1 && selectedItems[0].type == PasswordItem.TYPE_PASSWORD selectedItems.size == 1 && selectedItems[0].type == PasswordItem.TYPE_PASSWORD
return true return true
@ -227,7 +245,10 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
} }
R.id.menu_pin_password -> { R.id.menu_pin_password -> {
val passwordItem = recyclerAdapter.getSelectedItems()[0] val passwordItem = recyclerAdapter.getSelectedItems()[0]
shortcutHandler.addPinnedShortcut(passwordItem, passwordItem.createAuthEnabledIntent(requireContext())) shortcutHandler.addPinnedShortcut(
passwordItem,
passwordItem.createAuthEnabledIntent(requireContext())
)
false false
} }
else -> false else -> false
@ -244,7 +265,8 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
private fun animateFab(show: Boolean) = private fun animateFab(show: Boolean) =
with(binding.fab) { with(binding.fab) {
val animation = AnimationUtils.loadAnimation(context, if (show) R.anim.scale_up else R.anim.scale_down) val animation =
AnimationUtils.loadAnimation(context, if (show) R.anim.scale_up else R.anim.scale_down)
animation.setAnimationListener( animation.setAnimationListener(
object : Animation.AnimationListener { object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {} override fun onAnimationRepeat(animation: Animation?) {}
@ -258,7 +280,11 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
} }
} }
) )
animate().rotationBy(if (show) -90f else 90f).setStartDelay(if (show) 100 else 0).setDuration(100).start() animate()
.rotationBy(if (show) -90f else 90f)
.setStartDelay(if (show) 100 else 0)
.setDuration(100)
.start()
startAnimation(animation) startAnimation(animation)
} }
} }
@ -269,10 +295,15 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
listener = listener =
object : OnFragmentInteractionListener { object : OnFragmentInteractionListener {
override fun onFragmentInteraction(item: PasswordItem) { override fun onFragmentInteraction(item: PasswordItem) {
if (settings.getString(PreferenceKeys.SORT_ORDER) == PasswordSortOrder.RECENTLY_USED.name) { if (settings.getString(PreferenceKeys.SORT_ORDER) ==
PasswordSortOrder.RECENTLY_USED.name
) {
// save the time when password was used // save the time when password was used
val preferences = context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) val preferences =
preferences.edit { putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString()) } context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
preferences.edit {
putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString())
}
} }
if (item.type == PasswordItem.TYPE_CATEGORY) { if (item.type == PasswordItem.TYPE_CATEGORY) {
@ -287,7 +318,9 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
} }
} }
} }
.onFailure { throw ClassCastException("$context must implement OnFragmentInteractionListener") } .onFailure {
throw ClassCastException("$context must implement OnFragmentInteractionListener")
}
} }
private fun requireStore() = requireActivity() as PasswordStore private fun requireStore() = requireActivity() as PasswordStore
@ -322,7 +355,10 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
fun navigateTo(file: File) { fun navigateTo(file: File) {
requireStore().clearSearch() requireStore().clearSearch()
model.navigateTo(file, recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState()) model.navigateTo(
file,
recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState()
)
requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true) requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }

View file

@ -82,7 +82,9 @@ class PasswordStore : BaseGitActivity() {
} }
private val storagePermissionRequest = private val storagePermissionRequest =
registerForActivityResult(RequestPermission()) { granted -> if (granted) checkLocalRepository() } registerForActivityResult(RequestPermission()) { granted ->
if (granted) checkLocalRepository()
}
private val directorySelectAction = private val directorySelectAction =
registerForActivityResult(StartActivityForResult()) { result -> registerForActivityResult(StartActivityForResult()) { result ->
@ -128,7 +130,13 @@ class PasswordStore : BaseGitActivity() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(this@PasswordStore) MaterialAlertDialogBuilder(this@PasswordStore)
.setTitle(resources.getString(R.string.password_exists_title)) .setTitle(resources.getString(R.string.password_exists_title))
.setMessage(resources.getString(R.string.password_exists_message, destinationLongName, sourceLongName)) .setMessage(
resources.getString(
R.string.password_exists_message,
destinationLongName,
sourceLongName
)
)
.setPositiveButton(R.string.dialog_ok) { _, _ -> .setPositiveButton(R.string.dialog_ok) { _, _ ->
launch(Dispatchers.IO) { moveFile(source, destinationFile) } launch(Dispatchers.IO) { moveFile(source, destinationFile) }
} }
@ -143,11 +151,16 @@ class PasswordStore : BaseGitActivity() {
1 -> { 1 -> {
val source = File(filesToMove[0]) val source = File(filesToMove[0])
val basename = source.nameWithoutExtension val basename = source.nameWithoutExtension
val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) val sourceLongName =
getLongName(requireNotNull(source.parent), repositoryPath, basename)
val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
commitChange( commitChange(
resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName), resources.getString(
R.string.git_commit_move_text,
sourceLongName,
destinationLongName
),
) )
} }
} }
@ -168,8 +181,8 @@ class PasswordStore : BaseGitActivity() {
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
// open search view on search key, or Ctr+F // open search view on search key, or Ctr+F
if ((keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) && if ((keyCode == KeyEvent.KEYCODE_SEARCH ||
!searchItem.isActionViewExpanded keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) && !searchItem.isActionViewExpanded
) { ) {
searchItem.expandActionView() searchItem.expandActionView()
return true return true
@ -202,7 +215,9 @@ class PasswordStore : BaseGitActivity() {
model.currentDir.observe(this) { dir -> model.currentDir.observe(this) { dir ->
val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile
supportActionBar!!.apply { if (dir != basePath) title = dir.name else setTitle(R.string.app_name) } supportActionBar!!.apply {
if (dir != basePath) title = dir.name else setTitle(R.string.app_name)
}
} }
} }
@ -253,7 +268,8 @@ class PasswordStore : BaseGitActivity() {
val filter = s.trim() val filter = s.trim()
// List the contents of the current directory if the user enters a blank // List the contents of the current directory if the user enters a blank
// search term. // search term.
if (filter.isEmpty()) model.navigateTo(newDirectory = model.currentDir.value!!, pushPreviousLocation = false) if (filter.isEmpty())
model.navigateTo(newDirectory = model.currentDir.value!!, pushPreviousLocation = false)
else model.search(filter) else model.search(filter)
return true return true
} }
@ -288,7 +304,9 @@ class PasswordStore : BaseGitActivity() {
.setPositiveButton(resources.getString(R.string.dialog_ok), null) .setPositiveButton(resources.getString(R.string.dialog_ok), null)
when (id) { when (id) {
R.id.user_pref -> { R.id.user_pref -> {
runCatching { startActivity(Intent(this, SettingsActivity::class.java)) }.onFailure { e -> e.printStackTrace() } runCatching { startActivity(Intent(this, SettingsActivity::class.java)) }.onFailure { e ->
e.printStackTrace()
}
} }
R.id.git_push -> { R.id.git_push -> {
if (!PasswordRepository.isInitialized) { if (!PasswordRepository.isInitialized) {
@ -372,7 +390,8 @@ class PasswordStore : BaseGitActivity() {
if (localDir != null && settings.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)) { if (localDir != null && settings.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)) {
d { "Check, dir: ${localDir.absolutePath}" } d { "Check, dir: ${localDir.absolutePath}" }
// do not push the fragment if we already have it // do not push the fragment if we already have it
if (getPasswordFragment() == null || settings.getBoolean(PreferenceKeys.REPO_CHANGED, false)) { if (getPasswordFragment() == null || settings.getBoolean(PreferenceKeys.REPO_CHANGED, false)
) {
settings.edit { putBoolean(PreferenceKeys.REPO_CHANGED, false) } settings.edit { putBoolean(PreferenceKeys.REPO_CHANGED, false) }
val args = Bundle() val args = Bundle()
args.putString(REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath) args.putString(REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath)
@ -403,7 +422,9 @@ class PasswordStore : BaseGitActivity() {
fun decryptPassword(item: PasswordItem) { fun decryptPassword(item: PasswordItem) {
val authDecryptIntent = item.createAuthEnabledIntent(this) val authDecryptIntent = item.createAuthEnabledIntent(this)
val decryptIntent = val decryptIntent =
(authDecryptIntent.clone() as Intent).setComponent(ComponentName(this, DecryptActivity::class.java)) (authDecryptIntent.clone() as Intent).setComponent(
ComponentName(this, DecryptActivity::class.java)
)
startActivity(decryptIntent) startActivity(decryptIntent)
@ -439,7 +460,9 @@ class PasswordStore : BaseGitActivity() {
fun deletePasswords(selectedItems: List<PasswordItem>) { fun deletePasswords(selectedItems: List<PasswordItem>) {
var size = 0 var size = 0
selectedItems.forEach { if (it.file.isFile) size++ else size += it.file.listFilesRecursively().size } selectedItems.forEach {
if (it.file.isFile) size++ else size += it.file.listFilesRecursively().size
}
if (size == 0) { if (size == 0) {
selectedItems.map { item -> item.file.deleteRecursively() } selectedItems.map { item -> item.file.deleteRecursively() }
refreshPasswordList() refreshPasswordList()
@ -497,7 +520,10 @@ class PasswordStore : BaseGitActivity() {
* @see [CategoryRenameError] * @see [CategoryRenameError]
* @see [isInsideRepository] * @see [isInsideRepository]
*/ */
private fun renameCategory(oldCategory: PasswordItem, error: CategoryRenameError = CategoryRenameError.None) { private fun renameCategory(
oldCategory: PasswordItem,
error: CategoryRenameError = CategoryRenameError.None
) {
val view = layoutInflater.inflate(R.layout.folder_dialog_fragment, null) val view = layoutInflater.inflate(R.layout.folder_dialog_fragment, null)
val newCategoryEditText = view.findViewById<TextInputEditText>(R.id.folder_name_text) val newCategoryEditText = view.findViewById<TextInputEditText>(R.id.folder_name_text)
@ -513,16 +539,19 @@ class PasswordStore : BaseGitActivity() {
.setPositiveButton(R.string.dialog_ok) { _, _ -> .setPositiveButton(R.string.dialog_ok) { _, _ ->
val newCategory = File("${oldCategory.file.parent}/${newCategoryEditText.text}") val newCategory = File("${oldCategory.file.parent}/${newCategoryEditText.text}")
when { when {
newCategoryEditText.text.isNullOrBlank() -> renameCategory(oldCategory, CategoryRenameError.EmptyField) newCategoryEditText.text.isNullOrBlank() ->
renameCategory(oldCategory, CategoryRenameError.EmptyField)
newCategory.exists() -> renameCategory(oldCategory, CategoryRenameError.CategoryExists) newCategory.exists() -> renameCategory(oldCategory, CategoryRenameError.CategoryExists)
!newCategory.isInsideRepository() -> renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo) !newCategory.isInsideRepository() ->
renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo)
else -> else ->
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
moveFile(oldCategory.file, newCategory) moveFile(oldCategory.file, newCategory)
// associate the new category with the last category's timestamp in // associate the new category with the last category's timestamp in
// history // history
val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) val preference =
getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
val timestamp = preference.getString(oldCategory.file.absolutePath.base64()) val timestamp = preference.getString(oldCategory.file.absolutePath.base64())
if (timestamp != null) { if (timestamp != null) {
preference.edit { preference.edit {
@ -533,7 +562,11 @@ class PasswordStore : BaseGitActivity() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
commitChange( commitChange(
resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name), resources.getString(
R.string.git_commit_move_text,
oldCategory.name,
newCategory.name
),
) )
} }
} }
@ -584,7 +617,9 @@ class PasswordStore : BaseGitActivity() {
// Recursively list all files (not directories) below `source`, then // Recursively list all files (not directories) below `source`, then
// obtain the corresponding target file by resolving the relative path // obtain the corresponding target file by resolving the relative path
// starting at the destination folder. // starting at the destination folder.
source.listFilesRecursively().associateWith { destinationFile.resolve(it.relativeTo(source)) } source.listFilesRecursively().associateWith {
destinationFile.resolve(it.relativeTo(source))
}
} else { } else {
mapOf(source to destinationFile) mapOf(source to destinationFile)
} }

View file

@ -28,7 +28,9 @@ private val WEB_ADDRESS_REGEX = Patterns.WEB_URL.toRegex()
class ProxySelectorActivity : AppCompatActivity() { class ProxySelectorActivity : AppCompatActivity() {
private val binding by viewBinding(ActivityProxySelectorBinding::inflate) private val binding by viewBinding(ActivityProxySelectorBinding::inflate)
private val proxyPrefs by lazy(LazyThreadSafetyMode.NONE) { applicationContext.getEncryptedProxyPrefs() } private val proxyPrefs by lazy(LazyThreadSafetyMode.NONE) {
applicationContext.getEncryptedProxyPrefs()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -36,7 +38,9 @@ class ProxySelectorActivity : AppCompatActivity() {
with(binding) { with(binding) {
proxyHost.setText(proxyPrefs.getString(PreferenceKeys.PROXY_HOST)) proxyHost.setText(proxyPrefs.getString(PreferenceKeys.PROXY_HOST))
proxyUser.setText(proxyPrefs.getString(PreferenceKeys.PROXY_USERNAME)) proxyUser.setText(proxyPrefs.getString(PreferenceKeys.PROXY_USERNAME))
proxyPrefs.getInt(PreferenceKeys.PROXY_PORT, -1).takeIf { it != -1 }?.let { proxyPort.setText("$it") } proxyPrefs.getInt(PreferenceKeys.PROXY_PORT, -1).takeIf { it != -1 }?.let {
proxyPort.setText("$it")
}
proxyPassword.setText(proxyPrefs.getString(PreferenceKeys.PROXY_PASSWORD)) proxyPassword.setText(proxyPrefs.getString(PreferenceKeys.PROXY_PASSWORD))
save.setOnClickListener { saveSettings() } save.setOnClickListener { saveSettings() }
proxyHost.doOnTextChanged { text, _, _, _ -> proxyHost.doOnTextChanged { text, _, _, _ ->
@ -54,10 +58,18 @@ class ProxySelectorActivity : AppCompatActivity() {
private fun saveSettings() { private fun saveSettings() {
proxyPrefs.edit { proxyPrefs.edit {
binding.proxyHost.text?.toString()?.takeIf { it.isNotEmpty() }.let { GitSettings.proxyHost = it } binding.proxyHost.text?.toString()?.takeIf { it.isNotEmpty() }.let {
binding.proxyUser.text?.toString()?.takeIf { it.isNotEmpty() }.let { GitSettings.proxyUsername = it } GitSettings.proxyHost = it
binding.proxyPort.text?.toString()?.takeIf { it.isNotEmpty() }?.let { GitSettings.proxyPort = it.toInt() } }
binding.proxyPassword.text?.toString()?.takeIf { it.isNotEmpty() }.let { GitSettings.proxyPassword = it } binding.proxyUser.text?.toString()?.takeIf { it.isNotEmpty() }.let {
GitSettings.proxyUsername = it
}
binding.proxyPort.text?.toString()?.takeIf { it.isNotEmpty() }?.let {
GitSettings.proxyPort = it.toInt()
}
binding.proxyPassword.text?.toString()?.takeIf { it.isNotEmpty() }.let {
GitSettings.proxyPassword = it
}
} }
ProxyUtils.setDefaultProxy() ProxyUtils.setDefaultProxy()
Handler(Looper.getMainLooper()).postDelayed(500) { finish() } Handler(Looper.getMainLooper()).postDelayed(500) { finish() }

View file

@ -59,13 +59,18 @@ class AutofillSettings(private val activity: FragmentActivity) : SettingsProvide
val appLabel = it.first val appLabel = it.first
val supportDescription = val supportDescription =
when (it.second) { when (it.second) {
BrowserAutofillSupportLevel.None -> activity.getString(R.string.oreo_autofill_no_support) BrowserAutofillSupportLevel.None ->
BrowserAutofillSupportLevel.FlakyFill -> activity.getString(R.string.oreo_autofill_flaky_fill_support) activity.getString(R.string.oreo_autofill_no_support)
BrowserAutofillSupportLevel.FlakyFill ->
activity.getString(R.string.oreo_autofill_flaky_fill_support)
BrowserAutofillSupportLevel.PasswordFill -> BrowserAutofillSupportLevel.PasswordFill ->
activity.getString(R.string.oreo_autofill_password_fill_support) activity.getString(R.string.oreo_autofill_password_fill_support)
BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility -> BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility ->
activity.getString(R.string.oreo_autofill_password_fill_and_conditional_save_support) activity.getString(
BrowserAutofillSupportLevel.GeneralFill -> activity.getString(R.string.oreo_autofill_general_fill_support) R.string.oreo_autofill_password_fill_and_conditional_save_support
)
BrowserAutofillSupportLevel.GeneralFill ->
activity.getString(R.string.oreo_autofill_general_fill_support)
BrowserAutofillSupportLevel.GeneralFillAndSave -> BrowserAutofillSupportLevel.GeneralFillAndSave ->
activity.getString(R.string.oreo_autofill_general_fill_and_save_support) activity.getString(R.string.oreo_autofill_general_fill_and_save_support)
} }
@ -102,8 +107,10 @@ class AutofillSettings(private val activity: FragmentActivity) : SettingsProvide
false false
} }
} }
val values = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_values) val values =
val titles = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_entries) activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_values)
val titles =
activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_entries)
val items = values.zip(titles).map { SelectionItem(it.first, it.second, null) } val items = values.zip(titles).map { SelectionItem(it.first, it.second, null) }
singleChoice(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE, items) { singleChoice(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE, items) {
initialSelection = DirectoryStructure.DEFAULT.value initialSelection = DirectoryStructure.DEFAULT.value

View file

@ -39,7 +39,9 @@ class DirectorySelectionActivity : AppCompatActivity() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(resources.getString(R.string.sdcard_root_warning_title)) .setTitle(resources.getString(R.string.sdcard_root_warning_title))
.setMessage(resources.getString(R.string.sdcard_root_warning_message)) .setMessage(resources.getString(R.string.sdcard_root_warning_message))
.setPositiveButton(resources.getString(R.string.sdcard_root_warning_remove_everything)) { _, _ -> .setPositiveButton(resources.getString(R.string.sdcard_root_warning_remove_everything)) {
_,
_ ->
prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, uri.path) } prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, uri.path) }
} }
.setNegativeButton(R.string.dialog_cancel, null) .setNegativeButton(R.string.dialog_cancel, null)

View file

@ -26,7 +26,8 @@ class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider
builder.apply { builder.apply {
val themeValues = activity.resources.getStringArray(R.array.app_theme_values) val themeValues = activity.resources.getStringArray(R.array.app_theme_values)
val themeOptions = activity.resources.getStringArray(R.array.app_theme_options) val themeOptions = activity.resources.getStringArray(R.array.app_theme_options)
val themeItems = themeValues.zip(themeOptions).map { SelectionItem(it.first, it.second, null) } val themeItems =
themeValues.zip(themeOptions).map { SelectionItem(it.first, it.second, null) }
singleChoice(PreferenceKeys.APP_THEME, themeItems) { singleChoice(PreferenceKeys.APP_THEME, themeItems) {
initialSelection = activity.resources.getString(R.string.app_theme_def) initialSelection = activity.resources.getString(R.string.app_theme_def)
titleRes = R.string.pref_app_theme_title titleRes = R.string.pref_app_theme_title
@ -64,7 +65,8 @@ class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider
defaultValue = false defaultValue = false
enabled = canAuthenticate enabled = canAuthenticate
summaryRes = summaryRes =
if (canAuthenticate) R.string.pref_biometric_auth_summary else R.string.pref_biometric_auth_summary_error if (canAuthenticate) R.string.pref_biometric_auth_summary
else R.string.pref_biometric_auth_summary_error
onClick { onClick {
enabled = false enabled = false
val isChecked = checked val isChecked = checked

View file

@ -37,7 +37,9 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys
class RepositorySettings(private val activity: FragmentActivity) : SettingsProvider { class RepositorySettings(private val activity: FragmentActivity) : SettingsProvider {
private val encryptedPreferences by lazy(LazyThreadSafetyMode.NONE) { activity.getEncryptedGitPrefs() } private val encryptedPreferences by lazy(LazyThreadSafetyMode.NONE) {
activity.getEncryptedGitPrefs()
}
private fun <T : FragmentActivity> launchActivity(clazz: Class<T>) { private fun <T : FragmentActivity> launchActivity(clazz: Class<T>) {
activity.startActivity(Intent(activity, clazz)) activity.startActivity(Intent(activity, clazz))
@ -47,7 +49,9 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
.setTitle(activity.resources.getString(R.string.external_repository_dialog_title)) .setTitle(activity.resources.getString(R.string.external_repository_dialog_title))
.setMessage(activity.resources.getString(R.string.external_repository_dialog_text)) .setMessage(activity.resources.getString(R.string.external_repository_dialog_text))
.setPositiveButton(R.string.dialog_ok) { _, _ -> launchActivity(DirectorySelectionActivity::class.java) } .setPositiveButton(R.string.dialog_ok) { _, _ ->
launchActivity(DirectorySelectionActivity::class.java)
}
.setNegativeButton(R.string.dialog_cancel, null) .setNegativeButton(R.string.dialog_cancel, null)
.show() .show()
} }
@ -130,7 +134,9 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
} }
pref(PreferenceKeys.SSH_OPENKEYSTORE_CLEAR_KEY_ID) { pref(PreferenceKeys.SSH_OPENKEYSTORE_CLEAR_KEY_ID) {
titleRes = R.string.pref_title_openkeystore_clear_keyid titleRes = R.string.pref_title_openkeystore_clear_keyid
visible = activity.sharedPrefs.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty() ?: false visible =
activity.sharedPrefs.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty()
?: false
onClick { onClick {
activity.sharedPrefs.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null) } activity.sharedPrefs.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null) }
visible = false visible = false
@ -160,7 +166,9 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList())
} }
} }
activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) } activity.sharedPrefs.edit {
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
}
dialogInterface.cancel() dialogInterface.cancel()
activity.finish() activity.finish()
} }

View file

@ -68,7 +68,9 @@ class SettingsActivity : AppCompatActivity() {
getString(subScreen.titleRes) getString(subScreen.titleRes)
} }
} }
savedInstanceState?.getParcelable<PreferencesAdapter.SavedState>("adapter")?.let(adapter::loadSavedState) savedInstanceState
?.getParcelable<PreferencesAdapter.SavedState>("adapter")
?.let(adapter::loadSavedState)
binding.preferenceRecyclerView.adapter = adapter binding.preferenceRecyclerView.adapter = adapter
} }

View file

@ -20,7 +20,9 @@ class ShowSshKeyFragment : DialogFragment() {
return MaterialAlertDialogBuilder(requireActivity()).run { return MaterialAlertDialogBuilder(requireActivity()).run {
setMessage(getString(R.string.ssh_keygen_message, publicKey)) setMessage(getString(R.string.ssh_keygen_message, publicKey))
setTitle(R.string.your_public_key) setTitle(R.string.your_public_key)
setNegativeButton(R.string.ssh_keygen_later) { _, _ -> (activity as? SshKeyGenActivity)?.finish() } setNegativeButton(R.string.ssh_keygen_later) { _, _ ->
(activity as? SshKeyGenActivity)?.finish()
}
setPositiveButton(R.string.ssh_keygen_share) { _, _ -> setPositiveButton(R.string.ssh_keygen_share) { _, _ ->
val sendIntent = val sendIntent =
Intent().apply { Intent().apply {

View file

@ -30,9 +30,15 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
private enum class KeyGenType(val generateKey: suspend (requireAuthentication: Boolean) -> Unit) { private enum class KeyGenType(val generateKey: suspend (requireAuthentication: Boolean) -> Unit) {
Rsa({ requireAuthentication -> SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication) }), Rsa({ requireAuthentication ->
Ecdsa({ requireAuthentication -> SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication) }), SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication)
Ed25519({ requireAuthentication -> SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication) }), }),
Ecdsa({ requireAuthentication ->
SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication)
}),
Ed25519({ requireAuthentication ->
SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication)
}),
} }
class SshKeyGenActivity : AppCompatActivity() { class SshKeyGenActivity : AppCompatActivity() {
@ -50,7 +56,9 @@ class SshKeyGenActivity : AppCompatActivity() {
MaterialAlertDialogBuilder(this@SshKeyGenActivity).run { MaterialAlertDialogBuilder(this@SshKeyGenActivity).run {
setTitle(R.string.ssh_keygen_existing_title) setTitle(R.string.ssh_keygen_existing_title)
setMessage(R.string.ssh_keygen_existing_message) setMessage(R.string.ssh_keygen_existing_message)
setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> lifecycleScope.launch { generate() } } setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ ->
lifecycleScope.launch { generate() }
}
setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> finish() } setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> finish() }
show() show()
} }

View file

@ -26,7 +26,12 @@ class SshKeyImportActivity : AppCompatActivity() {
} }
runCatching { runCatching {
SshKey.import(uri) SshKey.import(uri)
Toast.makeText(this, resources.getString(R.string.ssh_key_success_dialog_title), Toast.LENGTH_LONG).show() Toast.makeText(
this,
resources.getString(R.string.ssh_key_success_dialog_title),
Toast.LENGTH_LONG
)
.show()
setResult(RESULT_OK) setResult(RESULT_OK)
finish() finish()
} }

View file

@ -19,7 +19,8 @@ import dev.msfjarvis.aps.R
object BiometricAuthenticator { object BiometricAuthenticator {
private const val TAG = "BiometricAuthenticator" private const val TAG = "BiometricAuthenticator"
private const val validAuthenticators = Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK private const val validAuthenticators =
Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
sealed class Result { sealed class Result {
data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result() data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
@ -29,7 +30,8 @@ object BiometricAuthenticator {
} }
fun canAuthenticate(activity: FragmentActivity): Boolean { fun canAuthenticate(activity: FragmentActivity): Boolean {
return BiometricManager.from(activity).canAuthenticate(validAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS return BiometricManager.from(activity).canAuthenticate(validAuthenticators) ==
BiometricManager.BIOMETRIC_SUCCESS
} }
fun authenticate( fun authenticate(
@ -55,7 +57,11 @@ object BiometricAuthenticator {
BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> { BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
Result.HardwareUnavailableOrDisabled Result.HardwareUnavailableOrDisabled
} }
else -> Result.Failure(errorCode, activity.getString(R.string.biometric_auth_error_reason, errString)) else ->
Result.Failure(
errorCode,
activity.getString(R.string.biometric_auth_error_reason, errString)
)
} }
) )
} }
@ -77,7 +83,11 @@ object BiometricAuthenticator {
.setTitle(activity.getString(dialogTitleRes)) .setTitle(activity.getString(dialogTitleRes))
.setAllowedAuthenticators(validAuthenticators) .setAllowedAuthenticators(validAuthenticators)
.build() .build()
BiometricPrompt(activity, ContextCompat.getMainExecutor(activity.applicationContext), authCallback) BiometricPrompt(
activity,
ContextCompat.getMainExecutor(activity.applicationContext),
authCallback
)
.authenticate(promptInfo) .authenticate(promptInfo)
} else { } else {
callback(Result.HardwareUnavailableOrDisabled) callback(Result.HardwareUnavailableOrDisabled)

View file

@ -61,7 +61,11 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
} }
} }
private fun makeMatchDataset(context: Context, file: File, imeSpec: InlinePresentationSpec?): Dataset? { private fun makeMatchDataset(
context: Context,
file: File,
imeSpec: InlinePresentationSpec?
): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
val metadata = makeFillMatchMetadata(context, file) val metadata = makeFillMatchMetadata(context, file)
val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context) val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
@ -82,12 +86,21 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
return makeIntentDataset(context, AutofillAction.Generate, intentSender, metadata, imeSpec) return makeIntentDataset(context, AutofillAction.Generate, intentSender, metadata, imeSpec)
} }
private fun makeFillOtpFromSmsDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? { private fun makeFillOtpFromSmsDataset(
context: Context,
imeSpec: InlinePresentationSpec?
): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.FillOtpFromSms)) return null if (!scenario.hasFieldsToFillOn(AutofillAction.FillOtpFromSms)) return null
if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null
val metadata = makeFillOtpFromSmsMetadata(context) val metadata = makeFillOtpFromSmsMetadata(context)
val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context) val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context)
return makeIntentDataset(context, AutofillAction.FillOtpFromSms, intentSender, metadata, imeSpec) return makeIntentDataset(
context,
AutofillAction.FillOtpFromSms,
intentSender,
metadata,
imeSpec
)
} }
private fun makePublisherChangedDataset( private fun makePublisherChangedDataset(
@ -150,7 +163,12 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
addDataset(it) addDataset(it)
} }
if (datasetCount == 0) return null if (datasetCount == 0) return null
setHeader(makeRemoteView(context, makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true)))) setHeader(
makeRemoteView(
context,
makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true))
)
)
makeSaveInfo()?.let { setSaveInfo(it) } makeSaveInfo()?.let { setSaveInfo(it) }
setClientState(clientState) setClientState(clientState)
setIgnoredIds(*ignoredIds.toTypedArray()) setIgnoredIds(*ignoredIds.toTypedArray())
@ -177,7 +195,11 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
} }
/** Creates and returns a suitable [FillResponse] to the Autofill framework. */ /** Creates and returns a suitable [FillResponse] to the Autofill framework. */
fun fillCredentials(context: Context, inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) { fun fillCredentials(
context: Context,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback
) {
AutofillMatcher.getMatchesFor(context, formOrigin) AutofillMatcher.getMatchesFor(context, formOrigin)
.fold( .fold(
success = { matchedFiles -> success = { matchedFiles ->

View file

@ -35,7 +35,9 @@ private fun Context.matchPreferences(formOrigin: FormOrigin): SharedPreferences
} }
class AutofillPublisherChangedException(val formOrigin: FormOrigin) : class AutofillPublisherChangedException(val formOrigin: FormOrigin) :
Exception("The publisher of '${formOrigin.identifier}' changed since an entry was first matched with this app") { Exception(
"The publisher of '${formOrigin.identifier}' changed since an entry was first matched with this app"
) {
init { init {
require(formOrigin is FormOrigin.App) require(formOrigin is FormOrigin.App)
@ -50,10 +52,12 @@ class AutofillMatcher {
private const val MAX_NUM_MATCHES = 10 private const val MAX_NUM_MATCHES = 10
private const val PREFERENCE_PREFIX_TOKEN = "token;" private const val PREFERENCE_PREFIX_TOKEN = "token;"
private fun tokenKey(formOrigin: FormOrigin.App) = "$PREFERENCE_PREFIX_TOKEN${formOrigin.identifier}" private fun tokenKey(formOrigin: FormOrigin.App) =
"$PREFERENCE_PREFIX_TOKEN${formOrigin.identifier}"
private const val PREFERENCE_PREFIX_MATCHES = "matches;" private const val PREFERENCE_PREFIX_MATCHES = "matches;"
private fun matchesKey(formOrigin: FormOrigin) = "$PREFERENCE_PREFIX_MATCHES${formOrigin.identifier}" private fun matchesKey(formOrigin: FormOrigin) =
"$PREFERENCE_PREFIX_MATCHES${formOrigin.identifier}"
private fun hasFormOriginHashChanged(context: Context, formOrigin: FormOrigin): Boolean { private fun hasFormOriginHashChanged(context: Context, formOrigin: FormOrigin): Boolean {
return when (formOrigin) { return when (formOrigin) {
@ -61,7 +65,8 @@ class AutofillMatcher {
is FormOrigin.App -> { is FormOrigin.App -> {
val packageName = formOrigin.identifier val packageName = formOrigin.identifier
val certificatesHash = computeCertificatesHash(context, packageName) val certificatesHash = computeCertificatesHash(context, packageName)
val storedCertificatesHash = context.autofillAppMatches.getString(tokenKey(formOrigin), null) ?: return false val storedCertificatesHash =
context.autofillAppMatches.getString(tokenKey(formOrigin), null) ?: return false
val hashHasChanged = certificatesHash != storedCertificatesHash val hashHasChanged = certificatesHash != storedCertificatesHash
if (hashHasChanged) { if (hashHasChanged) {
e { "$packageName: stored=$storedCertificatesHash, new=$certificatesHash" } e { "$packageName: stored=$storedCertificatesHash, new=$certificatesHash" }
@ -91,15 +96,21 @@ class AutofillMatcher {
* time the user associated an entry with it, an [AutofillPublisherChangedException] will be * time the user associated an entry with it, an [AutofillPublisherChangedException] will be
* thrown. * thrown.
*/ */
fun getMatchesFor(context: Context, formOrigin: FormOrigin): Result<List<File>, AutofillPublisherChangedException> { fun getMatchesFor(
context: Context,
formOrigin: FormOrigin
): Result<List<File>, AutofillPublisherChangedException> {
if (hasFormOriginHashChanged(context, formOrigin)) { if (hasFormOriginHashChanged(context, formOrigin)) {
return Err(AutofillPublisherChangedException(formOrigin)) return Err(AutofillPublisherChangedException(formOrigin))
} }
val matchPreferences = context.matchPreferences(formOrigin) val matchPreferences = context.matchPreferences(formOrigin)
val matchedFiles = matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) } val matchedFiles =
matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) }
return Ok( return Ok(
matchedFiles.filter { it.exists() }.also { validFiles -> matchedFiles.filter { it.exists() }.also { validFiles ->
matchPreferences.edit { putStringSet(matchesKey(formOrigin), validFiles.map { it.absolutePath }.toSet()) } matchPreferences.edit {
putStringSet(matchesKey(formOrigin), validFiles.map { it.absolutePath }.toSet())
}
} }
) )
} }
@ -127,7 +138,8 @@ class AutofillMatcher {
throw AutofillPublisherChangedException(formOrigin) throw AutofillPublisherChangedException(formOrigin)
} }
val matchPreferences = context.matchPreferences(formOrigin) val matchPreferences = context.matchPreferences(formOrigin)
val matchedFiles = matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) } val matchedFiles =
matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) }
val newFiles = setOf(file.absoluteFile).union(matchedFiles) val newFiles = setOf(file.absoluteFile).union(matchedFiles)
if (newFiles.size > MAX_NUM_MATCHES) { if (newFiles.size > MAX_NUM_MATCHES) {
Toast.makeText( Toast.makeText(
@ -138,7 +150,9 @@ class AutofillMatcher {
.show() .show()
return return
} }
matchPreferences.edit { putStringSet(matchesKey(formOrigin), newFiles.map { it.absolutePath }.toSet()) } matchPreferences.edit {
putStringSet(matchesKey(formOrigin), newFiles.map { it.absolutePath }.toSet())
}
storeFormOriginHash(context, formOrigin) storeFormOriginHash(context, formOrigin)
d { "Stored match for $formOrigin" } d { "Stored match for $formOrigin" }
} }
@ -153,7 +167,8 @@ class AutofillMatcher {
delete: Collection<File> = emptyList() delete: Collection<File> = emptyList()
) { ) {
val deletePathList = delete.map { it.absolutePath } val deletePathList = delete.map { it.absolutePath }
val oldNewPathMap = moveFromTo.mapValues { it.value.absolutePath }.mapKeys { it.key.absolutePath } val oldNewPathMap =
moveFromTo.mapValues { it.value.absolutePath }.mapKeys { it.key.absolutePath }
for (prefs in listOf(context.autofillAppMatches, context.autofillWebMatches)) { for (prefs in listOf(context.autofillAppMatches, context.autofillWebMatches)) {
for ((key, value) in prefs.all) { for ((key, value) in prefs.all) {
if (!key.startsWith(PREFERENCE_PREFIX_MATCHES)) continue if (!key.startsWith(PREFERENCE_PREFIX_MATCHES)) continue

View file

@ -96,7 +96,8 @@ enum class DirectoryStructure(val value: String) {
when (this) { when (this) {
EncryptedUsername -> null EncryptedUsername -> null
FileBased -> file.nameWithoutExtension.takeIf { file.parentFile != null } FileBased -> file.nameWithoutExtension.takeIf { file.parentFile != null }
DirectoryBased -> file.parentFile?.let { parentFile -> "${parentFile.name}/${file.nameWithoutExtension}" } DirectoryBased ->
file.parentFile?.let { parentFile -> "${parentFile.name}/${file.nameWithoutExtension}" }
?: file.nameWithoutExtension ?: file.nameWithoutExtension
} }
@ -138,7 +139,8 @@ object AutofillPreferences {
directoryStructure: DirectoryStructure directoryStructure: DirectoryStructure
): Credentials { ): Credentials {
// Always give priority to a username stored in the encrypted extras // Always give priority to a username stored in the encrypted extras
val username = entry.username ?: directoryStructure.getUsernameFor(file) ?: context.getDefaultUsername() val username =
entry.username ?: directoryStructure.getUsernameFor(file) ?: context.getDefaultUsername()
return Credentials(username, entry.password, entry.totp.value) return Credentials(username, entry.password, entry.totp.value)
} }
} }

View file

@ -154,7 +154,10 @@ class AutofillResponseBuilder(form: FillableForm) {
if (datasetCount == 0) return null if (datasetCount == 0) return null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setHeader( setHeader(
makeRemoteView(context, makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true))) makeRemoteView(
context,
makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true))
)
) )
} }
makeSaveInfo()?.let { setSaveInfo(it) } makeSaveInfo()?.let { setSaveInfo(it) }

View file

@ -51,13 +51,15 @@ fun makeInlinePresentation(
if (UiVersions.INLINE_UI_VERSION_1 !in UiVersions.getVersions(imeSpec.style)) return null if (UiVersions.INLINE_UI_VERSION_1 !in UiVersions.getVersions(imeSpec.style)) return null
val launchIntent = PendingIntent.getActivity(context, 0, Intent(context, PasswordStore::class.java), 0) val launchIntent =
PendingIntent.getActivity(context, 0, Intent(context, PasswordStore::class.java), 0)
val slice = val slice =
InlineSuggestionUi.newContentBuilder(launchIntent).run { InlineSuggestionUi.newContentBuilder(launchIntent).run {
setTitle(metadata.title) setTitle(metadata.title)
if (metadata.subtitle != null) setSubtitle(metadata.subtitle) if (metadata.subtitle != null) setSubtitle(metadata.subtitle)
setContentDescription( setContentDescription(
if (metadata.subtitle != null) "${metadata.title} - ${metadata.subtitle}" else metadata.title if (metadata.subtitle != null) "${metadata.title} - ${metadata.subtitle}"
else metadata.title
) )
setStartIcon(Icon.createWithResource(context, metadata.iconRes)) setStartIcon(Icon.createWithResource(context, metadata.iconRes))
build().slice build().slice
@ -69,13 +71,19 @@ fun makeInlinePresentation(
fun makeFillMatchMetadata(context: Context, file: File): DatasetMetadata { fun makeFillMatchMetadata(context: Context, file: File): DatasetMetadata {
val directoryStructure = AutofillPreferences.directoryStructure(context) val directoryStructure = AutofillPreferences.directoryStructure(context)
val relativeFile = file.relativeTo(PasswordRepository.getRepositoryDirectory()) val relativeFile = file.relativeTo(PasswordRepository.getRepositoryDirectory())
val title = directoryStructure.getIdentifierFor(relativeFile) ?: directoryStructure.getAccountPartFor(relativeFile)!! val title =
directoryStructure.getIdentifierFor(relativeFile)
?: directoryStructure.getAccountPartFor(relativeFile)!!
val subtitle = directoryStructure.getAccountPartFor(relativeFile) val subtitle = directoryStructure.getAccountPartFor(relativeFile)
return DatasetMetadata(title, subtitle, R.drawable.ic_person_black_24dp) return DatasetMetadata(title, subtitle, R.drawable.ic_person_black_24dp)
} }
fun makeSearchAndFillMetadata(context: Context) = fun makeSearchAndFillMetadata(context: Context) =
DatasetMetadata(context.getString(R.string.oreo_autofill_search_in_store), null, R.drawable.ic_search_black_24dp) DatasetMetadata(
context.getString(R.string.oreo_autofill_search_in_store),
null,
R.drawable.ic_search_black_24dp
)
fun makeGenerateAndFillMetadata(context: Context) = fun makeGenerateAndFillMetadata(context: Context) =
DatasetMetadata( DatasetMetadata(
@ -85,7 +93,11 @@ fun makeGenerateAndFillMetadata(context: Context) =
) )
fun makeFillOtpFromSmsMetadata(context: Context) = fun makeFillOtpFromSmsMetadata(context: Context) =
DatasetMetadata(context.getString(R.string.oreo_autofill_fill_otp_from_sms), null, R.drawable.ic_autofill_sms) DatasetMetadata(
context.getString(R.string.oreo_autofill_fill_otp_from_sms),
null,
R.drawable.ic_autofill_sms
)
fun makeEmptyMetadata() = DatasetMetadata("PLACEHOLDER", "PLACEHOLDER", R.mipmap.ic_launcher) fun makeEmptyMetadata() = DatasetMetadata("PLACEHOLDER", "PLACEHOLDER", R.mipmap.ic_launcher)

View file

@ -17,7 +17,8 @@ sealed class GpgIdentifier {
if (identifier.isEmpty()) return null if (identifier.isEmpty()) return null
// Match long key IDs: // Match long key IDs:
// FF22334455667788 or 0xFF22334455667788 // FF22334455667788 or 0xFF22334455667788
val maybeLongKeyId = identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{16}".toRegex()) } val maybeLongKeyId =
identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{16}".toRegex()) }
if (maybeLongKeyId != null) { if (maybeLongKeyId != null) {
val keyId = maybeLongKeyId.toULong(16) val keyId = maybeLongKeyId.toULong(16)
return KeyId(keyId.toLong()) return KeyId(keyId.toLong())
@ -25,7 +26,8 @@ sealed class GpgIdentifier {
// Match fingerprints: // Match fingerprints:
// FF223344556677889900112233445566778899 or 0xFF223344556677889900112233445566778899 // FF223344556677889900112233445566778899 or 0xFF223344556677889900112233445566778899
val maybeFingerprint = identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{40}".toRegex()) } val maybeFingerprint =
identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{40}".toRegex()) }
if (maybeFingerprint != null) { if (maybeFingerprint != null) {
// Truncating to the long key ID is not a security issue since OpenKeychain only // Truncating to the long key ID is not a security issue since OpenKeychain only
// accepts // accepts

View file

@ -41,7 +41,11 @@ fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) {
setOnShowListener { setOnShowListener {
findViewById<T>(id)?.apply { findViewById<T>(id)?.apply {
setOnFocusChangeListener { v, _ -> setOnFocusChangeListener { v, _ ->
v.post { context.getSystemService<InputMethodManager>()?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT) } v.post {
context
.getSystemService<InputMethodManager>()
?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
}
} }
requestFocus() requestFocus()
} }
@ -64,7 +68,8 @@ fun Context.getEncryptedProxyPrefs() = getEncryptedPrefs("http_proxy")
/** Get an instance of [EncryptedSharedPreferences] with the given [fileName] */ /** Get an instance of [EncryptedSharedPreferences] with the given [fileName] */
private fun Context.getEncryptedPrefs(fileName: String): SharedPreferences { private fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
val masterKeyAlias = MasterKey.Builder(applicationContext).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() val masterKeyAlias =
MasterKey.Builder(applicationContext).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
return EncryptedSharedPreferences.create( return EncryptedSharedPreferences.create(
applicationContext, applicationContext,
fileName, fileName,

View file

@ -20,8 +20,10 @@ import kotlin.reflect.KProperty
* Imported from * Imported from
* https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c * https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c
*/ */
class FragmentViewBindingDelegate<T : ViewBinding>(val fragment: Fragment, val viewBindingFactory: (View) -> T) : class FragmentViewBindingDelegate<T : ViewBinding>(
ReadOnlyProperty<Fragment, T> { val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var binding: T? = null private var binding: T? = null
@ -51,7 +53,9 @@ class FragmentViewBindingDelegate<T : ViewBinding>(val fragment: Fragment, val v
val lifecycle = fragment.viewLifecycleOwner.lifecycle val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.") throw IllegalStateException(
"Should not attempt to get bindings when Fragment views are destroyed."
)
} }
return viewBindingFactory(thisRef.requireView()).also { this.binding = it } return viewBindingFactory(thisRef.requireView()).also { this.binding = it }
@ -61,5 +65,6 @@ class FragmentViewBindingDelegate<T : ViewBinding>(val fragment: Fragment, val v
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) = fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory) FragmentViewBindingDelegate(this, viewBindingFactory)
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(crossinline bindingInflater: (LayoutInflater) -> T) = inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) } crossinline bindingInflater: (LayoutInflater) -> T
) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) }

View file

@ -14,7 +14,8 @@ import java.net.UnknownHostException
/** /**
* Supertype for all Git-related [Exception] s that can be thrown by [GitCommandExecutor.execute]. * Supertype for all Git-related [Exception] s that can be thrown by [GitCommandExecutor.execute].
*/ */
sealed class GitException(@StringRes res: Int, vararg fmt: String) : Exception(buildMessage(res, *fmt)) { sealed class GitException(@StringRes res: Int, vararg fmt: String) :
Exception(buildMessage(res, *fmt)) {
override val message = super.message!! override val message = super.message!!

View file

@ -74,11 +74,13 @@ class GitCommandExecutor(
// Code imported (modified) from Gerrit PushOp, license Apache v2 // Code imported (modified) from Gerrit PushOp, license Apache v2
for (rru in result.remoteUpdates) { for (rru in result.remoteUpdates) {
when (rru.status) { when (rru.status) {
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> throw PushException.NonFastForward RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD ->
throw PushException.NonFastForward
RemoteRefUpdate.Status.REJECTED_NODELETE, RemoteRefUpdate.Status.REJECTED_NODELETE,
RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED, RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED,
RemoteRefUpdate.Status.NON_EXISTING, RemoteRefUpdate.Status.NON_EXISTING,
RemoteRefUpdate.Status.NOT_ATTEMPTED, -> throw PushException.Generic(rru.status.name) RemoteRefUpdate.Status.NOT_ATTEMPTED, ->
throw PushException.Generic(rru.status.name)
RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> { RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> {
throw if ("non-fast-forward" == rru.message) { throw if ("non-fast-forward" == rru.message) {
PushException.RemoteRejected PushException.RemoteRejected

View file

@ -15,4 +15,9 @@ import java.util.Date
* @property authorName name of the commit's author without email address. * @property authorName name of the commit's author without email address.
* @property time time when the commit was created. * @property time time when the commit was created.
*/ */
data class GitCommit(val hash: String, val shortMessage: String, val authorName: String, val time: Date) data class GitCommit(
val hash: String,
val shortMessage: String,
val authorName: String,
val time: Date
)

View file

@ -40,7 +40,9 @@ class GitLogModel {
// Additionally, tests with 1000 commits in the log have not produced a significant delay in the // Additionally, tests with 1000 commits in the log have not produced a significant delay in the
// user experience. // user experience.
private val cache: MutableList<GitCommit> by lazy(LazyThreadSafetyMode.NONE) { private val cache: MutableList<GitCommit> by lazy(LazyThreadSafetyMode.NONE) {
commits().map { GitCommit(it.hash, it.shortMessage, it.authorIdent.name, it.time) }.toMutableList() commits()
.map { GitCommit(it.hash, it.shortMessage, it.authorIdent.name, it.time) }
.toMutableList()
} }
val size = cache.size val size = cache.size

View file

@ -11,13 +11,17 @@ import org.eclipse.jgit.api.RebaseCommand
import org.eclipse.jgit.api.ResetCommand import org.eclipse.jgit.api.ResetCommand
import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.lib.RepositoryState
class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) : GitOperation(callingActivity) { class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) :
GitOperation(callingActivity) {
private val merging = repository.repositoryState == RepositoryState.MERGING private val merging = repository.repositoryState == RepositoryState.MERGING
private val resetCommands = private val resetCommands =
arrayOf( arrayOf(
// git checkout -b conflict-branch // git checkout -b conflict-branch
git.checkout().setCreateBranch(true).setName("conflicting-$remoteBranch-${System.currentTimeMillis()}"), git
.checkout()
.setCreateBranch(true)
.setName("conflicting-$remoteBranch-${System.currentTimeMillis()}"),
// push the changes // push the changes
git.push().setRemote("origin"), git.push().setRemote("origin"),
// switch back to ${gitBranch} // switch back to ${gitBranch}
@ -47,8 +51,12 @@ class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) : GitOp
if (!git.repository.repositoryState.isRebasing && !merging) { if (!git.repository.repositoryState.isRebasing && !merging) {
MaterialAlertDialogBuilder(callingActivity) MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title)) .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title))
.setMessage(callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded)) .setMessage(
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() } callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded)
)
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
callingActivity.finish()
}
.show() .show()
false false
} else { } else {

View file

@ -14,7 +14,8 @@ import org.eclipse.jgit.api.GitCommand
* @param uri URL to clone the repository from * @param uri URL to clone the repository from
* @param callingActivity the calling activity * @param callingActivity the calling activity
*/ */
class CloneOperation(callingActivity: ContinuationContainerActivity, uri: String) : GitOperation(callingActivity) { class CloneOperation(callingActivity: ContinuationContainerActivity, uri: String) :
GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> = override val commands: Array<GitCommand<out Any>> =
arrayOf( arrayOf(

View file

@ -24,7 +24,8 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
class CredentialFinder(val callingActivity: FragmentActivity, val authMode: AuthMode) : InteractivePasswordFinder() { class CredentialFinder(val callingActivity: FragmentActivity, val authMode: AuthMode) :
InteractivePasswordFinder() {
override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) { override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) {
val gitOperationPrefs = callingActivity.getEncryptedGitPrefs() val gitOperationPrefs = callingActivity.getEncryptedGitPrefs()
@ -49,18 +50,22 @@ class CredentialFinder(val callingActivity: FragmentActivity, val authMode: Auth
rememberRes = R.string.git_operation_remember_password rememberRes = R.string.git_operation_remember_password
errorRes = R.string.git_operation_wrong_password errorRes = R.string.git_operation_wrong_password
} }
else -> throw IllegalStateException("Only SshKey and Password connection mode ask for passwords") else ->
throw IllegalStateException("Only SshKey and Password connection mode ask for passwords")
} }
if (isRetry) gitOperationPrefs.edit { remove(credentialPref) } if (isRetry) gitOperationPrefs.edit { remove(credentialPref) }
val storedCredential = gitOperationPrefs.getString(credentialPref, null) val storedCredential = gitOperationPrefs.getString(credentialPref, null)
if (storedCredential == null) { if (storedCredential == null) {
val layoutInflater = LayoutInflater.from(callingActivity) val layoutInflater = LayoutInflater.from(callingActivity)
@SuppressLint("InflateParams") val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null) @SuppressLint("InflateParams")
val credentialLayout = dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout) val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
val credentialLayout =
dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout)
val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential) val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential)
editCredential.setHint(hintRes) editCredential.setHint(hintRes)
val rememberCredential = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential) val rememberCredential =
dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential)
rememberCredential.setText(rememberRes) rememberCredential.setText(rememberRes)
if (isRetry) { if (isRetry) {
credentialLayout.error = callingActivity.resources.getString(errorRes) credentialLayout.error = callingActivity.resources.getString(errorRes)

View file

@ -60,7 +60,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
private val authActivity private val authActivity
get() = callingActivity as ContinuationContainerActivity get() = callingActivity as ContinuationContainerActivity
private class HttpsCredentialsProvider(private val passwordFinder: PasswordFinder) : CredentialsProvider() { private class HttpsCredentialsProvider(private val passwordFinder: PasswordFinder) :
CredentialsProvider() {
private var cachedPassword: CharArray? = null private var cachedPassword: CharArray? = null
@ -72,7 +73,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
is CredentialItem.Username -> item.value = uri?.user is CredentialItem.Username -> item.value = uri?.user
is CredentialItem.Password -> { is CredentialItem.Password -> {
item.value = item.value =
cachedPassword?.clone() ?: passwordFinder.reqPassword(null).also { cachedPassword = it.clone() } cachedPassword?.clone()
?: passwordFinder.reqPassword(null).also { cachedPassword = it.clone() }
} }
else -> UnsupportedCredentialItem(uri, item.javaClass.name) else -> UnsupportedCredentialItem(uri, item.javaClass.name)
} }
@ -102,7 +104,10 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
.onFailure { e -> e(e) } .onFailure { e -> e(e) }
} }
private fun registerAuthProviders(authMethod: SshAuthMethod, credentialsProvider: CredentialsProvider? = null) { private fun registerAuthProviders(
authMethod: SshAuthMethod,
credentialsProvider: CredentialsProvider? = null
) {
sshSessionFactory = SshjSessionFactory(authMethod, hostKeyFile) sshSessionFactory = SshjSessionFactory(authMethod, hostKeyFile)
commands.filterIsInstance<TransportCommand<*, *>>().forEach { command -> commands.filterIsInstance<TransportCommand<*, *>>().forEach { command ->
command.setTransportConfigCallback { transport: Transport -> command.setTransportConfigCallback { transport: Transport ->
@ -132,12 +137,12 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
MaterialAlertDialogBuilder(callingActivity) MaterialAlertDialogBuilder(callingActivity)
.setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text)) .setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text))
.setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title)) .setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title))
.setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ -> .setPositiveButton(
getSshKey(false) callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)
} ) { _, _ -> getSshKey(false) }
.setNegativeButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)) { _, _ -> .setNegativeButton(
getSshKey(true) callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)
} ) { _, _ -> getSshKey(true) }
.setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> .setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
// Finish the blank GitActivity so user doesn't have to press back // Finish the blank GitActivity so user doesn't have to press back
callingActivity.finish() callingActivity.finish()
@ -153,9 +158,10 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
val result = val result =
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
suspendCoroutine<BiometricAuthenticator.Result> { cont -> suspendCoroutine<BiometricAuthenticator.Result> { cont ->
BiometricAuthenticator.authenticate(callingActivity, R.string.biometric_prompt_title_ssh_auth) { BiometricAuthenticator.authenticate(
if (it !is BiometricAuthenticator.Result.Failure) cont.resume(it) callingActivity,
} R.string.biometric_prompt_title_ssh_auth
) { if (it !is BiometricAuthenticator.Result.Failure) cont.resume(it) }
} }
} }
when (result) { when (result) {
@ -193,7 +199,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
} }
AuthMode.OpenKeychain -> registerAuthProviders(SshAuthMethod.OpenKeychain(authActivity)) AuthMode.OpenKeychain -> registerAuthProviders(SshAuthMethod.OpenKeychain(authActivity))
AuthMode.Password -> { AuthMode.Password -> {
val httpsCredentialProvider = HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password)) val httpsCredentialProvider =
HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password))
registerAuthProviders(SshAuthMethod.Password(authActivity), httpsCredentialProvider) registerAuthProviders(SshAuthMethod.Password(authActivity), httpsCredentialProvider)
} }
AuthMode.None -> {} AuthMode.None -> {}

View file

@ -7,7 +7,8 @@ package dev.msfjarvis.aps.util.git.operation
import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity
import org.eclipse.jgit.api.GitCommand import org.eclipse.jgit.api.GitCommand
class PushOperation(callingActivity: ContinuationContainerActivity) : GitOperation(callingActivity) { class PushOperation(callingActivity: ContinuationContainerActivity) :
GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> = override val commands: Array<GitCommand<out Any>> =
arrayOf( arrayOf(

View file

@ -7,7 +7,8 @@ package dev.msfjarvis.aps.util.git.operation
import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity
import org.eclipse.jgit.api.ResetCommand import org.eclipse.jgit.api.ResetCommand
class ResetToRemoteOperation(callingActivity: ContinuationContainerActivity) : GitOperation(callingActivity) { class ResetToRemoteOperation(callingActivity: ContinuationContainerActivity) :
GitOperation(callingActivity) {
override val commands = override val commands =
arrayOf( arrayOf(

View file

@ -106,7 +106,8 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
val response = sshPublicKeyResponse.response as SshPublicKeyResponse val response = sshPublicKeyResponse.response as SshPublicKeyResponse
val sshPublicKey = response.sshPublicKey!! val sshPublicKey = response.sshPublicKey!!
publicKey = publicKey =
parseSshPublicKey(sshPublicKey) ?: throw IllegalStateException("OpenKeychain API returned invalid SSH key") parseSshPublicKey(sshPublicKey)
?: throw IllegalStateException("OpenKeychain API returned invalid SSH key")
} }
is ApiResponse.NoSuchKey -> is ApiResponse.NoSuchKey ->
if (isRetry) { if (isRetry) {
@ -122,13 +123,17 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
private suspend fun selectKey() { private suspend fun selectKey() {
when (val keySelectionResponse = executeApiRequest(KeySelectionRequest())) { when (val keySelectionResponse = executeApiRequest(KeySelectionRequest())) {
is ApiResponse.Success -> keyId = (keySelectionResponse.response as KeySelectionResponse).keyId is ApiResponse.Success ->
keyId = (keySelectionResponse.response as KeySelectionResponse).keyId
is ApiResponse.GeneralError -> throw keySelectionResponse.exception is ApiResponse.GeneralError -> throw keySelectionResponse.exception
is ApiResponse.NoSuchKey -> throw keySelectionResponse.exception is ApiResponse.NoSuchKey -> throw keySelectionResponse.exception
} }
} }
private suspend fun executeApiRequest(request: Request, resultOfUserInteraction: Intent? = null): ApiResponse { private suspend fun executeApiRequest(
request: Request,
resultOfUserInteraction: Intent? = null
): ApiResponse {
d { "executeRequest($request) called" } d { "executeRequest($request) called" }
val result = val result =
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -141,7 +146,11 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
} }
private suspend fun parseResult(request: Request, result: Intent): ApiResponse { private suspend fun parseResult(request: Request, result: Intent): ApiResponse {
return when (result.getIntExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, SshAuthenticationApi.RESULT_CODE_ERROR)) { return when (result.getIntExtra(
SshAuthenticationApi.EXTRA_RESULT_CODE,
SshAuthenticationApi.RESULT_CODE_ERROR
)
) {
SshAuthenticationApi.RESULT_CODE_SUCCESS -> { SshAuthenticationApi.RESULT_CODE_SUCCESS -> {
ApiResponse.Success( ApiResponse.Success(
when (request) { when (request) {
@ -153,20 +162,27 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
) )
} }
SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
val pendingIntent: PendingIntent = result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT)!! val pendingIntent: PendingIntent =
result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT)!!
val resultOfUserInteraction: Intent = val resultOfUserInteraction: Intent =
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
suspendCoroutine { cont -> suspendCoroutine { cont ->
activity.stashedCont = cont activity.stashedCont = cont
activity.continueAfterUserInteraction.launch(IntentSenderRequest.Builder(pendingIntent).build()) activity.continueAfterUserInteraction.launch(
IntentSenderRequest.Builder(pendingIntent).build()
)
} }
} }
executeApiRequest(request, resultOfUserInteraction) executeApiRequest(request, resultOfUserInteraction)
} }
else -> { else -> {
val error = result.getParcelableExtra<SshAuthenticationApiError>(SshAuthenticationApi.EXTRA_ERROR) val error =
result.getParcelableExtra<SshAuthenticationApiError>(SshAuthenticationApi.EXTRA_ERROR)
val exception = val exception =
UserAuthException(DisconnectReason.UNKNOWN, "Request ${request::class.simpleName} failed: ${error?.message}") UserAuthException(
DisconnectReason.UNKNOWN,
"Request ${request::class.simpleName} failed: ${error?.message}"
)
when (error?.error) { when (error?.error) {
SshAuthenticationApiError.NO_AUTH_KEY, SshAuthenticationApiError.NO_SUCH_KEY -> SshAuthenticationApiError.NO_AUTH_KEY, SshAuthenticationApiError.NO_SUCH_KEY ->
ApiResponse.NoSuchKey(exception) ApiResponse.NoSuchKey(exception)
@ -181,7 +197,9 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
privateKey = privateKey =
object : OpenKeychainPrivateKey { object : OpenKeychainPrivateKey {
override suspend fun sign(challenge: ByteArray, hashAlgorithm: Int) = override suspend fun sign(challenge: ByteArray, hashAlgorithm: Int) =
when (val signingResponse = executeApiRequest(SigningRequest(challenge, keyId, hashAlgorithm))) { when (val signingResponse =
executeApiRequest(SigningRequest(challenge, keyId, hashAlgorithm))
) {
is ApiResponse.Success -> (signingResponse.response as SigningResponse).signature is ApiResponse.Success -> (signingResponse.response as SigningResponse).signature
is ApiResponse.GeneralError -> throw signingResponse.exception is ApiResponse.GeneralError -> throw signingResponse.exception
is ApiResponse.NoSuchKey -> throw signingResponse.exception is ApiResponse.NoSuchKey -> throw signingResponse.exception

View file

@ -28,7 +28,8 @@ class OpenKeychainWrappedKeyAlgorithmFactory(private val factory: Factory.Named<
override fun create() = OpenKeychainWrappedKeyAlgorithm(factory.create()) override fun create() = OpenKeychainWrappedKeyAlgorithm(factory.create())
} }
class OpenKeychainWrappedKeyAlgorithm(private val keyAlgorithm: KeyAlgorithm) : KeyAlgorithm by keyAlgorithm { class OpenKeychainWrappedKeyAlgorithm(private val keyAlgorithm: KeyAlgorithm) :
KeyAlgorithm by keyAlgorithm {
private val hashAlgorithm = private val hashAlgorithm =
when (keyAlgorithm.keyAlgorithm) { when (keyAlgorithm.keyAlgorithm) {
@ -39,11 +40,14 @@ class OpenKeychainWrappedKeyAlgorithm(private val keyAlgorithm: KeyAlgorithm) :
else -> SshAuthenticationApi.SHA512 else -> SshAuthenticationApi.SHA512
} }
override fun newSignature() = OpenKeychainWrappedSignature(keyAlgorithm.newSignature(), hashAlgorithm) override fun newSignature() =
OpenKeychainWrappedSignature(keyAlgorithm.newSignature(), hashAlgorithm)
} }
class OpenKeychainWrappedSignature(private val wrappedSignature: Signature, private val hashAlgorithm: Int) : class OpenKeychainWrappedSignature(
Signature by wrappedSignature { private val wrappedSignature: Signature,
private val hashAlgorithm: Int
) : Signature by wrappedSignature {
private val data = ByteArrayOutputStream() private val data = ByteArrayOutputStream()

View file

@ -115,7 +115,8 @@ object SshKey {
private var type: Type? private var type: Type?
get() = Type.fromValue(context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_KEY_TYPE)) get() = Type.fromValue(context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_KEY_TYPE))
set(value) = context.sharedPrefs.edit { putString(PreferenceKeys.GIT_REMOTE_KEY_TYPE, value?.value) } set(value) =
context.sharedPrefs.edit { putString(PreferenceKeys.GIT_REMOTE_KEY_TYPE, value?.value) }
private val isStrongBoxSupported by lazy(LazyThreadSafetyMode.NONE) { private val isStrongBoxSupported by lazy(LazyThreadSafetyMode.NONE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
@ -138,13 +139,20 @@ object SshKey {
} }
} }
enum class Algorithm(val algorithm: String, val applyToSpec: KeyGenParameterSpec.Builder.() -> Unit) { enum class Algorithm(
val algorithm: String,
val applyToSpec: KeyGenParameterSpec.Builder.() -> Unit
) {
Rsa( Rsa(
KeyProperties.KEY_ALGORITHM_RSA, KeyProperties.KEY_ALGORITHM_RSA,
{ {
setKeySize(3072) setKeySize(3072)
setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
setDigests(KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) setDigests(
KeyProperties.DIGEST_SHA1,
KeyProperties.DIGEST_SHA256,
KeyProperties.DIGEST_SHA512
)
} }
), ),
Ecdsa( Ecdsa(
@ -163,7 +171,9 @@ object SshKey {
private fun delete() { private fun delete() {
androidKeystore.deleteEntry(KEYSTORE_ALIAS) androidKeystore.deleteEntry(KEYSTORE_ALIAS)
// Remove Tink key set used by AndroidX's EncryptedFile. // Remove Tink key set used by AndroidX's EncryptedFile.
context.getSharedPreferences(ANDROIDX_SECURITY_KEYSET_PREF_NAME, Context.MODE_PRIVATE).edit { clear() } context.getSharedPreferences(ANDROIDX_SECURITY_KEYSET_PREF_NAME, Context.MODE_PRIVATE).edit {
clear()
}
if (privateKeyFile.isFile) { if (privateKeyFile.isFile) {
privateKeyFile.delete() privateKeyFile.delete()
} }
@ -177,7 +187,8 @@ object SshKey {
fun import(uri: Uri) { fun import(uri: Uri) {
// First check whether the content at uri is likely an SSH private key. // First check whether the content at uri is likely an SSH private key.
val fileSize = val fileSize =
context.contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)?.use { cursor -> context.contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)?.use {
cursor ->
// Cursor returns only a single row. // Cursor returns only a single row.
cursor.moveToFirst() cursor.moveToFirst()
cursor.getInt(0) cursor.getInt(0)
@ -186,7 +197,9 @@ object SshKey {
// We assume that an SSH key's ideal size is > 0 bytes && < 100 kilobytes. // We assume that an SSH key's ideal size is > 0 bytes && < 100 kilobytes.
if (fileSize > 100_000 || fileSize == 0) if (fileSize > 100_000 || fileSize == 0)
throw IllegalArgumentException(context.getString(R.string.ssh_key_import_error_not_an_ssh_key_message)) throw IllegalArgumentException(
context.getString(R.string.ssh_key_import_error_not_an_ssh_key_message)
)
val sshKeyInputStream = val sshKeyInputStream =
context.contentResolver.openInputStream(uri) context.contentResolver.openInputStream(uri)
@ -199,7 +212,9 @@ object SshKey {
!Regex("BEGIN .* PRIVATE KEY").containsMatchIn(lines.first()) || !Regex("BEGIN .* PRIVATE KEY").containsMatchIn(lines.first()) ||
!Regex("END .* PRIVATE KEY").containsMatchIn(lines.last()) !Regex("END .* PRIVATE KEY").containsMatchIn(lines.last())
) )
throw IllegalArgumentException(context.getString(R.string.ssh_key_import_error_not_an_ssh_key_message)) throw IllegalArgumentException(
context.getString(R.string.ssh_key_import_error_not_an_ssh_key_message)
)
// At this point, we are reasonably confident that we have actually been provided a private // At this point, we are reasonably confident that we have actually been provided a private
// key and delete the old key. // key and delete the old key.
@ -249,7 +264,9 @@ object SshKey {
val encryptedPrivateKeyFile = getOrCreateWrappedPrivateKeyFile(requireAuthentication) val encryptedPrivateKeyFile = getOrCreateWrappedPrivateKeyFile(requireAuthentication)
// Generate the ed25519 key pair and encrypt the private key. // Generate the ed25519 key pair and encrypt the private key.
val keyPair = net.i2p.crypto.eddsa.KeyPairGenerator().generateKeyPair() val keyPair = net.i2p.crypto.eddsa.KeyPairGenerator().generateKeyPair()
encryptedPrivateKeyFile.openFileOutput().use { os -> os.write((keyPair.private as EdDSAPrivateKey).seed) } encryptedPrivateKeyFile.openFileOutput().use { os ->
os.write((keyPair.private as EdDSAPrivateKey).seed)
}
// Write public key in SSH format to .ssh_key.pub. // Write public key in SSH format to .ssh_key.pub.
publicKeyFile.writeText(toSshPublicKey(keyPair.public)) publicKeyFile.writeText(toSshPublicKey(keyPair.public))
@ -288,7 +305,8 @@ object SshKey {
fun provide(client: SSHClient, passphraseFinder: InteractivePasswordFinder): KeyProvider? = fun provide(client: SSHClient, passphraseFinder: InteractivePasswordFinder): KeyProvider? =
when (type) { when (type) {
Type.LegacyGenerated, Type.Imported -> client.loadKeys(privateKeyFile.absolutePath, passphraseFinder) Type.LegacyGenerated, Type.Imported ->
client.loadKeys(privateKeyFile.absolutePath, passphraseFinder)
Type.KeystoreNative -> KeystoreNativeKeyProvider Type.KeystoreNative -> KeystoreNativeKeyProvider
Type.KeystoreWrappedEd25519 -> KeystoreWrappedEd25519KeyProvider Type.KeystoreWrappedEd25519 -> KeystoreWrappedEd25519KeyProvider
null -> null null -> null
@ -305,7 +323,10 @@ object SshKey {
override fun getPrivate(): PrivateKey = override fun getPrivate(): PrivateKey =
runCatching { androidKeystore.sshPrivateKey!! }.getOrElse { error -> runCatching { androidKeystore.sshPrivateKey!! }.getOrElse { error ->
e(error) e(error)
throw IOException("Failed to access private key '$KEYSTORE_ALIAS' from Android Keystore", error) throw IOException(
"Failed to access private key '$KEYSTORE_ALIAS' from Android Keystore",
error
)
} }
override fun getType(): KeyType = KeyType.fromKey(public) override fun getType(): KeyType = KeyType.fromKey(public)
@ -326,7 +347,9 @@ object SshKey {
// for `requireAuthentication` is not used as the key already exists at this point. // for `requireAuthentication` is not used as the key already exists at this point.
val encryptedPrivateKeyFile = runBlocking { getOrCreateWrappedPrivateKeyFile(false) } val encryptedPrivateKeyFile = runBlocking { getOrCreateWrappedPrivateKeyFile(false) }
val rawPrivateKey = encryptedPrivateKeyFile.openFileInput().use { it.readBytes() } val rawPrivateKey = encryptedPrivateKeyFile.openFileInput().use { it.readBytes() }
EdDSAPrivateKey(EdDSAPrivateKeySpec(rawPrivateKey, EdDSANamedCurveTable.ED_25519_CURVE_SPEC)) EdDSAPrivateKey(
EdDSAPrivateKeySpec(rawPrivateKey, EdDSANamedCurveTable.ED_25519_CURVE_SPEC)
)
} }
.getOrElse { error -> .getOrElse { error ->
e(error) e(error)

View file

@ -37,7 +37,8 @@ fun setUpBouncyCastleForSshj() {
// Replace the Android BC provider with the Java BouncyCastle provider since the former does // Replace the Android BC provider with the Java BouncyCastle provider since the former does
// not include all the required algorithms. // not include all the required algorithms.
// Note: This may affect crypto operations in other parts of the application. // Note: This may affect crypto operations in other parts of the application.
val bcIndex = Security.getProviders().indexOfFirst { it.name == BouncyCastleProvider.PROVIDER_NAME } val bcIndex =
Security.getProviders().indexOfFirst { it.name == BouncyCastleProvider.PROVIDER_NAME }
if (bcIndex == -1) { if (bcIndex == -1) {
// No Android BC found, install Java BC at lowest priority. // No Android BC found, install Java BC at lowest priority.
Security.addProvider(BouncyCastleProvider()) Security.addProvider(BouncyCastleProvider())
@ -77,9 +78,11 @@ private abstract class AbstractLogger(private val name: String) : Logger {
override fun trace(msg: String, t: Throwable?) = t(msg, t) override fun trace(msg: String, t: Throwable?) = t(msg, t)
override fun trace(marker: Marker, msg: String) = trace(msg) override fun trace(marker: Marker, msg: String) = trace(msg)
override fun trace(marker: Marker?, format: String, arg: Any?) = trace(format, arg) override fun trace(marker: Marker?, format: String, arg: Any?) = trace(format, arg)
override fun trace(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = trace(format, arg1, arg2) override fun trace(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
trace(format, arg1, arg2)
override fun trace(marker: Marker?, format: String, vararg arguments: Any?) = trace(format, *arguments) override fun trace(marker: Marker?, format: String, vararg arguments: Any?) =
trace(format, *arguments)
override fun trace(marker: Marker?, msg: String, t: Throwable?) = trace(msg, t) override fun trace(marker: Marker?, msg: String, t: Throwable?) = trace(msg, t)
@ -90,9 +93,11 @@ private abstract class AbstractLogger(private val name: String) : Logger {
override fun debug(msg: String, t: Throwable?) = d(msg, t) override fun debug(msg: String, t: Throwable?) = d(msg, t)
override fun debug(marker: Marker, msg: String) = debug(msg) override fun debug(marker: Marker, msg: String) = debug(msg)
override fun debug(marker: Marker?, format: String, arg: Any?) = debug(format, arg) override fun debug(marker: Marker?, format: String, arg: Any?) = debug(format, arg)
override fun debug(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = debug(format, arg1, arg2) override fun debug(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
debug(format, arg1, arg2)
override fun debug(marker: Marker?, format: String, vararg arguments: Any?) = debug(format, *arguments) override fun debug(marker: Marker?, format: String, vararg arguments: Any?) =
debug(format, *arguments)
override fun debug(marker: Marker?, msg: String, t: Throwable?) = debug(msg, t) override fun debug(marker: Marker?, msg: String, t: Throwable?) = debug(msg, t)
@ -103,9 +108,11 @@ private abstract class AbstractLogger(private val name: String) : Logger {
override fun info(msg: String, t: Throwable?) = i(msg, t) override fun info(msg: String, t: Throwable?) = i(msg, t)
override fun info(marker: Marker, msg: String) = info(msg) override fun info(marker: Marker, msg: String) = info(msg)
override fun info(marker: Marker?, format: String, arg: Any?) = info(format, arg) override fun info(marker: Marker?, format: String, arg: Any?) = info(format, arg)
override fun info(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = info(format, arg1, arg2) override fun info(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
info(format, arg1, arg2)
override fun info(marker: Marker?, format: String, vararg arguments: Any?) = info(format, *arguments) override fun info(marker: Marker?, format: String, vararg arguments: Any?) =
info(format, *arguments)
override fun info(marker: Marker?, msg: String, t: Throwable?) = info(msg, t) override fun info(marker: Marker?, msg: String, t: Throwable?) = info(msg, t)
@ -116,9 +123,11 @@ private abstract class AbstractLogger(private val name: String) : Logger {
override fun warn(msg: String, t: Throwable?) = w(msg, t) override fun warn(msg: String, t: Throwable?) = w(msg, t)
override fun warn(marker: Marker, msg: String) = warn(msg) override fun warn(marker: Marker, msg: String) = warn(msg)
override fun warn(marker: Marker?, format: String, arg: Any?) = warn(format, arg) override fun warn(marker: Marker?, format: String, arg: Any?) = warn(format, arg)
override fun warn(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = warn(format, arg1, arg2) override fun warn(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
warn(format, arg1, arg2)
override fun warn(marker: Marker?, format: String, vararg arguments: Any?) = warn(format, *arguments) override fun warn(marker: Marker?, format: String, vararg arguments: Any?) =
warn(format, *arguments)
override fun warn(marker: Marker?, msg: String, t: Throwable?) = warn(msg, t) override fun warn(marker: Marker?, msg: String, t: Throwable?) = warn(msg, t)
@ -129,9 +138,11 @@ private abstract class AbstractLogger(private val name: String) : Logger {
override fun error(msg: String, t: Throwable?) = e(msg, t) override fun error(msg: String, t: Throwable?) = e(msg, t)
override fun error(marker: Marker, msg: String) = error(msg) override fun error(marker: Marker, msg: String) = error(msg)
override fun error(marker: Marker?, format: String, arg: Any?) = error(format, arg) override fun error(marker: Marker?, format: String, arg: Any?) = error(format, arg)
override fun error(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = error(format, arg1, arg2) override fun error(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
error(format, arg1, arg2)
override fun error(marker: Marker?, format: String, vararg arguments: Any?) = error(format, *arguments) override fun error(marker: Marker?, format: String, vararg arguments: Any?) =
error(format, *arguments)
override fun error(marker: Marker?, msg: String, t: Throwable?) = error(msg, t) override fun error(marker: Marker?, msg: String, t: Throwable?) = error(msg, t)
} }
@ -148,7 +159,8 @@ object TimberLoggerFactory : LoggerFactory {
// Replace slf4j's "{}" format string style with standard Java's "%s". // Replace slf4j's "{}" format string style with standard Java's "%s".
// The supposedly redundant escape on the } is not redundant. // The supposedly redundant escape on the } is not redundant.
@Suppress("RegExpRedundantEscape") private fun String.fix() = replace("""(?!<\\)\{\}""".toRegex(), "%s") @Suppress("RegExpRedundantEscape")
private fun String.fix() = replace("""(?!<\\)\{\}""".toRegex(), "%s")
override fun t(message: String, t: Throwable?, vararg args: Any?) { override fun t(message: String, t: Throwable?, vararg args: Any?) {
Timber.tag(name).v(t, message.fix(), *args) Timber.tag(name).v(t, message.fix(), *args)

View file

@ -52,7 +52,10 @@ abstract class InteractivePasswordFinder : PasswordFinder {
abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean)
final override fun reqPassword(resource: Resource<*>?): CharArray { final override fun reqPassword(resource: Resource<*>?): CharArray {
val password = runBlocking(Dispatchers.Main) { suspendCoroutine<String?> { cont -> askForPassword(cont, isRetry) } } val password =
runBlocking(Dispatchers.Main) {
suspendCoroutine<String?> { cont -> askForPassword(cont, isRetry) }
}
isRetry = true isRetry = true
return password?.toCharArray() ?: throw SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER) return password?.toCharArray() ?: throw SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER)
} }
@ -60,11 +63,17 @@ abstract class InteractivePasswordFinder : PasswordFinder {
final override fun shouldRetry(resource: Resource<*>?) = true final override fun shouldRetry(resource: Resource<*>?) = true
} }
class SshjSessionFactory(private val authMethod: SshAuthMethod, private val hostKeyFile: File) : SshSessionFactory() { class SshjSessionFactory(private val authMethod: SshAuthMethod, private val hostKeyFile: File) :
SshSessionFactory() {
private var currentSession: SshjSession? = null private var currentSession: SshjSession? = null
override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession { override fun getSession(
uri: URIish,
credentialsProvider: CredentialsProvider?,
fs: FS?,
tms: Int
): RemoteSession {
return currentSession return currentSession
?: SshjSession(uri, uri.user, authMethod, hostKeyFile).connect().also { ?: SshjSession(uri, uri.user, authMethod, hostKeyFile).connect().also {
d { "New SSH connection created" } d { "New SSH connection created" }
@ -81,7 +90,9 @@ private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier {
if (!hostKeyFile.exists()) { if (!hostKeyFile.exists()) {
return HostKeyVerifier { _, _, key -> return HostKeyVerifier { _, _, key ->
val digest = val digest =
runCatching { SecurityUtils.getMessageDigest("SHA-256") }.getOrElse { e -> throw SSHRuntimeException(e) } runCatching { SecurityUtils.getMessageDigest("SHA-256") }.getOrElse { e ->
throw SSHRuntimeException(e)
}
digest.update(PlainBuffer().putPublicKey(key).compactData) digest.update(PlainBuffer().putPublicKey(key).compactData)
val digestData = digest.digest() val digestData = digest.digest()
val hostKeyEntry = "SHA256:${Base64.encodeToString(digestData, Base64.NO_WRAP)}" val hostKeyEntry = "SHA256:${Base64.encodeToString(digestData, Base64.NO_WRAP)}"
@ -115,7 +126,9 @@ private class SshjSession(
val userPlusHost = "${uri.user}@${uri.host}" val userPlusHost = "${uri.user}@${uri.host}"
val realUser = userPlusHost.substringBeforeLast('@') val realUser = userPlusHost.substringBeforeLast('@')
val realHost = userPlusHost.substringAfterLast('@') val realHost = userPlusHost.substringAfterLast('@')
uri.setUser(realUser).setHost(realHost).also { d { "After fixup: user=${it.user}, host=${it.host}" } } uri.setUser(realUser).setHost(realHost).also {
d { "After fixup: user=${it.user}, host=${it.host}" }
}
} else { } else {
uri uri
} }
@ -131,7 +144,8 @@ private class SshjSession(
ssh.auth(username, passwordAuth) ssh.auth(username, passwordAuth)
} }
is SshAuthMethod.SshKey -> { is SshAuthMethod.SshKey -> {
val pubkeyAuth = AuthPublickey(SshKey.provide(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey))) val pubkeyAuth =
AuthPublickey(SshKey.provide(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey)))
ssh.auth(username, pubkeyAuth, passwordAuth) ssh.auth(username, pubkeyAuth, passwordAuth)
} }
is SshAuthMethod.OpenKeychain -> { is SshAuthMethod.OpenKeychain -> {
@ -174,7 +188,8 @@ private class SshjSession(
} }
} }
private class SshjProcess(private val command: Session.Command, private val timeout: Long) : Process() { private class SshjProcess(private val command: Session.Command, private val timeout: Long) :
Process() {
override fun waitFor(): Int { override fun waitFor(): Int {
command.join(timeout, TimeUnit.SECONDS) command.join(timeout, TimeUnit.SECONDS)

View file

@ -42,7 +42,10 @@ object PasswordGenerator {
*/ */
fun setPrefs(ctx: Context, options: List<PasswordOption>, targetLength: Int): Boolean { fun setPrefs(ctx: Context, options: List<PasswordOption>, targetLength: Int): Boolean {
ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE).edit { ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE).edit {
for (possibleOption in PasswordOption.values()) putBoolean(possibleOption.key, possibleOption in options) for (possibleOption in PasswordOption.values()) putBoolean(
possibleOption.key,
possibleOption in options
)
putInt("length", targetLength) putInt("length", targetLength)
} }
return true return true
@ -82,7 +85,9 @@ object PasswordGenerator {
} else { } else {
// The No* options are false, so the respective character category will be included. // The No* options are false, so the respective character category will be included.
when (option) { when (option) {
PasswordOption.NoDigits, PasswordOption.NoUppercaseLetters, PasswordOption.NoLowercaseLetters -> { PasswordOption.NoDigits,
PasswordOption.NoUppercaseLetters,
PasswordOption.NoLowercaseLetters -> {
numCharacterCategories++ numCharacterCategories++
} }
PasswordOption.NoAmbiguousCharacters, PasswordOption.NoAmbiguousCharacters,
@ -98,7 +103,9 @@ object PasswordGenerator {
throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_no_chars_error)) throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_no_chars_error))
} }
if (length < numCharacterCategories) { if (length < numCharacterCategories) {
throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_length_too_short_error)) throw PasswordGeneratorException(
ctx.resources.getString(R.string.pwgen_length_too_short_error)
)
} }
if (!(pwgenFlags hasFlag UPPERS) && !(pwgenFlags hasFlag LOWERS)) { if (!(pwgenFlags hasFlag UPPERS) && !(pwgenFlags hasFlag LOWERS)) {
phonemes = false phonemes = false
@ -114,7 +121,9 @@ object PasswordGenerator {
var iterations = 0 var iterations = 0
do { do {
if (iterations++ > 1000) if (iterations++ > 1000)
throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_max_iterations_exceeded)) throw PasswordGeneratorException(
ctx.resources.getString(R.string.pwgen_max_iterations_exceeded)
)
password = password =
if (phonemes) { if (phonemes) {
RandomPhonemesGenerator.generate(length, pwgenFlags) RandomPhonemesGenerator.generate(length, pwgenFlags)

View file

@ -36,7 +36,9 @@ object RandomPasswordGenerator {
var password = "" var password = ""
while (password.length < targetLength) { while (password.length < targetLength) {
val candidate = bank.secureRandomCharacter() val candidate = bank.secureRandomCharacter()
if (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && candidate in PasswordGenerator.AMBIGUOUS_STR) { if (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS &&
candidate in PasswordGenerator.AMBIGUOUS_STR
) {
continue continue
} }
password += candidate password += candidate

View file

@ -100,7 +100,9 @@ object RandomPhonemesGenerator {
if (!candidate.flags.hasFlag(nextBasicType) || if (!candidate.flags.hasFlag(nextBasicType) ||
(isStartOfPart && candidate.flags hasFlag NOT_FIRST) || (isStartOfPart && candidate.flags hasFlag NOT_FIRST) ||
// Don't let a diphthong that starts with a vowel follow a vowel. // Don't let a diphthong that starts with a vowel follow a vowel.
(previousFlags hasFlag VOWEL && candidate.flags hasFlag VOWEL && candidate.flags hasFlag DIPHTHONG) || (previousFlags hasFlag VOWEL &&
candidate.flags hasFlag VOWEL &&
candidate.flags hasFlag DIPHTHONG) ||
// Don't add multi-character candidates if we would go over the targetLength. // Don't add multi-character candidates if we would go over the targetLength.
(password.length + candidate.length > targetLength) || (password.length + candidate.length > targetLength) ||
(pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && candidate.isAmbiguous) (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && candidate.isAmbiguous)
@ -129,11 +131,15 @@ object RandomPhonemesGenerator {
// Second part: Add digits and symbols with a certain probability (if requested) if // Second part: Add digits and symbols with a certain probability (if requested) if
// they would not directly follow the first character in a pronounceable part. // they would not directly follow the first character in a pronounceable part.
if (!isStartOfPart && pwFlags hasFlag PasswordGenerator.DIGITS && secureRandomBiasedBoolean(30)) { if (!isStartOfPart &&
pwFlags hasFlag PasswordGenerator.DIGITS &&
secureRandomBiasedBoolean(30)
) {
var randomDigit: Char var randomDigit: Char
do { do {
randomDigit = secureRandomNumber(10).toString(10).first() randomDigit = secureRandomNumber(10).toString(10).first()
} while (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && randomDigit in PasswordGenerator.AMBIGUOUS_STR) } while (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS &&
randomDigit in PasswordGenerator.AMBIGUOUS_STR)
password += randomDigit password += randomDigit
// Begin a new pronounceable part after every digit. // Begin a new pronounceable part after every digit.
@ -143,11 +149,15 @@ object RandomPhonemesGenerator {
continue continue
} }
if (!isStartOfPart && pwFlags hasFlag PasswordGenerator.SYMBOLS && secureRandomBiasedBoolean(20)) { if (!isStartOfPart &&
pwFlags hasFlag PasswordGenerator.SYMBOLS &&
secureRandomBiasedBoolean(20)
) {
var randomSymbol: Char var randomSymbol: Char
do { do {
randomSymbol = PasswordGenerator.SYMBOLS_STR.secureRandomCharacter() randomSymbol = PasswordGenerator.SYMBOLS_STR.secureRandomCharacter()
} while (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && randomSymbol in PasswordGenerator.AMBIGUOUS_STR) } while (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS &&
randomSymbol in PasswordGenerator.AMBIGUOUS_STR)
password += randomSymbol password += randomSymbol
// Continue the password generation as if nothing was added. // Continue the password generation as if nothing was added.
} }
@ -157,8 +167,9 @@ object RandomPhonemesGenerator {
nextBasicType = nextBasicType =
when { when {
candidate.flags.hasFlag(CONSONANT) -> VOWEL candidate.flags.hasFlag(CONSONANT) -> VOWEL
previousFlags.hasFlag(VOWEL) || candidate.flags.hasFlag(DIPHTHONG) || secureRandomBiasedBoolean(60) -> previousFlags.hasFlag(VOWEL) ||
CONSONANT candidate.flags.hasFlag(DIPHTHONG) ||
secureRandomBiasedBoolean(60) -> CONSONANT
else -> VOWEL else -> VOWEL
} }
previousFlags = candidate.flags previousFlags = candidate.flags

View file

@ -109,7 +109,9 @@ class PasswordBuilder(ctx: Context) {
} }
else candidate else candidate
CapsType.TitleCase -> CapsType.TitleCase ->
candidate.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } candidate.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
CapsType.lowercase -> candidate.lowercase(Locale.getDefault()) CapsType.lowercase -> candidate.lowercase(Locale.getDefault())
CapsType.As_iS -> candidate CapsType.As_iS -> candidate
} }

View file

@ -30,7 +30,10 @@ class XkpwdDictionary(context: Context) {
context.resources.openRawResource(R.raw.xkpwdict).bufferedReader().readLines() context.resources.openRawResource(R.raw.xkpwdict).bufferedReader().readLines()
} }
words = lines.asSequence().map { it.trim() }.filter { it.isNotEmpty() && !it.contains(' ') }.groupBy { it.length } words =
lines.asSequence().map { it.trim() }.filter { it.isNotEmpty() && !it.contains(' ') }.groupBy {
it.length
}
} }
companion object { companion object {

View file

@ -140,7 +140,10 @@ class ClipboardService : Service() {
} }
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
private fun createNotificationApi24(pendingIntent: PendingIntent, clearTimeMs: Long): Notification { private fun createNotificationApi24(
pendingIntent: PendingIntent,
clearTimeMs: Long
): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID) return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.app_name)) .setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.tap_clear_clipboard)) .setContentText(getString(R.string.tap_clear_clipboard))
@ -157,7 +160,11 @@ class ClipboardService : Service() {
private fun createNotificationChannel() { private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = val serviceChannel =
NotificationChannel(CHANNEL_ID, getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW) NotificationChannel(
CHANNEL_ID,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService<NotificationManager>() val manager = getSystemService<NotificationManager>()
if (manager != null) { if (manager != null) {
manager.createNotificationChannel(serviceChannel) manager.createNotificationChannel(serviceChannel)

View file

@ -60,7 +60,11 @@ class OreoAutofillService : AutofillService() {
cachePublicSuffixList(applicationContext) cachePublicSuffixList(applicationContext)
} }
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback) { override fun onFillRequest(
request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback
) {
val structure = val structure =
request.fillContexts.lastOrNull()?.structure request.fillContexts.lastOrNull()?.structure
?: run { ?: run {
@ -93,7 +97,8 @@ class OreoAutofillService : AutofillService() {
return return
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Api30AutofillResponseBuilder(formToFill).fillCredentials(this, request.inlineSuggestionsRequest, callback) Api30AutofillResponseBuilder(formToFill)
.fillCredentials(this, request.inlineSuggestionsRequest, callback)
} else { } else {
AutofillResponseBuilder(formToFill).fillCredentials(this, callback) AutofillResponseBuilder(formToFill).fillCredentials(this, callback)
} }
@ -148,11 +153,13 @@ class OreoAutofillService : AutofillService() {
} }
} }
fun Context.getDefaultUsername() = sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME) fun Context.getDefaultUsername() =
sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME)
fun Context.getCustomSuffixes(): Sequence<String> { fun Context.getCustomSuffixes(): Sequence<String> {
return sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)?.splitToSequence('\n')?.filter { return sharedPrefs
it.isNotBlank() && it.first() != '.' && it.last() != '.' .getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)
} ?.splitToSequence('\n')
?.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' }
?: emptySequence() ?: emptySequence()
} }

View file

@ -137,7 +137,11 @@ class PasswordExportService : Service() {
private fun createNotificationChannel() { private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = val serviceChannel =
NotificationChannel(CHANNEL_ID, getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW) NotificationChannel(
CHANNEL_ID,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService<NotificationManager>() val manager = getSystemService<NotificationManager>()
if (manager != null) { if (manager != null) {
manager.createNotificationChannel(serviceChannel) manager.createNotificationChannel(serviceChannel)

View file

@ -25,7 +25,8 @@ enum class Protocol(val pref: String) {
private val map = values().associateBy(Protocol::pref) private val map = values().associateBy(Protocol::pref)
fun fromString(type: String?): Protocol { fun fromString(type: String?): Protocol {
return map[type ?: return Ssh] ?: throw IllegalArgumentException("$type is not a valid Protocol") return map[type ?: return Ssh]
?: throw IllegalArgumentException("$type is not a valid Protocol")
} }
} }
} }
@ -41,7 +42,8 @@ enum class AuthMode(val pref: String) {
private val map = values().associateBy(AuthMode::pref) private val map = values().associateBy(AuthMode::pref)
fun fromString(type: String?): AuthMode { fun fromString(type: String?): AuthMode {
return map[type ?: return SshKey] ?: throw IllegalArgumentException("$type is not a valid AuthMode") return map[type ?: return SshKey]
?: throw IllegalArgumentException("$type is not a valid AuthMode")
} }
} }
} }
@ -50,12 +52,18 @@ object GitSettings {
private const val DEFAULT_BRANCH = "master" private const val DEFAULT_BRANCH = "master"
private val settings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.sharedPrefs } private val settings by lazy(LazyThreadSafetyMode.PUBLICATION) {
Application.instance.sharedPrefs
}
private val encryptedSettings by lazy(LazyThreadSafetyMode.PUBLICATION) { private val encryptedSettings by lazy(LazyThreadSafetyMode.PUBLICATION) {
Application.instance.getEncryptedGitPrefs() Application.instance.getEncryptedGitPrefs()
} }
private val proxySettings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.getEncryptedProxyPrefs() } private val proxySettings by lazy(LazyThreadSafetyMode.PUBLICATION) {
private val hostKeyPath by lazy(LazyThreadSafetyMode.NONE) { "${Application.instance.filesDir}/.host_key" } Application.instance.getEncryptedProxyPrefs()
}
private val hostKeyPath by lazy(LazyThreadSafetyMode.NONE) {
"${Application.instance.filesDir}/.host_key"
}
var authMode var authMode
get() = AuthMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH)) get() = AuthMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH))

View file

@ -60,7 +60,8 @@ private fun migrateToGitUrlBasedConfig(sharedPrefs: SharedPreferences) {
val url = val url =
when { when {
urlWithFreeEntryScheme.startsWith("https://") -> urlWithFreeEntryScheme urlWithFreeEntryScheme.startsWith("https://") -> urlWithFreeEntryScheme
urlWithFreeEntryScheme.startsWith("http://") -> urlWithFreeEntryScheme.replaceFirst("http", "https") urlWithFreeEntryScheme.startsWith("http://") ->
urlWithFreeEntryScheme.replaceFirst("http", "https")
else -> "https://$urlWithFreeEntryScheme" else -> "https://$urlWithFreeEntryScheme"
} }
runCatching { if (URI(url).rawAuthority != null) url else null }.get() runCatching { if (URI(url).rawAuthority != null) url else null }.get()
@ -96,7 +97,10 @@ private fun migrateToHideAll(sharedPrefs: SharedPreferences) {
private fun migrateToSshKey(context: Context, sharedPrefs: SharedPreferences) { private fun migrateToSshKey(context: Context, sharedPrefs: SharedPreferences) {
val privateKeyFile = File(context.filesDir, ".ssh_key") val privateKeyFile = File(context.filesDir, ".ssh_key")
if (sharedPrefs.contains(PreferenceKeys.USE_GENERATED_KEY) && !SshKey.exists && privateKeyFile.exists()) { if (sharedPrefs.contains(PreferenceKeys.USE_GENERATED_KEY) &&
!SshKey.exists &&
privateKeyFile.exists()
) {
// Currently uses a private key imported or generated with an old version of Password Store. // Currently uses a private key imported or generated with an old version of Password Store.
// Generated keys come with a public key which the user should still be able to view after // Generated keys come with a public key which the user should still be able to view after
// the migration (not possible for regular imported keys), hence the special case. // the migration (not possible for regular imported keys), hence the special case.

View file

@ -18,10 +18,15 @@ enum class PasswordSortOrder(val comparator: java.util.Comparator<PasswordItem>)
(p1.type + p1.name).compareTo(p2.type + p2.name, ignoreCase = true) (p1.type + p1.name).compareTo(p2.type + p2.name, ignoreCase = true)
} }
), ),
INDEPENDENT(Comparator { p1: PasswordItem, p2: PasswordItem -> p1.name.compareTo(p2.name, ignoreCase = true) }), INDEPENDENT(
Comparator { p1: PasswordItem, p2: PasswordItem ->
p1.name.compareTo(p2.name, ignoreCase = true)
}
),
RECENTLY_USED( RECENTLY_USED(
Comparator { p1: PasswordItem, p2: PasswordItem -> Comparator { p1: PasswordItem, p2: PasswordItem ->
val recentHistory = Application.instance.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) val recentHistory =
Application.instance.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
val timeP1 = recentHistory.getString(p1.file.absolutePath.base64()) val timeP1 = recentHistory.getString(p1.file.absolutePath.base64())
val timeP2 = recentHistory.getString(p2.file.absolutePath.base64()) val timeP2 = recentHistory.getString(p2.file.absolutePath.base64())
when { when {

View file

@ -44,7 +44,8 @@ class UriTotpFinder @Inject constructor() : TotpFinder {
override fun findAlgorithm(content: String): String { override fun findAlgorithm(content: String): String {
content.split("\n".toRegex()).forEach { line -> content.split("\n".toRegex()).forEach { line ->
if (line.startsWith(TOTP_FIELDS[0]) && Uri.parse(line).getQueryParameter("algorithm") != null) { if (line.startsWith(TOTP_FIELDS[0]) && Uri.parse(line).getQueryParameter("algorithm") != null
) {
return Uri.parse(line).getQueryParameter("algorithm")!! return Uri.parse(line).getQueryParameter("algorithm")!!
} }
} }

View file

@ -75,8 +75,14 @@ private fun PasswordItem.Companion.makeComparator(
PasswordSortOrder.FILE_FIRST -> compareByDescending { it.type } PasswordSortOrder.FILE_FIRST -> compareByDescending { it.type }
PasswordSortOrder.RECENTLY_USED -> PasswordSortOrder.RECENTLY_USED.comparator PasswordSortOrder.RECENTLY_USED -> PasswordSortOrder.RECENTLY_USED.comparator
} }
.then(compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getIdentifierFor(it.file) }) .then(
.then(compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getUsernameFor(it.file) }) compareBy(nullsLast(CaseInsensitiveComparator)) {
directoryStructure.getIdentifierFor(it.file)
}
)
.then(
compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getUsernameFor(it.file) }
)
} }
val PasswordItem.stableId: String val PasswordItem.stableId: String
@ -179,7 +185,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
.mapLatest { searchAction -> .mapLatest { searchAction ->
val listResultFlow = val listResultFlow =
when (searchAction.searchMode) { when (searchAction.searchMode) {
SearchMode.RecursivelyInSubdirectories -> listFilesRecursively(searchAction.baseDirectory) SearchMode.RecursivelyInSubdirectories ->
listFilesRecursively(searchAction.baseDirectory)
SearchMode.InCurrentDirectoryOnly -> listFiles(searchAction.baseDirectory) SearchMode.InCurrentDirectoryOnly -> listFiles(searchAction.baseDirectory)
} }
val prefilteredResultFlow = val prefilteredResultFlow =
@ -188,9 +195,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
ListMode.DirectoriesOnly -> listResultFlow.filter { it.isDirectory } ListMode.DirectoriesOnly -> listResultFlow.filter { it.isDirectory }
ListMode.AllEntries -> listResultFlow ListMode.AllEntries -> listResultFlow
} }
val filterModeToUse = if (searchAction.filter == "") FilterMode.NoFilter else searchAction.filterMode
val passwordList = val passwordList =
when (filterModeToUse) { when (if (searchAction.filter == "") FilterMode.NoFilter else searchAction.filterMode) {
FilterMode.NoFilter -> { FilterMode.NoFilter -> {
prefilteredResultFlow.map { it.toPasswordItem() }.toList().sortedWith(itemComparator) prefilteredResultFlow.map { it.toPasswordItem() }.toList().sortedWith(itemComparator)
} }
@ -201,7 +207,9 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
val regex = generateStrictDomainRegex(searchAction.filter) val regex = generateStrictDomainRegex(searchAction.filter)
if (regex != null) { if (regex != null) {
prefilteredResultFlow prefilteredResultFlow
.filter { absoluteFile -> regex.containsMatchIn(absoluteFile.relativeTo(root).path) } .filter { absoluteFile ->
regex.containsMatchIn(absoluteFile.relativeTo(root).path)
}
.map { it.toPasswordItem() } .map { it.toPasswordItem() }
.toList() .toList()
.sortedWith(itemComparator) .sortedWith(itemComparator)
@ -218,7 +226,9 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
.filter { it.first > 0 } .filter { it.first > 0 }
.toList() .toList()
.sortedWith( .sortedWith(
compareByDescending<Pair<Int, PasswordItem>> { it.first }.thenBy(itemComparator) { it.second } compareByDescending<Pair<Int, PasswordItem>> { it.first }.thenBy(itemComparator) {
it.second
}
) )
.map { it.second } .map { it.second }
} }
@ -387,7 +397,9 @@ open class SearchableRepositoryAdapter<T : RecyclerView.ViewHolder>(
addObserver( addObserver(
object : SelectionTracker.SelectionObserver<String>() { object : SelectionTracker.SelectionObserver<String>() {
override fun onSelectionChanged() { override fun onSelectionChanged() {
this@SearchableRepositoryAdapter.onSelectionChangedListener?.invoke(requireSelectionTracker().selection) this@SearchableRepositoryAdapter.onSelectionChangedListener?.invoke(
requireSelectionTracker().selection
)
} }
} }
) )
@ -395,15 +407,23 @@ open class SearchableRepositoryAdapter<T : RecyclerView.ViewHolder>(
} }
private var onItemClickedListener: ((holder: T, item: PasswordItem) -> Unit)? = null private var onItemClickedListener: ((holder: T, item: PasswordItem) -> Unit)? = null
open fun onItemClicked(listener: (holder: T, item: PasswordItem) -> Unit): SearchableRepositoryAdapter<T> { open fun onItemClicked(
check(onItemClickedListener == null) { "Only a single listener can be registered for onItemClicked" } listener: (holder: T, item: PasswordItem) -> Unit
): SearchableRepositoryAdapter<T> {
check(onItemClickedListener == null) {
"Only a single listener can be registered for onItemClicked"
}
onItemClickedListener = listener onItemClickedListener = listener
return this return this
} }
private var onSelectionChangedListener: ((selection: Selection<String>) -> Unit)? = null private var onSelectionChangedListener: ((selection: Selection<String>) -> Unit)? = null
open fun onSelectionChanged(listener: (selection: Selection<String>) -> Unit): SearchableRepositoryAdapter<T> { open fun onSelectionChanged(
check(onSelectionChangedListener == null) { "Only a single listener can be registered for onSelectionChanged" } listener: (selection: Selection<String>) -> Unit
): SearchableRepositoryAdapter<T> {
check(onSelectionChangedListener == null) {
"Only a single listener can be registered for onSelectionChanged"
}
onSelectionChangedListener = listener onSelectionChangedListener = listener
return this return this
} }

View file

@ -96,4 +96,3 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>

View file

@ -77,7 +77,12 @@ class AutofillSmsActivity : AppCompatActivity() {
fun makeFillOtpFromSmsIntentSender(context: Context): IntentSender { fun makeFillOtpFromSmsIntentSender(context: Context): IntentSender {
val intent = Intent(context, AutofillSmsActivity::class.java) val intent = Intent(context, AutofillSmsActivity::class.java)
return PendingIntent.getActivity(context, fillOtpFromSmsRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT) return PendingIntent.getActivity(
context,
fillOtpFromSmsRequestCode++,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
)
.intentSender .intentSender
} }
} }
@ -122,15 +127,17 @@ class AutofillSmsActivity : AppCompatActivity() {
private suspend fun waitForSms() { private suspend fun waitForSms() {
val smsClient = SmsCodeRetriever.getAutofillClient(this@AutofillSmsActivity) val smsClient = SmsCodeRetriever.getAutofillClient(this@AutofillSmsActivity)
runCatching { withContext(Dispatchers.IO) { smsClient.startSmsCodeRetriever().suspendableAwait() } }.onFailure { e runCatching {
-> withContext(Dispatchers.IO) { smsClient.startSmsCodeRetriever().suspendableAwait() }
if (e is ResolvableApiException) {
e.startResolutionForResult(this@AutofillSmsActivity, 1)
} else {
e(e)
withContext(Dispatchers.Main) { finish() }
}
} }
.onFailure { e ->
if (e is ResolvableApiException) {
e.startResolutionForResult(this@AutofillSmsActivity, 1)
} else {
e(e)
withContext(Dispatchers.Main) { finish() }
}
}
} }
private val smsCodeRetrievedReceiver = private val smsCodeRetrievedReceiver =
@ -144,7 +151,10 @@ class AutofillSmsActivity : AppCompatActivity() {
clientState, clientState,
AutofillAction.FillOtpFromSms AutofillAction.FillOtpFromSms
) )
setResult(RESULT_OK, Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }) setResult(
RESULT_OK,
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }
)
finish() finish()
} }
} }

View file

@ -41,7 +41,8 @@ public sealed class FormOrigin(public open val identifier: String) {
when (this) { when (this) {
is Web -> identifier is Web -> identifier
is App -> { is App -> {
val info = context.packageManager.getApplicationInfo(identifier, PackageManager.GET_META_DATA) val info =
context.packageManager.getApplicationInfo(identifier, PackageManager.GET_META_DATA)
val label = context.packageManager.getApplicationLabel(info) val label = context.packageManager.getApplicationLabel(info)
if (untrusted) "$label" else "$label" if (untrusted) "$label" else "$label"
} }
@ -174,7 +175,10 @@ private class AutofillFormParser(
// the single origin among the detected fillable or saveable fields. If this origin // the single origin among the detected fillable or saveable fields. If this origin
// is null, but we encountered web origins elsewhere in the AssistStructure, the // is null, but we encountered web origins elsewhere in the AssistStructure, the
// situation is uncertain and Autofill should not be offered. // situation is uncertain and Autofill should not be offered.
webOriginToFormOrigin(context, scenario.allFields.map { it.webOrigin }.toSet().singleOrNull() ?: return null) webOriginToFormOrigin(
context,
scenario.allFields.map { it.webOrigin }.toSet().singleOrNull() ?: return null
)
} }
} }
} }
@ -204,7 +208,12 @@ private constructor(
): FillableForm? { ): FillableForm? {
val form = AutofillFormParser(context, structure, isManualRequest, customSuffixes) val form = AutofillFormParser(context, structure, isManualRequest, customSuffixes)
if (form.formOrigin == null || form.scenario == null) return null if (form.formOrigin == null || form.scenario == null) return null
return FillableForm(form.formOrigin, form.scenario.map { it.autofillId }, form.ignoredIds, form.saveFlags) return FillableForm(
form.formOrigin,
form.scenario.map { it.autofillId },
form.ignoredIds,
form.saveFlags
)
} }
} }

View file

@ -49,14 +49,19 @@ public fun computeCertificatesHash(context: Context, appPackage: String): String
// hashes comparable between versions and hence default to using the deprecated API. // hashes comparable between versions and hence default to using the deprecated API.
@SuppressLint("PackageManagerGetSignatures") @SuppressLint("PackageManagerGetSignatures")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val signaturesOld = context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNATURES).signatures val signaturesOld =
context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNATURES).signatures
val stableHashOld = stableHash(signaturesOld.map { it.toByteArray() }) val stableHashOld = stableHash(signaturesOld.map { it.toByteArray() })
if (Build.VERSION.SDK_INT >= 28) { if (Build.VERSION.SDK_INT >= 28) {
val info = context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNING_CERTIFICATES) val info =
val signaturesNew = info.signingInfo.signingCertificateHistory ?: info.signingInfo.apkContentsSigners context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNING_CERTIFICATES)
val signaturesNew =
info.signingInfo.signingCertificateHistory ?: info.signingInfo.apkContentsSigners
val stableHashNew = stableHash(signaturesNew.map { it.toByteArray() }) val stableHashNew = stableHash(signaturesNew.map { it.toByteArray() })
if (stableHashNew != stableHashOld) if (stableHashNew != stableHashOld)
tag("CertificatesHash").e { "Mismatch between old and new hash: $stableHashNew != $stableHashOld" } tag("CertificatesHash").e {
"Mismatch between old and new hash: $stableHashNew != $stableHashOld"
}
} }
return stableHashOld return stableHashOld
} }
@ -106,7 +111,10 @@ private fun visitViewNodes(structure: AssistStructure, block: (AssistStructure.V
} }
} }
private fun visitViewNode(node: AssistStructure.ViewNode, block: (AssistStructure.ViewNode) -> Unit) { private fun visitViewNode(
node: AssistStructure.ViewNode,
block: (AssistStructure.ViewNode) -> Unit
) {
block(node) block(node)
for (i in 0 until node.childCount) { for (i in 0 until node.childCount) {
visitViewNode(node.getChildAt(i), block) visitViewNode(node.getChildAt(i), block)
@ -114,7 +122,9 @@ private fun visitViewNode(node: AssistStructure.ViewNode, block: (AssistStructur
} }
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
internal fun AssistStructure.findNodeByAutofillId(autofillId: AutofillId): AssistStructure.ViewNode? { internal fun AssistStructure.findNodeByAutofillId(
autofillId: AutofillId
): AssistStructure.ViewNode? {
var node: AssistStructure.ViewNode? = null var node: AssistStructure.ViewNode? = null
visitViewNodes(this) { if (it.autofillId == autofillId) node = it } visitViewNodes(this) { if (it.autofillId == autofillId) node = it }
return node return node

View file

@ -56,9 +56,15 @@ public sealed class AutofillScenario<out T : Any> {
username = clientState.getParcelable(BUNDLE_KEY_USERNAME_ID) username = clientState.getParcelable(BUNDLE_KEY_USERNAME_ID)
fillUsername = clientState.getBoolean(BUNDLE_KEY_FILL_USERNAME) fillUsername = clientState.getBoolean(BUNDLE_KEY_FILL_USERNAME)
otp = clientState.getParcelable(BUNDLE_KEY_OTP_ID) otp = clientState.getParcelable(BUNDLE_KEY_OTP_ID)
currentPassword.addAll(clientState.getParcelableArrayList(BUNDLE_KEY_CURRENT_PASSWORD_IDS) ?: emptyList()) currentPassword.addAll(
newPassword.addAll(clientState.getParcelableArrayList(BUNDLE_KEY_NEW_PASSWORD_IDS) ?: emptyList()) clientState.getParcelableArrayList(BUNDLE_KEY_CURRENT_PASSWORD_IDS) ?: emptyList()
genericPassword.addAll(clientState.getParcelableArrayList(BUNDLE_KEY_GENERIC_PASSWORD_IDS) ?: emptyList()) )
newPassword.addAll(
clientState.getParcelableArrayList(BUNDLE_KEY_NEW_PASSWORD_IDS) ?: emptyList()
)
genericPassword.addAll(
clientState.getParcelableArrayList(BUNDLE_KEY_GENERIC_PASSWORD_IDS) ?: emptyList()
)
} }
.build() .build()
} catch (e: Throwable) { } catch (e: Throwable) {
@ -227,7 +233,9 @@ public fun Dataset.Builder.fillWith(
} }
} }
internal inline fun <T : Any, S : Any> AutofillScenario<T>.map(transform: (T) -> S): AutofillScenario<S> { internal inline fun <T : Any, S : Any> AutofillScenario<T>.map(
transform: (T) -> S
): AutofillScenario<S> {
val builder = AutofillScenario.Builder<S>() val builder = AutofillScenario.Builder<S>()
builder.username = username?.let(transform) builder.username = username?.let(transform)
builder.fillUsername = fillUsername builder.fillUsername = fillUsername
@ -253,7 +261,10 @@ internal fun AutofillScenario<AutofillId>.toBundle(): Bundle =
putParcelable(AutofillScenario.BUNDLE_KEY_USERNAME_ID, username) putParcelable(AutofillScenario.BUNDLE_KEY_USERNAME_ID, username)
putBoolean(AutofillScenario.BUNDLE_KEY_FILL_USERNAME, fillUsername) putBoolean(AutofillScenario.BUNDLE_KEY_FILL_USERNAME, fillUsername)
putParcelable(AutofillScenario.BUNDLE_KEY_OTP_ID, otp) putParcelable(AutofillScenario.BUNDLE_KEY_OTP_ID, otp)
putParcelableArrayList(AutofillScenario.BUNDLE_KEY_CURRENT_PASSWORD_IDS, ArrayList(currentPassword)) putParcelableArrayList(
AutofillScenario.BUNDLE_KEY_CURRENT_PASSWORD_IDS,
ArrayList(currentPassword)
)
putParcelableArrayList(AutofillScenario.BUNDLE_KEY_NEW_PASSWORD_IDS, ArrayList(newPassword)) putParcelableArrayList(AutofillScenario.BUNDLE_KEY_NEW_PASSWORD_IDS, ArrayList(newPassword))
} }
} }
@ -262,7 +273,10 @@ internal fun AutofillScenario<AutofillId>.toBundle(): Bundle =
putParcelable(AutofillScenario.BUNDLE_KEY_USERNAME_ID, username) putParcelable(AutofillScenario.BUNDLE_KEY_USERNAME_ID, username)
putBoolean(AutofillScenario.BUNDLE_KEY_FILL_USERNAME, fillUsername) putBoolean(AutofillScenario.BUNDLE_KEY_FILL_USERNAME, fillUsername)
putParcelable(AutofillScenario.BUNDLE_KEY_OTP_ID, otp) putParcelable(AutofillScenario.BUNDLE_KEY_OTP_ID, otp)
putParcelableArrayList(AutofillScenario.BUNDLE_KEY_GENERIC_PASSWORD_IDS, ArrayList(genericPassword)) putParcelableArrayList(
AutofillScenario.BUNDLE_KEY_GENERIC_PASSWORD_IDS,
ArrayList(genericPassword)
)
} }
} }
} }

View file

@ -9,11 +9,14 @@ import androidx.annotation.RequiresApi
import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Certain import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Certain
import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Likely import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Likely
private inline fun <T> Pair<T, T>.all(predicate: T.() -> Boolean) = predicate(first) && predicate(second) private inline fun <T> Pair<T, T>.all(predicate: T.() -> Boolean) =
predicate(first) && predicate(second)
private inline fun <T> Pair<T, T>.any(predicate: T.() -> Boolean) = predicate(first) || predicate(second) private inline fun <T> Pair<T, T>.any(predicate: T.() -> Boolean) =
predicate(first) || predicate(second)
private inline fun <T> Pair<T, T>.none(predicate: T.() -> Boolean) = !predicate(first) && !predicate(second) private inline fun <T> Pair<T, T>.none(predicate: T.() -> Boolean) =
!predicate(first) && !predicate(second)
/** /**
* The strategy used to detect [AutofillScenario] s; expressed using the DSL implemented in * The strategy used to detect [AutofillScenario] s; expressed using the DSL implemented in
@ -32,7 +35,8 @@ internal val autofillStrategy = strategy {
} }
currentPassword(optional = true) { currentPassword(optional = true) {
takeSingle { alreadyMatched -> takeSingle { alreadyMatched ->
val adjacentToNewPasswords = directlyPrecedes(alreadyMatched) || directlyFollows(alreadyMatched) val adjacentToNewPasswords =
directlyPrecedes(alreadyMatched) || directlyFollows(alreadyMatched)
// The Autofill framework has not hint that applies to current passwords only. // The Autofill framework has not hint that applies to current passwords only.
// In this scenario, we have already matched fields a pair of fields with a specific // In this scenario, we have already matched fields a pair of fields with a specific
// new password hint, so we take a generic Autofill password hint to mean a current // new password hint, so we take a generic Autofill password hint to mean a current
@ -109,7 +113,9 @@ internal val autofillStrategy = strategy {
rule(applyInSingleOriginMode = true) { rule(applyInSingleOriginMode = true) {
newPassword { takeSingle { hasHintNewPassword && isFocused } } newPassword { takeSingle { hasHintNewPassword && isFocused } }
username(optional = true) { username(optional = true) {
takeSingle { alreadyMatched -> usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull()) } takeSingle { alreadyMatched ->
usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull())
}
} }
} }
@ -119,7 +125,9 @@ internal val autofillStrategy = strategy {
rule(applyInSingleOriginMode = true) { rule(applyInSingleOriginMode = true) {
currentPassword { takeSingle { hasAutocompleteHintCurrentPassword && isFocused } } currentPassword { takeSingle { hasAutocompleteHintCurrentPassword && isFocused } }
username(optional = true) { username(optional = true) {
takeSingle { alreadyMatched -> usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull()) } takeSingle { alreadyMatched ->
usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull())
}
} }
} }
@ -129,7 +137,9 @@ internal val autofillStrategy = strategy {
rule(applyInSingleOriginMode = true) { rule(applyInSingleOriginMode = true) {
genericPassword { takeSingle { passwordCertainty >= Likely && isFocused } } genericPassword { takeSingle { passwordCertainty >= Likely && isFocused } }
username(optional = true) { username(optional = true) {
takeSingle { alreadyMatched -> usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull()) } takeSingle { alreadyMatched ->
usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull())
}
} }
} }
@ -139,12 +149,16 @@ internal val autofillStrategy = strategy {
rule { rule {
username { takeSingle { hasHintUsername && isFocused } } username { takeSingle { hasHintUsername && isFocused } }
currentPassword(matchHidden = true) { currentPassword(matchHidden = true) {
takeSingle { alreadyMatched -> directlyFollows(alreadyMatched.singleOrNull()) && couldBeTwoStepHiddenPassword } takeSingle { alreadyMatched ->
directlyFollows(alreadyMatched.singleOrNull()) && couldBeTwoStepHiddenPassword
}
} }
} }
// Match a single focused OTP field. // Match a single focused OTP field.
rule(applyInSingleOriginMode = true) { otp { takeSingle { otpCertainty >= Likely && isFocused } } } rule(applyInSingleOriginMode = true) {
otp { takeSingle { otpCertainty >= Likely && isFocused } }
}
// Match a single focused username field without a password field. // Match a single focused username field without a password field.
rule(applyInSingleOriginMode = true) { rule(applyInSingleOriginMode = true) {
@ -162,7 +176,9 @@ internal val autofillStrategy = strategy {
// This rule can apply in single origin mode since even though the password field may not be // This rule can apply in single origin mode since even though the password field may not be
// focused at the time the rule runs, the fill suggestion will only show if it ever receives // focused at the time the rule runs, the fill suggestion will only show if it ever receives
// focus. // focus.
rule(applyInSingleOriginMode = true) { currentPassword { takeSingle { hasAutocompleteHintCurrentPassword } } } rule(applyInSingleOriginMode = true) {
currentPassword { takeSingle { hasAutocompleteHintCurrentPassword } }
}
// See above. // See above.
rule(applyInSingleOriginMode = true) { genericPassword { takeSingle { true } } } rule(applyInSingleOriginMode = true) { genericPassword { takeSingle { true } } }
@ -171,10 +187,14 @@ internal val autofillStrategy = strategy {
rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) { rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) {
genericPassword { takeSingle { isFocused } } genericPassword { takeSingle { isFocused } }
username(optional = true) { username(optional = true) {
takeSingle { alreadyMatched -> usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull()) } takeSingle { alreadyMatched ->
usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull())
}
} }
} }
// Match any focused username field on manual request. // Match any focused username field on manual request.
rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) { username { takeSingle { isFocused } } } rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) {
username { takeSingle { isFocused } }
}
} }

View file

@ -20,28 +20,41 @@ internal interface FieldMatcher {
class Builder { class Builder {
private var takeSingle: (FormField.(List<FormField>) -> Boolean)? = null private var takeSingle: (FormField.(List<FormField>) -> Boolean)? = null
private val tieBreakersSingle: MutableList<FormField.(List<FormField>) -> Boolean> = mutableListOf() private val tieBreakersSingle: MutableList<FormField.(List<FormField>) -> Boolean> =
mutableListOf()
private var takePair: (Pair<FormField, FormField>.(List<FormField>) -> Boolean)? = null private var takePair: (Pair<FormField, FormField>.(List<FormField>) -> Boolean)? = null
private var tieBreakersPair: MutableList<Pair<FormField, FormField>.(List<FormField>) -> Boolean> = mutableListOf() private var tieBreakersPair:
MutableList<Pair<FormField, FormField>.(List<FormField>) -> Boolean> =
mutableListOf()
fun takeSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean = { true }) { fun takeSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean = { true }) {
check(takeSingle == null && takePair == null) { "Every block can only have at most one take{Single,Pair} block" } check(takeSingle == null && takePair == null) {
"Every block can only have at most one take{Single,Pair} block"
}
takeSingle = block takeSingle = block
} }
fun breakTieOnSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean) { fun breakTieOnSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean) {
check(takeSingle != null) { "Every block needs a takeSingle block before a breakTieOnSingle block" } check(takeSingle != null) {
"Every block needs a takeSingle block before a breakTieOnSingle block"
}
check(takePair == null) { "takePair cannot be mixed with breakTieOnSingle" } check(takePair == null) { "takePair cannot be mixed with breakTieOnSingle" }
tieBreakersSingle.add(block) tieBreakersSingle.add(block)
} }
fun takePair(block: Pair<FormField, FormField>.(alreadyMatched: List<FormField>) -> Boolean = { true }) { fun takePair(
check(takeSingle == null && takePair == null) { "Every block can only have at most one take{Single,Pair} block" } block: Pair<FormField, FormField>.(alreadyMatched: List<FormField>) -> Boolean = { true }
) {
check(takeSingle == null && takePair == null) {
"Every block can only have at most one take{Single,Pair} block"
}
takePair = block takePair = block
} }
fun breakTieOnPair(block: Pair<FormField, FormField>.(alreadyMatched: List<FormField>) -> Boolean) { fun breakTieOnPair(
block: Pair<FormField, FormField>.(alreadyMatched: List<FormField>) -> Boolean
) {
check(takePair != null) { "Every block needs a takePair block before a breakTieOnPair block" } check(takePair != null) { "Every block needs a takePair block before a breakTieOnPair block" }
check(takeSingle == null) { "takeSingle cannot be mixed with breakTieOnPair" } check(takeSingle == null) { "takeSingle cannot be mixed with breakTieOnPair" }
tieBreakersPair.add(block) tieBreakersPair.add(block)
@ -69,7 +82,8 @@ internal class SingleFieldMatcher(
class Builder { class Builder {
private var takeSingle: (FormField.(List<FormField>) -> Boolean)? = null private var takeSingle: (FormField.(List<FormField>) -> Boolean)? = null
private val tieBreakersSingle: MutableList<FormField.(List<FormField>) -> Boolean> = mutableListOf() private val tieBreakersSingle: MutableList<FormField.(List<FormField>) -> Boolean> =
mutableListOf()
fun takeSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean = { true }) { fun takeSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean = { true }) {
check(takeSingle == null) { "Every block can only have at most one takeSingle block" } check(takeSingle == null) { "Every block can only have at most one takeSingle block" }
@ -77,7 +91,9 @@ internal class SingleFieldMatcher(
} }
fun breakTieOnSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean) { fun breakTieOnSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean) {
check(takeSingle != null) { "Every block needs a takeSingle block before a breakTieOnSingle block" } check(takeSingle != null) {
"Every block needs a takeSingle block before a breakTieOnSingle block"
}
tieBreakersSingle.add(block) tieBreakersSingle.add(block)
} }
@ -180,7 +196,10 @@ private constructor(
} }
@AutofillDsl @AutofillDsl
class Builder(private val applyInSingleOriginMode: Boolean, private val applyOnManualRequestOnly: Boolean) { class Builder(
private val applyInSingleOriginMode: Boolean,
private val applyOnManualRequestOnly: Boolean
) {
companion object { companion object {
@ -286,9 +305,13 @@ private constructor(
"Rules with applyInSingleOriginMode set to true must not fill into hidden fields" "Rules with applyInSingleOriginMode set to true must not fill into hidden fields"
} }
} }
return AutofillRule(matchers, applyInSingleOriginMode, applyOnManualRequestOnly, name ?: "Rule #$ruleId").also { return AutofillRule(
ruleId++ matchers,
} applyInSingleOriginMode,
applyOnManualRequestOnly,
name ?: "Rule #$ruleId"
)
.also { ruleId++ }
} }
} }
@ -409,4 +432,5 @@ internal class AutofillStrategy private constructor(private val rules: List<Auto
} }
} }
internal fun strategy(block: AutofillStrategy.Builder.() -> Unit) = AutofillStrategy.Builder().apply(block).build() internal fun strategy(block: AutofillStrategy.Builder.() -> Unit) =
AutofillStrategy.Builder().apply(block).build()

View file

@ -63,7 +63,10 @@ private val TRUSTED_BROWSER_CERTIFICATE_HASH =
"com.chrome.canary" to arrayOf("IBnfofsj779wxbzRRDxb6rBPPy/0Nm6aweNFdjmiTPw="), "com.chrome.canary" to arrayOf("IBnfofsj779wxbzRRDxb6rBPPy/0Nm6aweNFdjmiTPw="),
"com.chrome.dev" to arrayOf("kETuX+5LvF4h3URmVDHE6x8fcaMnFqC8knvLs5Izyr8="), "com.chrome.dev" to arrayOf("kETuX+5LvF4h3URmVDHE6x8fcaMnFqC8knvLs5Izyr8="),
"com.duckduckgo.mobile.android" to "com.duckduckgo.mobile.android" to
arrayOf("u3uzHFc8RqHaf8XFKKas9DIQhFb+7FCBDH8zaU6z0tQ=", "8HB9AhwL8+b43MEbo/VwBCXVl9yjAaMeIQVWk067Gwo="), arrayOf(
"u3uzHFc8RqHaf8XFKKas9DIQhFb+7FCBDH8zaU6z0tQ=",
"8HB9AhwL8+b43MEbo/VwBCXVl9yjAaMeIQVWk067Gwo="
),
"com.microsoft.emmx" to arrayOf("AeGZlxCoLCdJtNUMRF3IXWcLYTYInQp2anOCfIKh6sk="), "com.microsoft.emmx" to arrayOf("AeGZlxCoLCdJtNUMRF3IXWcLYTYInQp2anOCfIKh6sk="),
"com.opera.mini.native" to arrayOf("V6y8Ul8bLr0ZGWzW8BQ5fMkQ/RiEHgroUP68Ph5ZP/I="), "com.opera.mini.native" to arrayOf("V6y8Ul8bLr0ZGWzW8BQ5fMkQ/RiEHgroUP68Ph5ZP/I="),
"com.opera.mini.native.beta" to arrayOf("V6y8Ul8bLr0ZGWzW8BQ5fMkQ/RiEHgroUP68Ph5ZP/I="), "com.opera.mini.native.beta" to arrayOf("V6y8Ul8bLr0ZGWzW8BQ5fMkQ/RiEHgroUP68Ph5ZP/I="),
@ -80,7 +83,8 @@ private val TRUSTED_BROWSER_CERTIFICATE_HASH =
"org.mozilla.klar" to arrayOf("YgOkc7421k7jf4f6UA7bx56rkwYQq5ufpMp9XB8bT/w="), "org.mozilla.klar" to arrayOf("YgOkc7421k7jf4f6UA7bx56rkwYQq5ufpMp9XB8bT/w="),
"org.torproject.torbrowser" to arrayOf("IAYfBF5zfGc3XBd5TP7bQ2oDzsa6y3y5+WZCIFyizsg="), "org.torproject.torbrowser" to arrayOf("IAYfBF5zfGc3XBd5TP7bQ2oDzsa6y3y5+WZCIFyizsg="),
"org.ungoogled.chromium.stable" to arrayOf("29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk="), "org.ungoogled.chromium.stable" to arrayOf("29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk="),
"org.ungoogled.chromium.extensions.stable" to arrayOf("29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk="), "org.ungoogled.chromium.extensions.stable" to
arrayOf("29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk="),
"com.kiwibrowser.browser" to arrayOf("wGnqlmMy6R4KDDzFd+b1Cf49ndr3AVrQxcXvj9o/hig="), "com.kiwibrowser.browser" to arrayOf("wGnqlmMy6R4KDDzFd+b1Cf49ndr3AVrQxcXvj9o/hig="),
) )
@ -162,19 +166,30 @@ private val BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY =
private fun isNoAccessibilityServiceEnabled(context: Context): Boolean { private fun isNoAccessibilityServiceEnabled(context: Context): Boolean {
// See https://chromium.googlesource.com/chromium/src/+/447a31e977a65e2eb78804e4a09633699b4ede33 // See https://chromium.googlesource.com/chromium/src/+/447a31e977a65e2eb78804e4a09633699b4ede33
return Settings.Secure.getString(context.contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) return Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
)
.isNullOrEmpty() .isNullOrEmpty()
} }
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun getBrowserSaveFlag(context: Context, appPackage: String): Int? = private fun getBrowserSaveFlag(context: Context, appPackage: String): Int? =
BROWSER_SAVE_FLAG[appPackage] BROWSER_SAVE_FLAG[appPackage]
?: BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY[appPackage]?.takeIf { isNoAccessibilityServiceEnabled(context) } ?: BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY[appPackage]?.takeIf {
isNoAccessibilityServiceEnabled(context)
}
internal data class BrowserAutofillSupportInfo(val multiOriginMethod: BrowserMultiOriginMethod, val saveFlags: Int?) internal data class BrowserAutofillSupportInfo(
val multiOriginMethod: BrowserMultiOriginMethod,
val saveFlags: Int?
)
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
internal fun getBrowserAutofillSupportInfoIfTrusted(context: Context, appPackage: String): BrowserAutofillSupportInfo? { internal fun getBrowserAutofillSupportInfoIfTrusted(
context: Context,
appPackage: String
): BrowserAutofillSupportInfo? {
if (!isTrustedBrowser(context, appPackage)) return null if (!isTrustedBrowser(context, appPackage)) return null
return BrowserAutofillSupportInfo( return BrowserAutofillSupportInfo(
multiOriginMethod = getBrowserMultiOriginMethod(appPackage), multiOriginMethod = getBrowserMultiOriginMethod(appPackage),
@ -197,14 +212,18 @@ public enum class BrowserAutofillSupportLevel {
} }
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun getBrowserAutofillSupportLevel(context: Context, appPackage: String): BrowserAutofillSupportLevel { private fun getBrowserAutofillSupportLevel(
context: Context,
appPackage: String
): BrowserAutofillSupportLevel {
val browserInfo = getBrowserAutofillSupportInfoIfTrusted(context, appPackage) val browserInfo = getBrowserAutofillSupportInfoIfTrusted(context, appPackage)
return when { return when {
browserInfo == null -> BrowserAutofillSupportLevel.None browserInfo == null -> BrowserAutofillSupportLevel.None
appPackage in FLAKY_BROWSERS -> BrowserAutofillSupportLevel.FlakyFill appPackage in FLAKY_BROWSERS -> BrowserAutofillSupportLevel.FlakyFill
appPackage in BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY -> appPackage in BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY ->
BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility
browserInfo.multiOriginMethod == BrowserMultiOriginMethod.None -> BrowserAutofillSupportLevel.PasswordFill browserInfo.multiOriginMethod == BrowserMultiOriginMethod.None ->
BrowserAutofillSupportLevel.PasswordFill
browserInfo.saveFlags == null -> BrowserAutofillSupportLevel.GeneralFill browserInfo.saveFlags == null -> BrowserAutofillSupportLevel.GeneralFill
else -> BrowserAutofillSupportLevel.GeneralFillAndSave else -> BrowserAutofillSupportLevel.GeneralFillAndSave
}.takeUnless { supportLevel -> }.takeUnless { supportLevel ->
@ -212,7 +231,8 @@ private fun getBrowserAutofillSupportLevel(context: Context, appPackage: String)
// (compatibility mode is only available on Android Pie and higher). Since all known browsers // (compatibility mode is only available on Android Pie and higher). Since all known browsers
// with native Autofill support offer full save support as well, we reuse the list of those // with native Autofill support offer full save support as well, we reuse the list of those
// browsers here. // browsers here.
supportLevel != BrowserAutofillSupportLevel.GeneralFillAndSave && Build.VERSION.SDK_INT < Build.VERSION_CODES.P supportLevel != BrowserAutofillSupportLevel.GeneralFillAndSave &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.P
} }
?: BrowserAutofillSupportLevel.None ?: BrowserAutofillSupportLevel.None
} }
@ -222,9 +242,15 @@ public fun getInstalledBrowsersWithAutofillSupportLevel(
context: Context context: Context
): List<Pair<String, BrowserAutofillSupportLevel>> { ): List<Pair<String, BrowserAutofillSupportLevel>> {
val testWebIntent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("http://example.org") } val testWebIntent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("http://example.org") }
val installedBrowsers = context.packageManager.queryIntentActivities(testWebIntent, PackageManager.MATCH_ALL) val installedBrowsers =
context.packageManager.queryIntentActivities(testWebIntent, PackageManager.MATCH_ALL)
return installedBrowsers return installedBrowsers
.map { it to getBrowserAutofillSupportLevel(context, it.activityInfo.packageName) } .map { it to getBrowserAutofillSupportLevel(context, it.activityInfo.packageName) }
.filter { it.first.isDefault || it.second != BrowserAutofillSupportLevel.None } .filter { it.first.isDefault || it.second != BrowserAutofillSupportLevel.None }
.map { context.packageManager.getApplicationLabel(it.first.activityInfo.applicationInfo).toString() to it.second } .map {
context
.packageManager
.getApplicationLabel(it.first.activityInfo.applicationInfo)
.toString() to it.second
}
} }

View file

@ -108,9 +108,14 @@ internal class FormField(
"text", "text",
) )
private val HTML_INPUT_FIELD_TYPES_FILLABLE = private val HTML_INPUT_FIELD_TYPES_FILLABLE =
(HTML_INPUT_FIELD_TYPES_USERNAME + HTML_INPUT_FIELD_TYPES_PASSWORD + HTML_INPUT_FIELD_TYPES_OTP).toSet().toList() (HTML_INPUT_FIELD_TYPES_USERNAME +
HTML_INPUT_FIELD_TYPES_PASSWORD +
HTML_INPUT_FIELD_TYPES_OTP)
.toSet()
.toList()
@RequiresApi(Build.VERSION_CODES.O) private fun isSupportedHint(hint: String) = hint in HINTS_FILLABLE @RequiresApi(Build.VERSION_CODES.O)
private fun isSupportedHint(hint: String) = hint in HINTS_FILLABLE
private val EXCLUDED_TERMS = private val EXCLUDED_TERMS =
listOf( listOf(
"url_bar", // Chrome/Edge/Firefox address bar "url_bar", // Chrome/Edge/Firefox address bar
@ -214,7 +219,8 @@ internal class FormField(
private val hasAutocompleteHintUsername = htmlAutocomplete == "username" private val hasAutocompleteHintUsername = htmlAutocomplete == "username"
val hasAutocompleteHintCurrentPassword = htmlAutocomplete == "current-password" val hasAutocompleteHintCurrentPassword = htmlAutocomplete == "current-password"
private val hasAutocompleteHintNewPassword = htmlAutocomplete == "new-password" private val hasAutocompleteHintNewPassword = htmlAutocomplete == "new-password"
private val hasAutocompleteHintPassword = hasAutocompleteHintCurrentPassword || hasAutocompleteHintNewPassword private val hasAutocompleteHintPassword =
hasAutocompleteHintCurrentPassword || hasAutocompleteHintNewPassword
private val hasAutocompleteHintOtp = htmlAutocomplete == "one-time-code" private val hasAutocompleteHintOtp = htmlAutocomplete == "one-time-code"
// Results of hint-based field type detection // Results of hint-based field type detection
@ -238,7 +244,9 @@ internal class FormField(
// fields to the fill rules and only exclude those fields that have incompatible autocomplete // fields to the fill rules and only exclude those fields that have incompatible autocomplete
// hint. // hint.
val couldBeTwoStepHiddenPassword = val couldBeTwoStepHiddenPassword =
!isVisible && isHtmlPasswordField && (hasAutocompleteHintCurrentPassword || htmlAutocomplete == null) !isVisible &&
isHtmlPasswordField &&
(hasAutocompleteHintCurrentPassword || htmlAutocomplete == null)
// Since many site put autocomplete=off on login forms for compliance reasons or since they are // Since many site put autocomplete=off on login forms for compliance reasons or since they are
// worried of the user's browser automatically (i.e., without any user interaction) filling // worried of the user's browser automatically (i.e., without any user interaction) filling
@ -247,7 +255,8 @@ internal class FormField(
private val excludedByHints = excludedByAutofillHints private val excludedByHints = excludedByAutofillHints
// Only offer to fill into custom views if they explicitly opted into Autofill. // Only offer to fill into custom views if they explicitly opted into Autofill.
val relevantField = hasAutofillTypeText && (isTextField || autofillHints.isNotEmpty()) && !excludedByHints val relevantField =
hasAutofillTypeText && (isTextField || autofillHints.isNotEmpty()) && !excludedByHints
// Exclude fields based on hint, resource ID or HTML name. // Exclude fields based on hint, resource ID or HTML name.
// Note: We still report excluded fields as relevant since they count for adjacency heuristics, // Note: We still report excluded fields as relevant since they count for adjacency heuristics,
@ -260,7 +269,8 @@ internal class FormField(
notExcluded && (isAndroidPasswordField || isHtmlPasswordField || hasHintPassword) notExcluded && (isAndroidPasswordField || isHtmlPasswordField || hasHintPassword)
private val isCertainPasswordField = isPossiblePasswordField && hasHintPassword private val isCertainPasswordField = isPossiblePasswordField && hasHintPassword
private val isLikelyPasswordField = private val isLikelyPasswordField =
isPossiblePasswordField && (isCertainPasswordField || PASSWORD_HEURISTIC_TERMS.anyMatchesFieldInfo) isPossiblePasswordField &&
(isCertainPasswordField || PASSWORD_HEURISTIC_TERMS.anyMatchesFieldInfo)
val passwordCertainty = val passwordCertainty =
if (isCertainPasswordField) CertaintyLevel.Certain if (isCertainPasswordField) CertaintyLevel.Certain
else if (isLikelyPasswordField) CertaintyLevel.Likely else if (isLikelyPasswordField) CertaintyLevel.Likely
@ -273,17 +283,20 @@ internal class FormField(
isPossibleOtpField && isPossibleOtpField &&
(isCertainOtpField || (isCertainOtpField ||
OTP_HEURISTIC_TERMS.anyMatchesFieldInfo || OTP_HEURISTIC_TERMS.anyMatchesFieldInfo ||
((htmlMaxLength == null || htmlMaxLength in 6..8) && OTP_WEAK_HEURISTIC_TERMS.anyMatchesFieldInfo)) ((htmlMaxLength == null || htmlMaxLength in 6..8) &&
OTP_WEAK_HEURISTIC_TERMS.anyMatchesFieldInfo))
val otpCertainty = val otpCertainty =
if (isCertainOtpField) CertaintyLevel.Certain if (isCertainOtpField) CertaintyLevel.Certain
else if (isLikelyOtpField) CertaintyLevel.Likely else if (isLikelyOtpField) CertaintyLevel.Likely
else if (isPossibleOtpField) CertaintyLevel.Possible else CertaintyLevel.Impossible else if (isPossibleOtpField) CertaintyLevel.Possible else CertaintyLevel.Impossible
// Username field heuristics (based only on the current field) // Username field heuristics (based only on the current field)
private val isPossibleUsernameField = notExcluded && !isPossiblePasswordField && !isCertainOtpField private val isPossibleUsernameField =
notExcluded && !isPossiblePasswordField && !isCertainOtpField
private val isCertainUsernameField = isPossibleUsernameField && hasHintUsername private val isCertainUsernameField = isPossibleUsernameField && hasHintUsername
private val isLikelyUsernameField = private val isLikelyUsernameField =
isPossibleUsernameField && (isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.anyMatchesFieldInfo)) isPossibleUsernameField &&
(isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.anyMatchesFieldInfo))
val usernameCertainty = val usernameCertainty =
if (isCertainUsernameField) CertaintyLevel.Certain if (isCertainUsernameField) CertaintyLevel.Certain
else if (isLikelyUsernameField) CertaintyLevel.Likely else if (isLikelyUsernameField) CertaintyLevel.Likely

View file

@ -34,12 +34,18 @@ public fun cachePublicSuffixList(context: Context) {
* Note: Invalid domains, such as IP addresses, are returned unchanged and thus never collide with * Note: Invalid domains, such as IP addresses, are returned unchanged and thus never collide with
* the return value for valid domains. * the return value for valid domains.
*/ */
internal fun getPublicSuffixPlusOne(context: Context, domain: String, customSuffixes: Sequence<String>) = runBlocking { internal fun getPublicSuffixPlusOne(
context: Context,
domain: String,
customSuffixes: Sequence<String>
) = runBlocking {
// We only feed valid domain names which are not IP addresses into getPublicSuffixPlusOne. // We only feed valid domain names which are not IP addresses into getPublicSuffixPlusOne.
// We do not check whether the domain actually exists (actually, not even whether its TLD // We do not check whether the domain actually exists (actually, not even whether its TLD
// exists). As long as we restrict ourselves to syntactically valid domain names, // exists). As long as we restrict ourselves to syntactically valid domain names,
// getPublicSuffixPlusOne will return non-colliding results. // getPublicSuffixPlusOne will return non-colliding results.
if (!Patterns.DOMAIN_NAME.matcher(domain).matches() || Patterns.IP_ADDRESS.matcher(domain).matches()) { if (!Patterns.DOMAIN_NAME.matcher(domain).matches() ||
Patterns.IP_ADDRESS.matcher(domain).matches()
) {
domain domain
} else { } else {
getCanonicalSuffix(context, domain, customSuffixes) getCanonicalSuffix(context, domain, customSuffixes)
@ -60,7 +66,11 @@ private fun getSuffixPlusUpToOne(domain: String, suffix: String): String? {
return "$lastPrefixPart.$suffix" return "$lastPrefixPart.$suffix"
} }
private suspend fun getCanonicalSuffix(context: Context, domain: String, customSuffixes: Sequence<String>): String { private suspend fun getCanonicalSuffix(
context: Context,
domain: String,
customSuffixes: Sequence<String>
): String {
val publicSuffixList = PublicSuffixListCache.getOrCachePublicSuffixList(context) val publicSuffixList = PublicSuffixListCache.getOrCachePublicSuffixList(context)
val publicSuffixPlusOne = publicSuffixList.getPublicSuffixPlusOne(domain).await() ?: return domain val publicSuffixPlusOne = publicSuffixList.getPublicSuffixPlusOne(domain).await() ?: return domain
var longestSuffix = publicSuffixPlusOne var longestSuffix = publicSuffixPlusOne

View file

@ -58,7 +58,8 @@ internal class PublicSuffixList(
fun getPublicSuffixPlusOne(domain: String): Deferred<String?> = fun getPublicSuffixPlusOne(domain: String): Deferred<String?> =
scope.async { scope.async {
when (val offset = data.getPublicSuffixOffset(domain)) { when (val offset = data.getPublicSuffixOffset(domain)) {
is PublicSuffixOffset.Offset -> domain.split('.').drop(offset.value).joinToString(separator = ".") is PublicSuffixOffset.Offset ->
domain.split('.').drop(offset.value).joinToString(separator = ".")
else -> null else -> null
} }
} }

View file

@ -12,7 +12,10 @@ import java.net.IDN
import mozilla.components.lib.publicsuffixlist.ext.binarySearch import mozilla.components.lib.publicsuffixlist.ext.binarySearch
/** Class wrapping the public suffix list data and offering methods for accessing rules in it. */ /** Class wrapping the public suffix list data and offering methods for accessing rules in it. */
internal class PublicSuffixListData(private val rules: ByteArray, private val exceptions: ByteArray) { internal class PublicSuffixListData(
private val rules: ByteArray,
private val exceptions: ByteArray
) {
private fun binarySearchRules(labels: List<ByteArray>, labelIndex: Int): String? { private fun binarySearchRules(labels: List<ByteArray>, labelIndex: Int): String? {
return rules.binarySearch(labels, labelIndex) return rules.binarySearch(labels, labelIndex)

View file

@ -30,7 +30,12 @@ internal object PublicSuffixListLoader {
@Suppress("MagicNumber") @Suppress("MagicNumber")
private fun BufferedInputStream.readInt(): Int { private fun BufferedInputStream.readInt(): Int {
return (read() and 0xff shl 24 or (read() and 0xff shl 16) or (read() and 0xff shl 8) or (read() and 0xff)) return (read() and
0xff shl
24 or
(read() and 0xff shl 16) or
(read() and 0xff shl 8) or
(read() and 0xff))
} }
private fun BufferedInputStream.readFully(size: Int): ByteArray { private fun BufferedInputStream.readFully(size: Int): ByteArray {

View file

@ -1,11 +0,0 @@
/**
* IDEs don't support this very well for buildSrc, so we use the regular dependency format
* until that changes.
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
*/

View file

@ -115,11 +115,13 @@ constructor(
.lineSequence() .lineSequence()
.filter { line -> .filter { line ->
return@filter when { return@filter when {
USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } && !foundUsername -> { USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } &&
!foundUsername -> {
foundUsername = true foundUsername = true
false false
} }
line.startsWith("otpauth://", ignoreCase = true) || line.startsWith("totp:", ignoreCase = true) -> { line.startsWith("otpauth://", ignoreCase = true) ||
line.startsWith("totp:", ignoreCase = true) -> {
false false
} }
else -> { else -> {
@ -174,7 +176,8 @@ constructor(
private fun findUsername(): String? { private fun findUsername(): String? {
extraContentString.splitToSequence("\n").forEach { line -> extraContentString.splitToSequence("\n").forEach { line ->
for (prefix in USERNAME_FIELDS) { for (prefix in USERNAME_FIELDS) {
if (line.startsWith(prefix, ignoreCase = true)) return line.substring(prefix.length).trimStart() if (line.startsWith(prefix, ignoreCase = true))
return line.substring(prefix.length).trimStart()
} }
} }
return null return null

View file

@ -23,7 +23,8 @@ internal object Otp {
check(STEAM_ALPHABET.size == 26) check(STEAM_ALPHABET.size == 26)
} }
fun calculateCode(secret: String, counter: Long, algorithm: String, digits: String) = runCatching { fun calculateCode(secret: String, counter: Long, algorithm: String, digits: String) =
runCatching {
val algo = "Hmac${algorithm.uppercase(Locale.ROOT)}" val algo = "Hmac${algorithm.uppercase(Locale.ROOT)}"
val decodedSecret = BASE_32.decode(secret) val decodedSecret = BASE_32.decode(secret)
val secretKey = SecretKeySpec(decodedSecret, algo) val secretKey = SecretKeySpec(decodedSecret, algo)

View file

@ -19,7 +19,8 @@ import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class) @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
internal class PasswordEntryTest { internal class PasswordEntryTest {
private fun makeEntry(content: String) = PasswordEntry(fakeClock, testFinder, testScope, content.encodeToByteArray()) private fun makeEntry(content: String) =
PasswordEntry(fakeClock, testFinder, testScope, content.encodeToByteArray())
@Test @Test
fun testGetPassword() { fun testGetPassword() {
@ -49,7 +50,10 @@ internal class PasswordEntryTest {
assertEquals("blubb", makeEntry("\nblubb").extraContentString) assertEquals("blubb", makeEntry("\nblubb").extraContentString)
assertEquals("blubb", makeEntry("blubb\npassword: foo").extraContentString) assertEquals("blubb", makeEntry("blubb\npassword: foo").extraContentString)
assertEquals("blubb", makeEntry("password: foo\nblubb").extraContentString) assertEquals("blubb", makeEntry("password: foo\nblubb").extraContentString)
assertEquals("blubb\nusername: bar", makeEntry("blubb\npassword: foo\nusername: bar").extraContentString) assertEquals(
"blubb\nusername: bar",
makeEntry("blubb\npassword: foo\nusername: bar").extraContentString
)
assertEquals("", makeEntry("\n").extraContentString) assertEquals("", makeEntry("\n").extraContentString)
assertEquals("", makeEntry("").extraContentString) assertEquals("", makeEntry("").extraContentString)
} }

View file

@ -15,16 +15,34 @@ internal class OtpTest {
@Test @Test
fun testOtpGeneration6Digits() { fun testOtpGeneration6Digits() {
assertEquals("953550", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333298159 / (1000 * 30), "SHA1", "6").get()) assertEquals(
assertEquals("275379", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333571918 / (1000 * 30), "SHA1", "6").get()) "953550",
assertEquals("867507", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333600517 / (1000 * 57), "SHA1", "6").get()) Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333298159 / (1000 * 30), "SHA1", "6").get()
)
assertEquals(
"275379",
Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333571918 / (1000 * 30), "SHA1", "6").get()
)
assertEquals(
"867507",
Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333600517 / (1000 * 57), "SHA1", "6").get()
)
} }
@Test @Test
fun testOtpGeneration10Digits() { fun testOtpGeneration10Digits() {
assertEquals("0740900914", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333655044 / (1000 * 30), "SHA1", "10").get()) assertEquals(
assertEquals("0070632029", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333691405 / (1000 * 30), "SHA1", "10").get()) "0740900914",
assertEquals("1017265882", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333728893 / (1000 * 83), "SHA1", "10").get()) Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333655044 / (1000 * 30), "SHA1", "10").get()
)
assertEquals(
"0070632029",
Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333691405 / (1000 * 30), "SHA1", "10").get()
)
assertEquals(
"1017265882",
Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333728893 / (1000 * 83), "SHA1", "10").get()
)
} }
@Test @Test
@ -42,7 +60,10 @@ internal class OtpTest {
"127764", "127764",
Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAAAAA", 1593367111963 / (1000 * 30), "SHA1", "6").get() Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAAAAA", 1593367111963 / (1000 * 30), "SHA1", "6").get()
) )
assertEquals("047515", Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAA", 1593367171420 / (1000 * 30), "SHA1", "6").get()) assertEquals(
"047515",
Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAA", 1593367171420 / (1000 * 30), "SHA1", "6").get()
)
} }
@Test @Test

View file

@ -15,7 +15,11 @@ public class AutocryptPeerUpdate() : Parcelable {
private var effectiveDate: Date? = null private var effectiveDate: Date? = null
private lateinit var preferEncrypt: PreferEncrypt private lateinit var preferEncrypt: PreferEncrypt
internal constructor(keyData: ByteArray?, effectiveDate: Date?, preferEncrypt: PreferEncrypt) : this() { internal constructor(
keyData: ByteArray?,
effectiveDate: Date?,
preferEncrypt: PreferEncrypt
) : this() {
this.keyData = keyData this.keyData = keyData
this.effectiveDate = effectiveDate this.effectiveDate = effectiveDate
this.preferEncrypt = preferEncrypt this.preferEncrypt = preferEncrypt

View file

@ -21,7 +21,11 @@ public class OpenPgpApi(private val context: Context, private val service: IOpen
private val pipeIdGen: AtomicInteger = AtomicInteger() private val pipeIdGen: AtomicInteger = AtomicInteger()
public suspend fun executeApi(data: Intent, inputStream: InputStream?, outputStream: OutputStream?): Intent { public suspend fun executeApi(
data: Intent,
inputStream: InputStream?,
outputStream: OutputStream?
): Intent {
var input: ParcelFileDescriptor? = null var input: ParcelFileDescriptor? = null
return try { return try {
if (inputStream != null) { if (inputStream != null) {
@ -124,7 +128,8 @@ public class OpenPgpApi(private val context: Context, private val service: IOpen
* *
* This action uses no extras. * This action uses no extras.
*/ */
public const val ACTION_CHECK_PERMISSION: String = "org.openintents.openpgp.action.CHECK_PERMISSION" public const val ACTION_CHECK_PERMISSION: String =
"org.openintents.openpgp.action.CHECK_PERMISSION"
/** /**
* Sign text resulting in a cleartext signature Some magic pre-processing of the text is done to * Sign text resulting in a cleartext signature Some magic pre-processing of the text is done to
@ -178,9 +183,11 @@ public class OpenPgpApi(private val context: Context, private val service: IOpen
* passphrase) String EXTRA_ORIGINAL_FILENAME (original filename to be encrypted as metadata) * passphrase) String EXTRA_ORIGINAL_FILENAME (original filename to be encrypted as metadata)
* boolean EXTRA_ENABLE_COMPRESSION (enable ZLIB compression, default ist true) * boolean EXTRA_ENABLE_COMPRESSION (enable ZLIB compression, default ist true)
*/ */
public const val ACTION_SIGN_AND_ENCRYPT: String = "org.openintents.openpgp.action.SIGN_AND_ENCRYPT" public const val ACTION_SIGN_AND_ENCRYPT: String =
"org.openintents.openpgp.action.SIGN_AND_ENCRYPT"
public const val ACTION_QUERY_AUTOCRYPT_STATUS: String = "org.openintents.openpgp.action.QUERY_AUTOCRYPT_STATUS" public const val ACTION_QUERY_AUTOCRYPT_STATUS: String =
"org.openintents.openpgp.action.QUERY_AUTOCRYPT_STATUS"
/** /**
* Decrypts and verifies given input stream. This methods handles encrypted-only, * Decrypts and verifies given input stream. This methods handles encrypted-only,
@ -208,7 +215,8 @@ public class OpenPgpApi(private val context: Context, private val service: IOpen
* returned extras: OpenPgpDecryptMetadata RESULT_METADATA String RESULT_CHARSET (charset which * returned extras: OpenPgpDecryptMetadata RESULT_METADATA String RESULT_CHARSET (charset which
* was specified in the headers of ascii armored input, if any) * was specified in the headers of ascii armored input, if any)
*/ */
public const val ACTION_DECRYPT_METADATA: String = "org.openintents.openpgp.action.DECRYPT_METADATA" public const val ACTION_DECRYPT_METADATA: String =
"org.openintents.openpgp.action.DECRYPT_METADATA"
/** /**
* Select key id for signing * Select key id for signing
@ -217,7 +225,8 @@ public class OpenPgpApi(private val context: Context, private val service: IOpen
* *
* returned extras: long EXTRA_SIGN_KEY_ID * returned extras: long EXTRA_SIGN_KEY_ID
*/ */
public const val ACTION_GET_SIGN_KEY_ID: String = "org.openintents.openpgp.action.GET_SIGN_KEY_ID" public const val ACTION_GET_SIGN_KEY_ID: String =
"org.openintents.openpgp.action.GET_SIGN_KEY_ID"
/** /**
* Get key ids based on given user ids (=emails) * Get key ids based on given user ids (=emails)
@ -254,7 +263,8 @@ public class OpenPgpApi(private val context: Context, private val service: IOpen
*/ */
public const val ACTION_BACKUP: String = "org.openintents.openpgp.action.BACKUP" public const val ACTION_BACKUP: String = "org.openintents.openpgp.action.BACKUP"
public const val ACTION_UPDATE_AUTOCRYPT_PEER: String = "org.openintents.openpgp.action.UPDATE_AUTOCRYPT_PEER" public const val ACTION_UPDATE_AUTOCRYPT_PEER: String =
"org.openintents.openpgp.action.UPDATE_AUTOCRYPT_PEER"
/* Intent extras */ /* Intent extras */
public const val EXTRA_API_VERSION: String = "api_version" public const val EXTRA_API_VERSION: String = "api_version"
@ -323,7 +333,8 @@ public class OpenPgpApi(private val context: Context, private val service: IOpen
public const val EXTRA_DATA_LENGTH: String = "data_length" public const val EXTRA_DATA_LENGTH: String = "data_length"
public const val EXTRA_DECRYPTION_RESULT: String = "decryption_result" public const val EXTRA_DECRYPTION_RESULT: String = "decryption_result"
public const val EXTRA_SENDER_ADDRESS: String = "sender_address" public const val EXTRA_SENDER_ADDRESS: String = "sender_address"
public const val EXTRA_SUPPORT_OVERRIDE_CRYPTO_WARNING: String = "support_override_crpto_warning" public const val EXTRA_SUPPORT_OVERRIDE_CRYPTO_WARNING: String =
"support_override_crpto_warning"
public const val EXTRA_AUTOCRYPT_PEER_ID: String = "autocrypt_peer_id" public const val EXTRA_AUTOCRYPT_PEER_ID: String = "autocrypt_peer_id"
public const val EXTRA_AUTOCRYPT_PEER_UPDATE: String = "autocrypt_peer_update" public const val EXTRA_AUTOCRYPT_PEER_UPDATE: String = "autocrypt_peer_update"
public const val EXTRA_AUTOCRYPT_PEER_GOSSIP_UPDATES: String = "autocrypt_peer_gossip_updates" public const val EXTRA_AUTOCRYPT_PEER_GOSSIP_UPDATES: String = "autocrypt_peer_gossip_updates"

View file

@ -65,7 +65,12 @@ public class OpenPgpServiceConnection(context: Context, providerPackageName: Str
val serviceIntent = Intent(OpenPgpApi.SERVICE_INTENT_2) val serviceIntent = Intent(OpenPgpApi.SERVICE_INTENT_2)
// NOTE: setPackage is very important to restrict the intent to this provider only! // NOTE: setPackage is very important to restrict the intent to this provider only!
serviceIntent.setPackage(mProviderPackageName) serviceIntent.setPackage(mProviderPackageName)
val connect = mApplicationContext.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE) val connect =
mApplicationContext.bindService(
serviceIntent,
mServiceConnection,
Context.BIND_AUTO_CREATE
)
if (!connect) { if (!connect) {
throw Exception("bindService() returned false!") throw Exception("bindService() returned false!")
} }

View file

@ -13,7 +13,10 @@ import java.util.regex.Pattern
public object OpenPgpUtils { public object OpenPgpUtils {
private val PGP_MESSAGE: Pattern = private val PGP_MESSAGE: Pattern =
Pattern.compile(".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL) Pattern.compile(
".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*",
Pattern.DOTALL
)
private val PGP_SIGNED_MESSAGE: Pattern = private val PGP_SIGNED_MESSAGE: Pattern =
Pattern.compile( Pattern.compile(
".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*", ".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
@ -103,5 +106,9 @@ public object OpenPgpUtils {
return if (userIdBuilder.isEmpty()) null else userIdBuilder.toString() return if (userIdBuilder.isEmpty()) null else userIdBuilder.toString()
} }
public class UserId(public val name: String?, public val email: String?, public val comment: String?) : Serializable public class UserId(
public val name: String?,
public val email: String?,
public val comment: String?
) : Serializable
} }

View file

@ -22,7 +22,11 @@ public class OpenPgpDecryptionResult() : Parcelable {
decryptedSessionKey = null decryptedSessionKey = null
} }
private constructor(result: Int, sessionKey: ByteArray?, decryptedSessionKey: ByteArray?) : this() { private constructor(
result: Int,
sessionKey: ByteArray?,
decryptedSessionKey: ByteArray?
) : this() {
this.result = result this.result = result
if (sessionKey == null != (decryptedSessionKey == null)) { if (sessionKey == null != (decryptedSessionKey == null)) {
throw AssertionError("sessionkey must be null iff decryptedSessionKey is null") throw AssertionError("sessionkey must be null iff decryptedSessionKey is null")

View file

@ -32,7 +32,12 @@ public class OpenPgpMetadata() : Parcelable {
this.charset = charset this.charset = charset
} }
private constructor(filename: String?, mimeType: String?, modificationTime: Long, originalSize: Long) : this() { private constructor(
filename: String?,
mimeType: String?,
modificationTime: Long,
originalSize: Long
) : this() {
this.filename = filename this.filename = filename
this.mimeType = mimeType this.mimeType = mimeType
this.modificationTime = modificationTime this.modificationTime = modificationTime

View file

@ -58,7 +58,8 @@ public class OpenPgpSignatureResult : Parcelable {
} }
// backward compatibility for this exact version // backward compatibility for this exact version
if (version > 2) { if (version > 2) {
senderStatusResult = readEnumWithNullAndFallback(source, SenderStatusResult.values(), SenderStatusResult.UNKNOWN) senderStatusResult =
readEnumWithNullAndFallback(source, SenderStatusResult.values(), SenderStatusResult.UNKNOWN)
confirmedUserIds = source.createStringArrayList() confirmedUserIds = source.createStringArrayList()
} else { } else {
senderStatusResult = SenderStatusResult.UNKNOWN senderStatusResult = SenderStatusResult.UNKNOWN
@ -151,7 +152,9 @@ public class OpenPgpSignatureResult : Parcelable {
) )
} }
public fun withAutocryptPeerResult(autocryptPeerentityResult: AutocryptPeerResult?): OpenPgpSignatureResult { public fun withAutocryptPeerResult(
autocryptPeerentityResult: AutocryptPeerResult?
): OpenPgpSignatureResult {
return OpenPgpSignatureResult( return OpenPgpSignatureResult(
result, result,
primaryUserId, primaryUserId,
@ -253,18 +256,55 @@ public class OpenPgpSignatureResult : Parcelable {
} }
public fun createWithNoSignature(): OpenPgpSignatureResult { public fun createWithNoSignature(): OpenPgpSignatureResult {
return OpenPgpSignatureResult(RESULT_NO_SIGNATURE, null, 0L, null, null, null, null, null, null) return OpenPgpSignatureResult(
RESULT_NO_SIGNATURE,
null,
0L,
null,
null,
null,
null,
null,
null
)
} }
public fun createWithKeyMissing(keyId: Long, signatureTimestamp: Date?): OpenPgpSignatureResult { public fun createWithKeyMissing(
return OpenPgpSignatureResult(RESULT_KEY_MISSING, null, keyId, null, null, null, null, signatureTimestamp, null) keyId: Long,
signatureTimestamp: Date?
): OpenPgpSignatureResult {
return OpenPgpSignatureResult(
RESULT_KEY_MISSING,
null,
keyId,
null,
null,
null,
null,
signatureTimestamp,
null
)
} }
public fun createWithInvalidSignature(): OpenPgpSignatureResult { public fun createWithInvalidSignature(): OpenPgpSignatureResult {
return OpenPgpSignatureResult(RESULT_INVALID_SIGNATURE, null, 0L, null, null, null, null, null, null) return OpenPgpSignatureResult(
RESULT_INVALID_SIGNATURE,
null,
0L,
null,
null,
null,
null,
null,
null
)
} }
private fun <T : Enum<T>?> readEnumWithNullAndFallback(source: Parcel, enumValues: Array<T>, fallback: T?): T? { private fun <T : Enum<T>?> readEnumWithNullAndFallback(
source: Parcel,
enumValues: Array<T>,
fallback: T?
): T? {
val valueOrdinal = source.readInt() val valueOrdinal = source.readInt()
if (valueOrdinal == -1) { if (valueOrdinal == -1) {
return null return null

Some files were not shown because too many files have changed in this diff Show more