all: reformat with Spotless again
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
parent
2fcb285e27
commit
7e2eb2425e
108 changed files with 1385 additions and 473 deletions
|
@ -115,7 +115,10 @@ class MigrationsTest {
|
|||
putBoolean(PreferenceKeys.CLEAR_CLIPBOARD_20X, true)
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,9 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
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())
|
||||
StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
|
||||
StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())
|
||||
|
|
|
@ -199,8 +199,11 @@ object PasswordRepository {
|
|||
fun getFilesList(path: File?): ArrayList<File> {
|
||||
if (path == null || !path.exists()) return ArrayList()
|
||||
|
||||
val directories = (path.listFiles(FileFilter { pathname -> pathname.isDirectory }) ?: emptyArray()).toList()
|
||||
val files = (path.listFiles(FileFilter { pathname -> pathname.extension == "gpg" }) ?: emptyArray()).toList()
|
||||
val directories =
|
||||
(path.listFiles(FileFilter { pathname -> pathname.isDirectory }) ?: emptyArray()).toList()
|
||||
val files =
|
||||
(path.listFiles(FileFilter { pathname -> pathname.extension == "gpg" }) ?: emptyArray())
|
||||
.toList()
|
||||
|
||||
val items = ArrayList<File>()
|
||||
items.addAll(directories)
|
||||
|
@ -216,7 +219,11 @@ object PasswordRepository {
|
|||
* @return a list of password items
|
||||
*/
|
||||
@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
|
||||
val passList = getFilesList(path).also { it.sortBy { f -> f.name } }
|
||||
val passwordList = ArrayList<PasswordItem>()
|
||||
|
|
|
@ -55,7 +55,8 @@ class FieldItemAdapter(
|
|||
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) {
|
||||
with(binding) {
|
||||
|
@ -66,7 +67,8 @@ class FieldItemAdapter(
|
|||
when (fieldItem.action) {
|
||||
FieldItem.ActionType.COPY -> {
|
||||
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
|
||||
setEndIconOnClickListener { copyTextToClipBoard(itemText.text.toString()) }
|
||||
}
|
||||
|
|
|
@ -35,7 +35,9 @@ open class 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
|
||||
}
|
||||
|
||||
|
@ -59,7 +61,8 @@ open class PasswordItemRecyclerAdapter :
|
|||
name.text = spannable
|
||||
if (item.type == PasswordItem.TYPE_CATEGORY) {
|
||||
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.text = "$count"
|
||||
} 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>? {
|
||||
val view = recyclerView.findChildViewUnder(event.x, event.y) ?: return null
|
||||
|
|
|
@ -73,7 +73,12 @@ class AutofillDecryptActivity : AppCompatActivity() {
|
|||
putExtra(EXTRA_SEARCH_ACTION, false)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -124,9 +129,17 @@ class AutofillDecryptActivity : AppCompatActivity() {
|
|||
setResult(RESULT_CANCELED)
|
||||
} else {
|
||||
val fillInDataset =
|
||||
AutofillResponseBuilder.makeFillInDataset(this@AutofillDecryptActivity, credentials, clientState, action)
|
||||
AutofillResponseBuilder.makeFillInDataset(
|
||||
this@AutofillDecryptActivity,
|
||||
credentials,
|
||||
clientState,
|
||||
action
|
||||
)
|
||||
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() }
|
||||
|
@ -137,7 +150,11 @@ class AutofillDecryptActivity : AppCompatActivity() {
|
|||
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
|
||||
val openPgpService =
|
||||
suspendCoroutine<IOpenPgpService2> { cont ->
|
||||
|
@ -177,7 +194,9 @@ class AutofillDecryptActivity : AppCompatActivity() {
|
|||
return null
|
||||
}
|
||||
.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 -> {
|
||||
runCatching {
|
||||
val entry =
|
||||
|
@ -185,7 +204,12 @@ class AutofillDecryptActivity : AppCompatActivity() {
|
|||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
passwordEntryFactory.create(lifecycleScope, decryptedOutput.toByteArray())
|
||||
}
|
||||
AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure)
|
||||
AutofillPreferences.credentialsFromStoreEntry(
|
||||
this,
|
||||
file,
|
||||
entry,
|
||||
directoryStructure
|
||||
)
|
||||
}
|
||||
.getOrElse { e ->
|
||||
e(e) { "Failed to parse password entry" }
|
||||
|
@ -193,7 +217,8 @@ class AutofillDecryptActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
|
||||
val pendingIntent: PendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!!
|
||||
val pendingIntent: PendingIntent =
|
||||
result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!!
|
||||
runCatching {
|
||||
val intentToResume =
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -215,10 +240,16 @@ class AutofillDecryptActivity : AppCompatActivity() {
|
|||
val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR)
|
||||
if (error != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(applicationContext, "Error from OpenKeyChain: ${error.message}", Toast.LENGTH_LONG)
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
"Error from OpenKeyChain: ${error.message}",
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
e { "OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}" }
|
||||
e {
|
||||
"OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}"
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
|
|
|
@ -46,11 +46,16 @@ class AutofillFilterView : AppCompatActivity() {
|
|||
private const val HEIGHT_PERCENTAGE = 0.9
|
||||
private const val WIDTH_PERCENTAGE = 0.75
|
||||
|
||||
private const val EXTRA_FORM_ORIGIN_WEB = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_WEB"
|
||||
private const val EXTRA_FORM_ORIGIN_APP = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_APP"
|
||||
private const val EXTRA_FORM_ORIGIN_WEB =
|
||||
"dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_WEB"
|
||||
private const val EXTRA_FORM_ORIGIN_APP =
|
||||
"dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_APP"
|
||||
private var matchAndDecryptFileRequestCode = 1
|
||||
|
||||
fun makeMatchAndDecryptFileIntentSender(context: Context, formOrigin: FormOrigin): IntentSender {
|
||||
fun makeMatchAndDecryptFileIntentSender(
|
||||
context: Context,
|
||||
formOrigin: FormOrigin
|
||||
): IntentSender {
|
||||
val intent =
|
||||
Intent(context, AutofillFilterView::class.java).apply {
|
||||
when (formOrigin) {
|
||||
|
@ -108,7 +113,9 @@ class AutofillFilterView : AppCompatActivity() {
|
|||
FormOrigin.App(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_APP)!!)
|
||||
}
|
||||
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()
|
||||
return
|
||||
}
|
||||
|
@ -125,7 +132,8 @@ class AutofillFilterView : AppCompatActivity() {
|
|||
with(binding) {
|
||||
rvPassword.apply {
|
||||
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 pathToIdentifier = directoryStructure.getPathToIdentifierFor(file)
|
||||
val identifier = directoryStructure.getIdentifierFor(file)
|
||||
|
@ -171,10 +179,15 @@ class AutofillFilterView : AppCompatActivity() {
|
|||
setOnCheckedChangeListener { _, _ -> updateSearch() }
|
||||
}
|
||||
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 ->
|
||||
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
|
||||
// the message is not yet shown (and vice versa).
|
||||
if ((list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) ||
|
||||
|
@ -189,16 +202,21 @@ class AutofillFilterView : AppCompatActivity() {
|
|||
private fun updateSearch() {
|
||||
model.search(
|
||||
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,
|
||||
listMode = ListMode.FilesOnly
|
||||
)
|
||||
}
|
||||
|
||||
private fun decryptAndFill(item: PasswordItem) {
|
||||
if (binding.shouldClear.isChecked) AutofillMatcher.clearMatchesFor(applicationContext, formOrigin)
|
||||
if (binding.shouldMatch.isChecked) AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file)
|
||||
if (binding.shouldClear.isChecked)
|
||||
AutofillMatcher.clearMatchesFor(applicationContext, formOrigin)
|
||||
if (binding.shouldMatch.isChecked)
|
||||
AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file)
|
||||
// intent?.extras? is checked to be non-null in onCreate
|
||||
decryptAction.launch(AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this))
|
||||
decryptAction.launch(
|
||||
AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,9 +83,15 @@ class AutofillPublisherChangedActivity : AppCompatActivity() {
|
|||
resetButton.visibility = View.VISIBLE
|
||||
}
|
||||
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)
|
||||
setResult(RESULT_OK, Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) })
|
||||
setResult(
|
||||
RESULT_OK,
|
||||
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) }
|
||||
)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
@ -96,13 +102,18 @@ class AutofillPublisherChangedActivity : AppCompatActivity() {
|
|||
with(binding) {
|
||||
val packageInfo = packageManager.getPackageInfo(appPackage, PackageManager.GET_META_DATA)
|
||||
val installTime = DateUtils.getRelativeTimeSpanString(packageInfo.firstInstallTime)
|
||||
warningAppInstallDate.text = getString(R.string.oreo_autofill_warning_publisher_install_time, installTime)
|
||||
warningAppInstallDate.text =
|
||||
getString(R.string.oreo_autofill_warning_publisher_install_time, installTime)
|
||||
val appInfo = packageManager.getApplicationInfo(appPackage, PackageManager.GET_META_DATA)
|
||||
warningAppName.text = "“${packageManager.getApplicationLabel(appInfo)}”"
|
||||
|
||||
val currentHash = computeCertificatesHash(this@AutofillPublisherChangedActivity, appPackage)
|
||||
warningAppAdvancedInfo.text =
|
||||
getString(R.string.oreo_autofill_warning_publisher_advanced_info_template, appPackage, currentHash)
|
||||
getString(
|
||||
R.string.oreo_autofill_warning_publisher_advanced_info_template,
|
||||
appPackage,
|
||||
currentHash
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure { e ->
|
||||
|
|
|
@ -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_PASSWORD = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_PASSWORD"
|
||||
private const val EXTRA_NAME = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_NAME"
|
||||
private const val EXTRA_SHOULD_MATCH_APP = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP"
|
||||
private const val EXTRA_SHOULD_MATCH_WEB = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_WEB"
|
||||
private const val EXTRA_GENERATE_PASSWORD = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_GENERATE_PASSWORD"
|
||||
private const val EXTRA_SHOULD_MATCH_APP =
|
||||
"dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP"
|
||||
private const val EXTRA_SHOULD_MATCH_WEB =
|
||||
"dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_WEB"
|
||||
private const val EXTRA_GENERATE_PASSWORD =
|
||||
"dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_GENERATE_PASSWORD"
|
||||
|
||||
private var saveRequestCode = 1
|
||||
|
||||
fun makeSaveIntentSender(context: Context, credentials: Credentials?, formOrigin: FormOrigin): IntentSender {
|
||||
fun makeSaveIntentSender(
|
||||
context: Context,
|
||||
credentials: Credentials?,
|
||||
formOrigin: FormOrigin
|
||||
): IntentSender {
|
||||
val identifier = formOrigin.getPrettyIdentifier(context, untrusted = false)
|
||||
// Prevent directory traversals
|
||||
val sanitizedIdentifier =
|
||||
|
@ -52,7 +59,11 @@ class AutofillSaveActivity : AppCompatActivity() {
|
|||
sanitizedIdentifier = sanitizedIdentifier,
|
||||
username = credentials?.username
|
||||
)
|
||||
val fileName = directoryStructure.getSaveFileName(username = credentials?.username, identifier = identifier)
|
||||
val fileName =
|
||||
directoryStructure.getSaveFileName(
|
||||
username = credentials?.username,
|
||||
identifier = identifier
|
||||
)
|
||||
val intent =
|
||||
Intent(context, AutofillSaveActivity::class.java).apply {
|
||||
putExtras(
|
||||
|
@ -60,13 +71,20 @@ class AutofillSaveActivity : AppCompatActivity() {
|
|||
EXTRA_FOLDER_NAME to folderName,
|
||||
EXTRA_NAME to fileName,
|
||||
EXTRA_PASSWORD to credentials?.password,
|
||||
EXTRA_SHOULD_MATCH_APP to formOrigin.identifier.takeIf { formOrigin is FormOrigin.App },
|
||||
EXTRA_SHOULD_MATCH_WEB to formOrigin.identifier.takeIf { formOrigin is FormOrigin.Web },
|
||||
EXTRA_SHOULD_MATCH_APP to
|
||||
formOrigin.identifier.takeIf { formOrigin is FormOrigin.App },
|
||||
EXTRA_SHOULD_MATCH_WEB to
|
||||
formOrigin.identifier.takeIf { formOrigin is FormOrigin.Web },
|
||||
EXTRA_GENERATE_PASSWORD to (credentials == null)
|
||||
)
|
||||
)
|
||||
}
|
||||
return PendingIntent.getActivity(context, saveRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
saveRequestCode++,
|
||||
intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
)
|
||||
.intentSender
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +112,8 @@ class AutofillSaveActivity : AppCompatActivity() {
|
|||
"FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath,
|
||||
PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
|
||||
PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
|
||||
PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
|
||||
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 fillInDataset =
|
||||
AutofillResponseBuilder.makeFillInDataset(this, credentials, clientState, AutofillAction.Generate)
|
||||
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }
|
||||
AutofillResponseBuilder.makeFillInDataset(
|
||||
this,
|
||||
credentials,
|
||||
clientState,
|
||||
AutofillAction.Generate
|
||||
)
|
||||
Intent().apply {
|
||||
putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset)
|
||||
}
|
||||
} else {
|
||||
// Password was extracted from a form, there is nothing to fill.
|
||||
Intent()
|
||||
|
|
|
@ -157,7 +157,10 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
|
|||
return
|
||||
} else {
|
||||
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 {
|
||||
val relativePath = getRelativePath(fullPath, repositoryPath)
|
||||
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 */
|
||||
|
|
|
@ -44,7 +44,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
|
|||
private val binding by viewBinding(DecryptLayoutBinding::inflate)
|
||||
@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 val userInteractionRequiredResult =
|
||||
|
@ -136,7 +138,10 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
|
|||
intent.putExtra("REPO_PATH", repoPath)
|
||||
intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name)
|
||||
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)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
|
@ -150,7 +155,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
|
|||
type = "text/plain"
|
||||
}
|
||||
// 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)
|
||||
|
@ -166,7 +173,10 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
|
|||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
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)) {
|
||||
OpenPgpApi.RESULT_CODE_SUCCESS -> {
|
||||
startAutoDismissTimer()
|
||||
|
@ -174,7 +184,8 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
|
|||
val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true)
|
||||
val entry = passwordEntryFactory.create(lifecycleScope, outputStream.toByteArray())
|
||||
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)) {
|
||||
copyPasswordToClipboard(entry.password)
|
||||
|
@ -190,7 +201,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
|
|||
if (entry.hasTotp()) {
|
||||
launch {
|
||||
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!!))
|
||||
}
|
||||
|
||||
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
|
||||
adapter.updateItems(items)
|
||||
|
|
|
@ -55,7 +55,9 @@ class GetKeyIdsActivity : BasePgpActivity() {
|
|||
OpenPgpApi.RESULT_CODE_SUCCESS -> {
|
||||
runCatching {
|
||||
val ids =
|
||||
result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)?.map { OpenPgpUtils.convertKeyIdToHex(it) }
|
||||
result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)?.map {
|
||||
OpenPgpUtils.convertKeyIdToHex(it)
|
||||
}
|
||||
?: emptyList()
|
||||
val keyResult = Intent().putExtra(OpenPgpApi.EXTRA_KEY_IDS, ids.toTypedArray())
|
||||
setResult(RESULT_OK, keyResult)
|
||||
|
|
|
@ -63,14 +63,24 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
|
||||
@Inject lateinit var passwordEntryFactory: PasswordEntryFactory
|
||||
|
||||
private val suggestedName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) }
|
||||
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 suggestedName by lazy(LazyThreadSafetyMode.NONE) {
|
||||
intent.getStringExtra(EXTRA_FILE_NAME)
|
||||
}
|
||||
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) {
|
||||
intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
|
||||
}
|
||||
private val editing by lazy(LazyThreadSafetyMode.NONE) { intent.getBooleanExtra(EXTRA_EDITING, false) }
|
||||
private val oldFileName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) }
|
||||
private val editing by lazy(LazyThreadSafetyMode.NONE) {
|
||||
intent.getBooleanExtra(EXTRA_EDITING, false)
|
||||
}
|
||||
private val oldFileName by lazy(LazyThreadSafetyMode.NONE) {
|
||||
intent.getStringExtra(EXTRA_FILE_NAME)
|
||||
}
|
||||
private var oldCategory: String? = null
|
||||
private var copy: Boolean = false
|
||||
private var encryptionIntent: Intent = Intent()
|
||||
|
@ -99,7 +109,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data)
|
||||
val contents = "${intentResult.contents}\n"
|
||||
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)
|
||||
snackbar(message = getString(R.string.otp_import_success))
|
||||
} else {
|
||||
|
@ -113,18 +124,27 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds ->
|
||||
lifecycleScope.launch {
|
||||
val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id")
|
||||
withContext(Dispatchers.IO) { gpgIdentifierFile.writeText((keyIds + "").joinToString("\n")) }
|
||||
withContext(Dispatchers.IO) {
|
||||
gpgIdentifierFile.writeText((keyIds + "").joinToString("\n"))
|
||||
}
|
||||
commitChange(
|
||||
getString(
|
||||
R.string.git_commit_gpg_id,
|
||||
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
|
||||
getLongName(
|
||||
gpgIdentifierFile.parentFile!!.absolutePath,
|
||||
repoPath,
|
||||
gpgIdentifierFile.name
|
||||
)
|
||||
)
|
||||
)
|
||||
.onSuccess { encrypt(encryptionIntent) }
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
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) {
|
||||
setContentView(root)
|
||||
generatePassword.setOnClickListener { generatePassword() }
|
||||
otpImportButton.setOnClickListener {
|
||||
supportFragmentManager.setFragmentResultListener(OTP_RESULT_REQUEST_KEY, this@PasswordCreationActivity) {
|
||||
requestKey,
|
||||
bundle ->
|
||||
supportFragmentManager.setFragmentResultListener(
|
||||
OTP_RESULT_REQUEST_KEY,
|
||||
this@PasswordCreationActivity
|
||||
) { requestKey, bundle ->
|
||||
if (requestKey == OTP_RESULT_REQUEST_KEY) {
|
||||
val contents = bundle.getString(RESULT)
|
||||
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)
|
||||
}
|
||||
}
|
||||
val hasCamera = packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) == true
|
||||
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)
|
||||
.setItems(items) { _, 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
|
||||
// FileBased.
|
||||
if (suggestedName == null &&
|
||||
AutofillPreferences.directoryStructure(this@PasswordCreationActivity) == DirectoryStructure.FileBased
|
||||
AutofillPreferences.directoryStructure(this@PasswordCreationActivity) ==
|
||||
DirectoryStructure.FileBased
|
||||
) {
|
||||
encryptUsername.apply {
|
||||
visibility = View.VISIBLE
|
||||
|
@ -226,7 +254,10 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
// User wants to disable username encryption, so we extract the
|
||||
// username from the encrypted extras and use it as the filename.
|
||||
val entry =
|
||||
passwordEntryFactory.create(lifecycleScope, "PASSWORD\n${extraContent.text}".encodeToByteArray())
|
||||
passwordEntryFactory.create(
|
||||
lifecycleScope,
|
||||
"PASSWORD\n${extraContent.text}".encodeToByteArray()
|
||||
)
|
||||
val username = entry.username
|
||||
|
||||
// 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 {
|
||||
password.setText(it)
|
||||
|
@ -279,21 +312,29 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
}
|
||||
|
||||
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) {
|
||||
binding.password.setText(bundle.getString(RESULT))
|
||||
}
|
||||
}
|
||||
when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) {
|
||||
KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
|
||||
KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment().show(supportFragmentManager, "xkpwgenerator")
|
||||
KEY_PWGEN_TYPE_CLASSIC ->
|
||||
PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
|
||||
KEY_PWGEN_TYPE_XKPASSWD ->
|
||||
XkPasswordGeneratorDialogFragment().show(supportFragmentManager, "xkpwgenerator")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViewState() =
|
||||
with(binding) {
|
||||
// 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 {
|
||||
if (visibility != View.VISIBLE) return@apply
|
||||
val hasUsernameInFileName = filename.text.toString().isNotBlank()
|
||||
|
@ -354,14 +395,18 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
}
|
||||
}
|
||||
if (gpgIdentifiers.isEmpty()) {
|
||||
gpgKeySelectAction.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java))
|
||||
gpgKeySelectAction.launch(
|
||||
Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)
|
||||
)
|
||||
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()) {
|
||||
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()) {
|
||||
encryptionIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds)
|
||||
}
|
||||
|
@ -396,7 +441,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
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)) {
|
||||
OpenPgpApi.RESULT_CODE_SUCCESS -> {
|
||||
runCatching {
|
||||
|
@ -405,7 +452,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
// Additionally, if we were editing and the incoming and outgoing
|
||||
// filenames differ, it means we renamed. Ensure that the target
|
||||
// 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))
|
||||
return@runCatching
|
||||
}
|
||||
|
@ -415,7 +464,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
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
|
||||
// history
|
||||
|
@ -432,7 +483,10 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
val returnIntent = Intent()
|
||||
returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path)
|
||||
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) {
|
||||
val directoryStructure = AutofillPreferences.directoryStructure(applicationContext)
|
||||
|
@ -442,13 +496,18 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
|
|||
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")
|
||||
if (oldFile.path != file.path && !oldFile.delete()) {
|
||||
setResult(RESULT_CANCELED)
|
||||
MaterialAlertDialogBuilder(this@PasswordCreationActivity)
|
||||
.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)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> finish() }
|
||||
.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 {
|
||||
commitChange(resources.getString(commitMessageRes, getLongName(fullPath, repoPath, editName)))
|
||||
commitChange(
|
||||
resources.getString(commitMessageRes, getLongName(fullPath, repoPath, editName))
|
||||
)
|
||||
.onSuccess {
|
||||
setResult(RESULT_OK, returnIntent)
|
||||
finish()
|
||||
|
|
|
@ -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()
|
||||
return layoutInflater.inflate(R.layout.basic_bottom_sheet, container, false)
|
||||
}
|
||||
|
@ -85,7 +89,9 @@ private constructor(
|
|||
}
|
||||
if (negativeButtonClickListener != null) {
|
||||
binding.bottomSheetCancelButton.isVisible = true
|
||||
negativeButtonLabel?.let { buttonLbl -> binding.bottomSheetCancelButton.text = buttonLbl }
|
||||
negativeButtonLabel?.let { buttonLbl ->
|
||||
binding.bottomSheetCancelButton.text = buttonLbl
|
||||
}
|
||||
binding.bottomSheetCancelButton.setOnClickListener {
|
||||
negativeButtonClickListener.onClick(it)
|
||||
dismiss()
|
||||
|
@ -95,7 +101,9 @@ private constructor(
|
|||
}
|
||||
)
|
||||
val gradientDrawable =
|
||||
GradientDrawable().apply { setColor(requireContext().resolveAttribute(android.R.attr.windowBackground)) }
|
||||
GradientDrawable().apply {
|
||||
setColor(requireContext().resolveAttribute(android.R.attr.windowBackground))
|
||||
}
|
||||
view.background = gradientDrawable
|
||||
}
|
||||
|
||||
|
@ -133,13 +141,19 @@ private constructor(
|
|||
return this
|
||||
}
|
||||
|
||||
fun setPositiveButtonClickListener(buttonLabel: String? = null, listener: View.OnClickListener): Builder {
|
||||
fun setPositiveButtonClickListener(
|
||||
buttonLabel: String? = null,
|
||||
listener: View.OnClickListener
|
||||
): Builder {
|
||||
this.positiveButtonClickListener = listener
|
||||
this.positiveButtonLabel = buttonLabel
|
||||
return this
|
||||
}
|
||||
|
||||
fun setNegativeButtonClickListener(buttonLabel: String? = null, listener: View.OnClickListener): Builder {
|
||||
fun setNegativeButtonClickListener(
|
||||
buttonLabel: String? = null,
|
||||
listener: View.OnClickListener
|
||||
): Builder {
|
||||
this.negativeButtonClickListener = listener
|
||||
this.negativeButtonLabel = buttonLabel
|
||||
return this
|
||||
|
|
|
@ -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()
|
||||
return inflater.inflate(R.layout.item_create_sheet, container, false)
|
||||
}
|
||||
|
@ -67,7 +71,9 @@ class ItemCreationBottomSheet : BottomSheetDialogFragment() {
|
|||
}
|
||||
)
|
||||
val gradientDrawable =
|
||||
GradientDrawable().apply { setColor(requireContext().resolveAttribute(android.R.attr.windowBackground)) }
|
||||
GradientDrawable().apply {
|
||||
setColor(requireContext().resolveAttribute(android.R.attr.windowBackground))
|
||||
}
|
||||
view.background = gradientDrawable
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,10 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
|
|||
val callingActivity = requireActivity()
|
||||
val binding = FragmentPwgenBinding.inflate(layoutInflater)
|
||||
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)
|
||||
|
||||
|
@ -65,7 +68,9 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
|
|||
.apply {
|
||||
setOnShowListener {
|
||||
generate(binding.passwordText)
|
||||
getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { generate(binding.passwordText) }
|
||||
getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
|
||||
generate(binding.passwordText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,9 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
|
|||
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.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
|
||||
|
||||
|
@ -85,8 +87,12 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
|
|||
.setMinimumWordLength(DEFAULT_MIN_WORD_LENGTH)
|
||||
.setMaximumWordLength(DEFAULT_MAX_WORD_LENGTH)
|
||||
.setSeparator(binding.xkSeparator.text.toString())
|
||||
.appendNumbers(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_DIGIT })
|
||||
.appendSymbols(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_SYMBOL })
|
||||
.appendNumbers(
|
||||
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()))
|
||||
.create()
|
||||
.fold(
|
||||
|
|
|
@ -24,7 +24,10 @@ class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
|
|||
|
||||
passwordList = SelectFolderFragment()
|
||||
val args = Bundle()
|
||||
args.putString(PasswordStore.REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath)
|
||||
args.putString(
|
||||
PasswordStore.REQUEST_ARG_PATH,
|
||||
PasswordRepository.getRepositoryDirectory().absolutePath
|
||||
)
|
||||
|
||||
passwordList.arguments = args
|
||||
|
||||
|
@ -32,7 +35,9 @@ class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
|
|||
|
||||
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 {
|
||||
|
|
|
@ -35,7 +35,10 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.fab.hide()
|
||||
recyclerAdapter = PasswordItemRecyclerAdapter().onItemClicked { _, item -> listener.onFragmentInteraction(item) }
|
||||
recyclerAdapter =
|
||||
PasswordItemRecyclerAdapter().onItemClicked { _, item ->
|
||||
listener.onFragmentInteraction(item)
|
||||
}
|
||||
binding.passRecycler.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
itemAnimator = null
|
||||
|
@ -47,7 +50,9 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
|
|||
|
||||
val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH))
|
||||
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) {
|
||||
|
@ -58,12 +63,16 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
|
|||
override fun onFragmentInteraction(item: PasswordItem) {
|
||||
if (item.type == PasswordItem.TYPE_CATEGORY) {
|
||||
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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
err is TransportException && err.disconnectReason == DisconnectReason.HOST_KEY_NOT_VERIFIABLE -> {
|
||||
err is TransportException &&
|
||||
err.disconnectReason == DisconnectReason.HOST_KEY_NOT_VERIFIABLE -> {
|
||||
SSHException(
|
||||
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."
|
||||
|
@ -135,7 +136,9 @@ abstract class BaseGitActivity : ContinuationContainerActivity() {
|
|||
private fun isExplicitlyUserInitiatedError(throwable: Throwable): Boolean {
|
||||
var cause: Throwable? = throwable
|
||||
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
|
||||
}
|
||||
return false
|
||||
|
@ -154,7 +157,8 @@ abstract class BaseGitActivity : ContinuationContainerActivity() {
|
|||
while ((rootCause is org.eclipse.jgit.errors.TransportException ||
|
||||
rootCause is org.eclipse.jgit.api.errors.TransportException ||
|
||||
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
|
||||
}
|
||||
return rootCause
|
||||
|
|
|
@ -55,7 +55,12 @@ class GitConfigActivity : BaseGitActivity() {
|
|||
} else {
|
||||
GitSettings.authorEmail = email
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +82,8 @@ class GitConfigActivity : BaseGitActivity() {
|
|||
if (repo != null) {
|
||||
binding.gitHeadStatus.text = headStatusMsg(repo)
|
||||
// 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.alpha = if (needsAbort) 1.0f else 0.5f
|
||||
}
|
||||
|
|
|
@ -89,7 +89,12 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
binding.clearHostKeyButton.isVisible = GitSettings.hasSavedHostKey()
|
||||
binding.clearHostKeyButton.setOnClickListener {
|
||||
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
|
||||
}
|
||||
binding.saveButton.setOnClickListener {
|
||||
|
@ -102,7 +107,9 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
BasicBottomSheet.Builder(this)
|
||||
.setTitleRes(R.string.https_scheme_with_port_title)
|
||||
.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()
|
||||
.show(supportFragmentManager, "SSH_SCHEME_WARNING")
|
||||
return@setOnClickListener
|
||||
|
@ -110,7 +117,9 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
BasicBottomSheet.Builder(this)
|
||||
.setTitleRes(R.string.ssh_scheme_needed_title)
|
||||
.setMessageRes(R.string.ssh_scheme_needed_message)
|
||||
.setPositiveButtonClickListener { @Suppress("SetTextI18n") binding.serverUrl.setText("ssh://$newUrl") }
|
||||
.setPositiveButtonClickListener {
|
||||
@Suppress("SetTextI18n") binding.serverUrl.setText("ssh://$newUrl")
|
||||
}
|
||||
.build()
|
||||
.show(supportFragmentManager, "SSH_SCHEME_WARNING")
|
||||
return@setOnClickListener
|
||||
|
@ -133,7 +142,12 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
)
|
||||
) {
|
||||
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 -> {
|
||||
when (updateResult.newProtocol) {
|
||||
|
@ -154,9 +168,14 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
}
|
||||
}
|
||||
GitSettings.UpdateConnectionSettingsResult.Valid -> {
|
||||
if (isClone && PasswordRepository.getRepository(null) == null) PasswordRepository.initialize()
|
||||
if (isClone && PasswordRepository.getRepository(null) == null)
|
||||
PasswordRepository.initialize()
|
||||
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()
|
||||
Handler(Looper.getMainLooper()).postDelayed(500) { finish() }
|
||||
} else {
|
||||
|
@ -206,7 +225,9 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory())
|
||||
val localDirFiles = localDir.listFiles() ?: emptyArray()
|
||||
// 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)
|
||||
.setTitle(R.string.dialog_delete_title)
|
||||
|
@ -269,7 +290,9 @@ class GitServerConfigActivity : BaseGitActivity() {
|
|||
private val PORT_REGEX = ":[0-9]{1,5}/".toRegex()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,8 @@ class GitLogAdapter : RecyclerView.Adapter<GitLogAdapter.ViewHolder>() {
|
|||
|
||||
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) =
|
||||
with(binding) {
|
||||
|
|
|
@ -24,7 +24,9 @@ class CloneFragment : Fragment(R.layout.fragment_clone) {
|
|||
|
||||
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 =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
|
|
|
@ -32,7 +32,9 @@ import me.msfjarvis.openpgpktx.util.OpenPgpApi
|
|||
|
||||
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 gpgKeySelectAction =
|
||||
|
@ -45,13 +47,17 @@ class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) {
|
|||
gpgIdentifierFile.writeText((keyIds + "").joinToString("\n"))
|
||||
}
|
||||
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()
|
||||
} else {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,9 @@ import java.io.File
|
|||
|
||||
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) {
|
||||
Intent(requireContext(), DirectorySelectionActivity::class.java)
|
||||
}
|
||||
|
@ -65,7 +67,9 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
|
|||
externalDirectorySelectAction.launch(directorySelectIntent)
|
||||
}
|
||||
|
||||
private val repositoryUsePermGrantedAction = createPermGrantedAction { initializeRepositoryInfo() }
|
||||
private val repositoryUsePermGrantedAction = createPermGrantedAction {
|
||||
initializeRepositoryInfo()
|
||||
}
|
||||
|
||||
private val repositoryChangePermGrantedAction = createPermGrantedAction {
|
||||
repositoryInitAction.launch(directorySelectIntent)
|
||||
|
@ -132,7 +136,12 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
|
|||
dir.isDirectory && // The directory, is really a directory
|
||||
dir.listFilesRecursively().isNotEmpty() && // The directory contains 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()
|
||||
return true
|
||||
|
|
|
@ -27,6 +27,8 @@ class WelcomeFragment : Fragment(R.layout.fragment_welcome) {
|
|||
binding.letsGo.setOnClickListener {
|
||||
parentFragmentManager.performTransactionWithBackStack(CloneFragment.newInstance())
|
||||
}
|
||||
binding.settingsButton.setOnClickListener { startActivity(Intent(requireContext(), SettingsActivity::class.java)) }
|
||||
binding.settingsButton.setOnClickListener {
|
||||
startActivity(Intent(requireContext(), SettingsActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,8 +77,12 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
settings = requireContext().sharedPrefs
|
||||
initializePasswordList()
|
||||
binding.fab.setOnClickListener { ItemCreationBottomSheet().show(childFragmentManager, "BOTTOM_SHEET") }
|
||||
childFragmentManager.setFragmentResultListener(ITEM_CREATION_REQUEST_KEY, viewLifecycleOwner) { _, bundle ->
|
||||
binding.fab.setOnClickListener {
|
||||
ItemCreationBottomSheet().show(childFragmentManager, "BOTTOM_SHEET")
|
||||
}
|
||||
childFragmentManager.setFragmentResultListener(ITEM_CREATION_REQUEST_KEY, viewLifecycleOwner) {
|
||||
_,
|
||||
bundle ->
|
||||
when (bundle.getString(ACTION_KEY)) {
|
||||
ACTION_FOLDER -> requireStore().createFolder()
|
||||
ACTION_PASSWORD -> requireStore().createPassword()
|
||||
|
@ -88,7 +92,8 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
|
||||
private fun initializePasswordList() {
|
||||
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 {
|
||||
if (!hasGitDir) {
|
||||
requireStore().refreshPasswordList()
|
||||
|
@ -118,7 +123,9 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
binding.swipeRefresher.isRefreshing = false
|
||||
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
|
||||
|
||||
if (actionMode == null)
|
||||
actionMode = requireStore().startSupportActionMode(actionModeCallback) ?: return@onSelectionChanged
|
||||
actionMode =
|
||||
requireStore().startSupportActionMode(actionModeCallback) ?: return@onSelectionChanged
|
||||
|
||||
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()
|
||||
} else {
|
||||
actionMode!!.finish()
|
||||
|
@ -171,14 +184,18 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
recyclerView.scrollToPosition(0)
|
||||
}
|
||||
scrollTarget != null -> {
|
||||
scrollTarget?.let { recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it)) }
|
||||
scrollTarget?.let {
|
||||
recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it))
|
||||
}
|
||||
scrollTarget = null
|
||||
}
|
||||
else -> {
|
||||
// When the result is not filtered and there is a saved scroll position for
|
||||
// it,
|
||||
// we try to restore it.
|
||||
recyclerViewStateToRestore?.let { recyclerView.layoutManager!!.onRestoreInstanceState(it) }
|
||||
recyclerViewStateToRestore?.let {
|
||||
recyclerView.layoutManager!!.onRestoreInstanceState(it)
|
||||
}
|
||||
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.
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
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 =
|
||||
selectedItems.size == 1 && selectedItems[0].type == PasswordItem.TYPE_PASSWORD
|
||||
return true
|
||||
|
@ -227,7 +245,10 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
}
|
||||
R.id.menu_pin_password -> {
|
||||
val passwordItem = recyclerAdapter.getSelectedItems()[0]
|
||||
shortcutHandler.addPinnedShortcut(passwordItem, passwordItem.createAuthEnabledIntent(requireContext()))
|
||||
shortcutHandler.addPinnedShortcut(
|
||||
passwordItem,
|
||||
passwordItem.createAuthEnabledIntent(requireContext())
|
||||
)
|
||||
false
|
||||
}
|
||||
else -> false
|
||||
|
@ -244,7 +265,8 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
|
||||
private fun animateFab(show: Boolean) =
|
||||
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(
|
||||
object : Animation.AnimationListener {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -269,10 +295,15 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
listener =
|
||||
object : OnFragmentInteractionListener {
|
||||
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
|
||||
val preferences = context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
|
||||
preferences.edit { putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString()) }
|
||||
val preferences =
|
||||
context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
|
||||
preferences.edit {
|
||||
putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString())
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -322,7 +355,10 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
|
||||
fun navigateTo(file: File) {
|
||||
requireStore().clearSearch()
|
||||
model.navigateTo(file, recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState())
|
||||
model.navigateTo(
|
||||
file,
|
||||
recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState()
|
||||
)
|
||||
requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,9 @@ class PasswordStore : BaseGitActivity() {
|
|||
}
|
||||
|
||||
private val storagePermissionRequest =
|
||||
registerForActivityResult(RequestPermission()) { granted -> if (granted) checkLocalRepository() }
|
||||
registerForActivityResult(RequestPermission()) { granted ->
|
||||
if (granted) checkLocalRepository()
|
||||
}
|
||||
|
||||
private val directorySelectAction =
|
||||
registerForActivityResult(StartActivityForResult()) { result ->
|
||||
|
@ -128,7 +130,13 @@ class PasswordStore : BaseGitActivity() {
|
|||
withContext(Dispatchers.Main) {
|
||||
MaterialAlertDialogBuilder(this@PasswordStore)
|
||||
.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) { _, _ ->
|
||||
launch(Dispatchers.IO) { moveFile(source, destinationFile) }
|
||||
}
|
||||
|
@ -143,11 +151,16 @@ class PasswordStore : BaseGitActivity() {
|
|||
1 -> {
|
||||
val source = File(filesToMove[0])
|
||||
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)
|
||||
withContext(Dispatchers.Main) {
|
||||
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 {
|
||||
// open search view on search key, or Ctr+F
|
||||
if ((keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) &&
|
||||
!searchItem.isActionViewExpanded
|
||||
if ((keyCode == KeyEvent.KEYCODE_SEARCH ||
|
||||
keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) && !searchItem.isActionViewExpanded
|
||||
) {
|
||||
searchItem.expandActionView()
|
||||
return true
|
||||
|
@ -202,7 +215,9 @@ class PasswordStore : BaseGitActivity() {
|
|||
|
||||
model.currentDir.observe(this) { dir ->
|
||||
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()
|
||||
// List the contents of the current directory if the user enters a blank
|
||||
// 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)
|
||||
return true
|
||||
}
|
||||
|
@ -288,7 +304,9 @@ class PasswordStore : BaseGitActivity() {
|
|||
.setPositiveButton(resources.getString(R.string.dialog_ok), null)
|
||||
when (id) {
|
||||
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 -> {
|
||||
if (!PasswordRepository.isInitialized) {
|
||||
|
@ -372,7 +390,8 @@ class PasswordStore : BaseGitActivity() {
|
|||
if (localDir != null && settings.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)) {
|
||||
d { "Check, dir: ${localDir.absolutePath}" }
|
||||
// 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) }
|
||||
val args = Bundle()
|
||||
args.putString(REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath)
|
||||
|
@ -403,7 +422,9 @@ class PasswordStore : BaseGitActivity() {
|
|||
fun decryptPassword(item: PasswordItem) {
|
||||
val authDecryptIntent = item.createAuthEnabledIntent(this)
|
||||
val decryptIntent =
|
||||
(authDecryptIntent.clone() as Intent).setComponent(ComponentName(this, DecryptActivity::class.java))
|
||||
(authDecryptIntent.clone() as Intent).setComponent(
|
||||
ComponentName(this, DecryptActivity::class.java)
|
||||
)
|
||||
|
||||
startActivity(decryptIntent)
|
||||
|
||||
|
@ -439,7 +460,9 @@ class PasswordStore : BaseGitActivity() {
|
|||
|
||||
fun deletePasswords(selectedItems: List<PasswordItem>) {
|
||||
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) {
|
||||
selectedItems.map { item -> item.file.deleteRecursively() }
|
||||
refreshPasswordList()
|
||||
|
@ -497,7 +520,10 @@ class PasswordStore : BaseGitActivity() {
|
|||
* @see [CategoryRenameError]
|
||||
* @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 newCategoryEditText = view.findViewById<TextInputEditText>(R.id.folder_name_text)
|
||||
|
||||
|
@ -513,16 +539,19 @@ class PasswordStore : BaseGitActivity() {
|
|||
.setPositiveButton(R.string.dialog_ok) { _, _ ->
|
||||
val newCategory = File("${oldCategory.file.parent}/${newCategoryEditText.text}")
|
||||
when {
|
||||
newCategoryEditText.text.isNullOrBlank() -> renameCategory(oldCategory, CategoryRenameError.EmptyField)
|
||||
newCategoryEditText.text.isNullOrBlank() ->
|
||||
renameCategory(oldCategory, CategoryRenameError.EmptyField)
|
||||
newCategory.exists() -> renameCategory(oldCategory, CategoryRenameError.CategoryExists)
|
||||
!newCategory.isInsideRepository() -> renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo)
|
||||
!newCategory.isInsideRepository() ->
|
||||
renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo)
|
||||
else ->
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
moveFile(oldCategory.file, newCategory)
|
||||
|
||||
// associate the new category with the last category's timestamp in
|
||||
// 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())
|
||||
if (timestamp != null) {
|
||||
preference.edit {
|
||||
|
@ -533,7 +562,11 @@ class PasswordStore : BaseGitActivity() {
|
|||
|
||||
withContext(Dispatchers.Main) {
|
||||
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
|
||||
// obtain the corresponding target file by resolving the relative path
|
||||
// starting at the destination folder.
|
||||
source.listFilesRecursively().associateWith { destinationFile.resolve(it.relativeTo(source)) }
|
||||
source.listFilesRecursively().associateWith {
|
||||
destinationFile.resolve(it.relativeTo(source))
|
||||
}
|
||||
} else {
|
||||
mapOf(source to destinationFile)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,9 @@ private val WEB_ADDRESS_REGEX = Patterns.WEB_URL.toRegex()
|
|||
class ProxySelectorActivity : AppCompatActivity() {
|
||||
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -36,7 +38,9 @@ class ProxySelectorActivity : AppCompatActivity() {
|
|||
with(binding) {
|
||||
proxyHost.setText(proxyPrefs.getString(PreferenceKeys.PROXY_HOST))
|
||||
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))
|
||||
save.setOnClickListener { saveSettings() }
|
||||
proxyHost.doOnTextChanged { text, _, _, _ ->
|
||||
|
@ -54,10 +58,18 @@ class ProxySelectorActivity : AppCompatActivity() {
|
|||
|
||||
private fun saveSettings() {
|
||||
proxyPrefs.edit {
|
||||
binding.proxyHost.text?.toString()?.takeIf { it.isNotEmpty() }.let { GitSettings.proxyHost = 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 }
|
||||
binding.proxyHost.text?.toString()?.takeIf { it.isNotEmpty() }.let {
|
||||
GitSettings.proxyHost = 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()
|
||||
Handler(Looper.getMainLooper()).postDelayed(500) { finish() }
|
||||
|
|
|
@ -59,13 +59,18 @@ class AutofillSettings(private val activity: FragmentActivity) : SettingsProvide
|
|||
val appLabel = it.first
|
||||
val supportDescription =
|
||||
when (it.second) {
|
||||
BrowserAutofillSupportLevel.None -> activity.getString(R.string.oreo_autofill_no_support)
|
||||
BrowserAutofillSupportLevel.FlakyFill -> activity.getString(R.string.oreo_autofill_flaky_fill_support)
|
||||
BrowserAutofillSupportLevel.None ->
|
||||
activity.getString(R.string.oreo_autofill_no_support)
|
||||
BrowserAutofillSupportLevel.FlakyFill ->
|
||||
activity.getString(R.string.oreo_autofill_flaky_fill_support)
|
||||
BrowserAutofillSupportLevel.PasswordFill ->
|
||||
activity.getString(R.string.oreo_autofill_password_fill_support)
|
||||
BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility ->
|
||||
activity.getString(R.string.oreo_autofill_password_fill_and_conditional_save_support)
|
||||
BrowserAutofillSupportLevel.GeneralFill -> activity.getString(R.string.oreo_autofill_general_fill_support)
|
||||
activity.getString(
|
||||
R.string.oreo_autofill_password_fill_and_conditional_save_support
|
||||
)
|
||||
BrowserAutofillSupportLevel.GeneralFill ->
|
||||
activity.getString(R.string.oreo_autofill_general_fill_support)
|
||||
BrowserAutofillSupportLevel.GeneralFillAndSave ->
|
||||
activity.getString(R.string.oreo_autofill_general_fill_and_save_support)
|
||||
}
|
||||
|
@ -102,8 +107,10 @@ class AutofillSettings(private val activity: FragmentActivity) : SettingsProvide
|
|||
false
|
||||
}
|
||||
}
|
||||
val values = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_values)
|
||||
val titles = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_entries)
|
||||
val values =
|
||||
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) }
|
||||
singleChoice(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE, items) {
|
||||
initialSelection = DirectoryStructure.DEFAULT.value
|
||||
|
|
|
@ -39,7 +39,9 @@ class DirectorySelectionActivity : AppCompatActivity() {
|
|||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(resources.getString(R.string.sdcard_root_warning_title))
|
||||
.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) }
|
||||
}
|
||||
.setNegativeButton(R.string.dialog_cancel, null)
|
||||
|
|
|
@ -26,7 +26,8 @@ class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider
|
|||
builder.apply {
|
||||
val themeValues = activity.resources.getStringArray(R.array.app_theme_values)
|
||||
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) {
|
||||
initialSelection = activity.resources.getString(R.string.app_theme_def)
|
||||
titleRes = R.string.pref_app_theme_title
|
||||
|
@ -64,7 +65,8 @@ class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider
|
|||
defaultValue = false
|
||||
enabled = canAuthenticate
|
||||
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 {
|
||||
enabled = false
|
||||
val isChecked = checked
|
||||
|
|
|
@ -37,7 +37,9 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys
|
|||
|
||||
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>) {
|
||||
activity.startActivity(Intent(activity, clazz))
|
||||
|
@ -47,7 +49,9 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
|
|||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(activity.resources.getString(R.string.external_repository_dialog_title))
|
||||
.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)
|
||||
.show()
|
||||
}
|
||||
|
@ -130,7 +134,9 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
|
|||
}
|
||||
pref(PreferenceKeys.SSH_OPENKEYSTORE_CLEAR_KEY_ID) {
|
||||
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 {
|
||||
activity.sharedPrefs.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null) }
|
||||
visible = false
|
||||
|
@ -160,7 +166,9 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
|
|||
removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList())
|
||||
}
|
||||
}
|
||||
activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) }
|
||||
activity.sharedPrefs.edit {
|
||||
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
|
||||
}
|
||||
dialogInterface.cancel()
|
||||
activity.finish()
|
||||
}
|
||||
|
|
|
@ -68,7 +68,9 @@ class SettingsActivity : AppCompatActivity() {
|
|||
getString(subScreen.titleRes)
|
||||
}
|
||||
}
|
||||
savedInstanceState?.getParcelable<PreferencesAdapter.SavedState>("adapter")?.let(adapter::loadSavedState)
|
||||
savedInstanceState
|
||||
?.getParcelable<PreferencesAdapter.SavedState>("adapter")
|
||||
?.let(adapter::loadSavedState)
|
||||
binding.preferenceRecyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,9 @@ class ShowSshKeyFragment : DialogFragment() {
|
|||
return MaterialAlertDialogBuilder(requireActivity()).run {
|
||||
setMessage(getString(R.string.ssh_keygen_message, publicKey))
|
||||
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) { _, _ ->
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
|
|
|
@ -30,9 +30,15 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
|
||||
private enum class KeyGenType(val generateKey: suspend (requireAuthentication: Boolean) -> Unit) {
|
||||
Rsa({ requireAuthentication -> SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication) }),
|
||||
Ecdsa({ requireAuthentication -> SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication) }),
|
||||
Ed25519({ requireAuthentication -> SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication) }),
|
||||
Rsa({ requireAuthentication ->
|
||||
SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication)
|
||||
}),
|
||||
Ecdsa({ requireAuthentication ->
|
||||
SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication)
|
||||
}),
|
||||
Ed25519({ requireAuthentication ->
|
||||
SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication)
|
||||
}),
|
||||
}
|
||||
|
||||
class SshKeyGenActivity : AppCompatActivity() {
|
||||
|
@ -50,7 +56,9 @@ class SshKeyGenActivity : AppCompatActivity() {
|
|||
MaterialAlertDialogBuilder(this@SshKeyGenActivity).run {
|
||||
setTitle(R.string.ssh_keygen_existing_title)
|
||||
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() }
|
||||
show()
|
||||
}
|
||||
|
|
|
@ -26,7 +26,12 @@ class SshKeyImportActivity : AppCompatActivity() {
|
|||
}
|
||||
runCatching {
|
||||
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)
|
||||
finish()
|
||||
}
|
||||
|
|
|
@ -19,7 +19,8 @@ import dev.msfjarvis.aps.R
|
|||
object 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 {
|
||||
data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
|
||||
|
@ -29,7 +30,8 @@ object BiometricAuthenticator {
|
|||
}
|
||||
|
||||
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(
|
||||
|
@ -55,7 +57,11 @@ object BiometricAuthenticator {
|
|||
BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
|
||||
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))
|
||||
.setAllowedAuthenticators(validAuthenticators)
|
||||
.build()
|
||||
BiometricPrompt(activity, ContextCompat.getMainExecutor(activity.applicationContext), authCallback)
|
||||
BiometricPrompt(
|
||||
activity,
|
||||
ContextCompat.getMainExecutor(activity.applicationContext),
|
||||
authCallback
|
||||
)
|
||||
.authenticate(promptInfo)
|
||||
} else {
|
||||
callback(Result.HardwareUnavailableOrDisabled)
|
||||
|
|
|
@ -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
|
||||
val metadata = makeFillMatchMetadata(context, file)
|
||||
val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
|
||||
|
@ -82,12 +86,21 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
|
|||
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 (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null
|
||||
val metadata = makeFillOtpFromSmsMetadata(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(
|
||||
|
@ -150,7 +163,12 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
|
|||
addDataset(it)
|
||||
}
|
||||
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) }
|
||||
setClientState(clientState)
|
||||
setIgnoredIds(*ignoredIds.toTypedArray())
|
||||
|
@ -177,7 +195,11 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
|
|||
}
|
||||
|
||||
/** 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)
|
||||
.fold(
|
||||
success = { matchedFiles ->
|
||||
|
|
|
@ -35,7 +35,9 @@ private fun Context.matchPreferences(formOrigin: FormOrigin): SharedPreferences
|
|||
}
|
||||
|
||||
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 {
|
||||
require(formOrigin is FormOrigin.App)
|
||||
|
@ -50,10 +52,12 @@ class AutofillMatcher {
|
|||
private const val MAX_NUM_MATCHES = 10
|
||||
|
||||
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 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 {
|
||||
return when (formOrigin) {
|
||||
|
@ -61,7 +65,8 @@ class AutofillMatcher {
|
|||
is FormOrigin.App -> {
|
||||
val packageName = formOrigin.identifier
|
||||
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
|
||||
if (hashHasChanged) {
|
||||
e { "$packageName: stored=$storedCertificatesHash, new=$certificatesHash" }
|
||||
|
@ -91,15 +96,21 @@ class AutofillMatcher {
|
|||
* time the user associated an entry with it, an [AutofillPublisherChangedException] will be
|
||||
* 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)) {
|
||||
return Err(AutofillPublisherChangedException(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(
|
||||
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)
|
||||
}
|
||||
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)
|
||||
if (newFiles.size > MAX_NUM_MATCHES) {
|
||||
Toast.makeText(
|
||||
|
@ -138,7 +150,9 @@ class AutofillMatcher {
|
|||
.show()
|
||||
return
|
||||
}
|
||||
matchPreferences.edit { putStringSet(matchesKey(formOrigin), newFiles.map { it.absolutePath }.toSet()) }
|
||||
matchPreferences.edit {
|
||||
putStringSet(matchesKey(formOrigin), newFiles.map { it.absolutePath }.toSet())
|
||||
}
|
||||
storeFormOriginHash(context, formOrigin)
|
||||
d { "Stored match for $formOrigin" }
|
||||
}
|
||||
|
@ -153,7 +167,8 @@ class AutofillMatcher {
|
|||
delete: Collection<File> = emptyList()
|
||||
) {
|
||||
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 ((key, value) in prefs.all) {
|
||||
if (!key.startsWith(PREFERENCE_PREFIX_MATCHES)) continue
|
||||
|
|
|
@ -96,7 +96,8 @@ enum class DirectoryStructure(val value: String) {
|
|||
when (this) {
|
||||
EncryptedUsername -> 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
|
||||
}
|
||||
|
||||
|
@ -138,7 +139,8 @@ object AutofillPreferences {
|
|||
directoryStructure: DirectoryStructure
|
||||
): Credentials {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -154,7 +154,10 @@ class AutofillResponseBuilder(form: FillableForm) {
|
|||
if (datasetCount == 0) return null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
setHeader(
|
||||
makeRemoteView(context, makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true)))
|
||||
makeRemoteView(
|
||||
context,
|
||||
makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true))
|
||||
)
|
||||
)
|
||||
}
|
||||
makeSaveInfo()?.let { setSaveInfo(it) }
|
||||
|
|
|
@ -51,13 +51,15 @@ fun makeInlinePresentation(
|
|||
|
||||
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 =
|
||||
InlineSuggestionUi.newContentBuilder(launchIntent).run {
|
||||
setTitle(metadata.title)
|
||||
if (metadata.subtitle != null) setSubtitle(metadata.subtitle)
|
||||
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))
|
||||
build().slice
|
||||
|
@ -69,13 +71,19 @@ fun makeInlinePresentation(
|
|||
fun makeFillMatchMetadata(context: Context, file: File): DatasetMetadata {
|
||||
val directoryStructure = AutofillPreferences.directoryStructure(context)
|
||||
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)
|
||||
return DatasetMetadata(title, subtitle, R.drawable.ic_person_black_24dp)
|
||||
}
|
||||
|
||||
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) =
|
||||
DatasetMetadata(
|
||||
|
@ -85,7 +93,11 @@ fun makeGenerateAndFillMetadata(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)
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ sealed class GpgIdentifier {
|
|||
if (identifier.isEmpty()) return null
|
||||
// Match long key IDs:
|
||||
// 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) {
|
||||
val keyId = maybeLongKeyId.toULong(16)
|
||||
return KeyId(keyId.toLong())
|
||||
|
@ -25,7 +26,8 @@ sealed class GpgIdentifier {
|
|||
|
||||
// Match fingerprints:
|
||||
// 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) {
|
||||
// Truncating to the long key ID is not a security issue since OpenKeychain only
|
||||
// accepts
|
||||
|
|
|
@ -41,7 +41,11 @@ fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) {
|
|||
setOnShowListener {
|
||||
findViewById<T>(id)?.apply {
|
||||
setOnFocusChangeListener { v, _ ->
|
||||
v.post { context.getSystemService<InputMethodManager>()?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT) }
|
||||
v.post {
|
||||
context
|
||||
.getSystemService<InputMethodManager>()
|
||||
?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
requestFocus()
|
||||
}
|
||||
|
@ -64,7 +68,8 @@ fun Context.getEncryptedProxyPrefs() = getEncryptedPrefs("http_proxy")
|
|||
|
||||
/** Get an instance of [EncryptedSharedPreferences] with the given [fileName] */
|
||||
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(
|
||||
applicationContext,
|
||||
fileName,
|
||||
|
|
|
@ -20,8 +20,10 @@ import kotlin.reflect.KProperty
|
|||
* Imported from
|
||||
* 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) :
|
||||
ReadOnlyProperty<Fragment, T> {
|
||||
class FragmentViewBindingDelegate<T : ViewBinding>(
|
||||
val fragment: Fragment,
|
||||
val viewBindingFactory: (View) -> T
|
||||
) : ReadOnlyProperty<Fragment, T> {
|
||||
|
||||
private var binding: T? = null
|
||||
|
||||
|
@ -51,7 +53,9 @@ class FragmentViewBindingDelegate<T : ViewBinding>(val fragment: Fragment, val v
|
|||
|
||||
val lifecycle = fragment.viewLifecycleOwner.lifecycle
|
||||
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 }
|
||||
|
@ -61,5 +65,6 @@ class FragmentViewBindingDelegate<T : ViewBinding>(val fragment: Fragment, val v
|
|||
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
|
||||
FragmentViewBindingDelegate(this, viewBindingFactory)
|
||||
|
||||
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(crossinline bindingInflater: (LayoutInflater) -> T) =
|
||||
lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) }
|
||||
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
|
||||
crossinline bindingInflater: (LayoutInflater) -> T
|
||||
) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) }
|
||||
|
|
|
@ -14,7 +14,8 @@ import java.net.UnknownHostException
|
|||
/**
|
||||
* 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!!
|
||||
|
||||
|
|
|
@ -74,11 +74,13 @@ class GitCommandExecutor(
|
|||
// Code imported (modified) from Gerrit PushOp, license Apache v2
|
||||
for (rru in result.remoteUpdates) {
|
||||
when (rru.status) {
|
||||
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> throw PushException.NonFastForward
|
||||
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD ->
|
||||
throw PushException.NonFastForward
|
||||
RemoteRefUpdate.Status.REJECTED_NODELETE,
|
||||
RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED,
|
||||
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 -> {
|
||||
throw if ("non-fast-forward" == rru.message) {
|
||||
PushException.RemoteRejected
|
||||
|
|
|
@ -15,4 +15,9 @@ import java.util.Date
|
|||
* @property authorName name of the commit's author without email address.
|
||||
* @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
|
||||
)
|
||||
|
|
|
@ -40,7 +40,9 @@ class GitLogModel {
|
|||
// Additionally, tests with 1000 commits in the log have not produced a significant delay in the
|
||||
// user experience.
|
||||
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
|
||||
|
||||
|
|
|
@ -11,13 +11,17 @@ import org.eclipse.jgit.api.RebaseCommand
|
|||
import org.eclipse.jgit.api.ResetCommand
|
||||
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 resetCommands =
|
||||
arrayOf(
|
||||
// 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
|
||||
git.push().setRemote("origin"),
|
||||
// switch back to ${gitBranch}
|
||||
|
@ -47,8 +51,12 @@ class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) : GitOp
|
|||
if (!git.repository.repositoryState.isRebasing && !merging) {
|
||||
MaterialAlertDialogBuilder(callingActivity)
|
||||
.setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title))
|
||||
.setMessage(callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded))
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() }
|
||||
.setMessage(
|
||||
callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded)
|
||||
)
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
|
||||
callingActivity.finish()
|
||||
}
|
||||
.show()
|
||||
false
|
||||
} else {
|
||||
|
|
|
@ -14,7 +14,8 @@ import org.eclipse.jgit.api.GitCommand
|
|||
* @param uri URL to clone the repository from
|
||||
* @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>> =
|
||||
arrayOf(
|
||||
|
|
|
@ -24,7 +24,8 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys
|
|||
import kotlin.coroutines.Continuation
|
||||
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) {
|
||||
val gitOperationPrefs = callingActivity.getEncryptedGitPrefs()
|
||||
|
@ -49,18 +50,22 @@ class CredentialFinder(val callingActivity: FragmentActivity, val authMode: Auth
|
|||
rememberRes = R.string.git_operation_remember_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) }
|
||||
val storedCredential = gitOperationPrefs.getString(credentialPref, null)
|
||||
if (storedCredential == null) {
|
||||
val layoutInflater = LayoutInflater.from(callingActivity)
|
||||
|
||||
@SuppressLint("InflateParams") val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
|
||||
val credentialLayout = dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout)
|
||||
@SuppressLint("InflateParams")
|
||||
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)
|
||||
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)
|
||||
if (isRetry) {
|
||||
credentialLayout.error = callingActivity.resources.getString(errorRes)
|
||||
|
|
|
@ -60,7 +60,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
|
|||
private val authActivity
|
||||
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
|
||||
|
||||
|
@ -72,7 +73,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
|
|||
is CredentialItem.Username -> item.value = uri?.user
|
||||
is CredentialItem.Password -> {
|
||||
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)
|
||||
}
|
||||
|
@ -102,7 +104,10 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
|
|||
.onFailure { e -> e(e) }
|
||||
}
|
||||
|
||||
private fun registerAuthProviders(authMethod: SshAuthMethod, credentialsProvider: CredentialsProvider? = null) {
|
||||
private fun registerAuthProviders(
|
||||
authMethod: SshAuthMethod,
|
||||
credentialsProvider: CredentialsProvider? = null
|
||||
) {
|
||||
sshSessionFactory = SshjSessionFactory(authMethod, hostKeyFile)
|
||||
commands.filterIsInstance<TransportCommand<*, *>>().forEach { command ->
|
||||
command.setTransportConfigCallback { transport: Transport ->
|
||||
|
@ -132,12 +137,12 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
|
|||
MaterialAlertDialogBuilder(callingActivity)
|
||||
.setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text))
|
||||
.setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title))
|
||||
.setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ ->
|
||||
getSshKey(false)
|
||||
}
|
||||
.setNegativeButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)) { _, _ ->
|
||||
getSshKey(true)
|
||||
}
|
||||
.setPositiveButton(
|
||||
callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)
|
||||
) { _, _ -> getSshKey(false) }
|
||||
.setNegativeButton(
|
||||
callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)
|
||||
) { _, _ -> getSshKey(true) }
|
||||
.setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
|
||||
// Finish the blank GitActivity so user doesn't have to press back
|
||||
callingActivity.finish()
|
||||
|
@ -153,9 +158,10 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
|
|||
val result =
|
||||
withContext(Dispatchers.Main) {
|
||||
suspendCoroutine<BiometricAuthenticator.Result> { cont ->
|
||||
BiometricAuthenticator.authenticate(callingActivity, R.string.biometric_prompt_title_ssh_auth) {
|
||||
if (it !is BiometricAuthenticator.Result.Failure) cont.resume(it)
|
||||
}
|
||||
BiometricAuthenticator.authenticate(
|
||||
callingActivity,
|
||||
R.string.biometric_prompt_title_ssh_auth
|
||||
) { if (it !is BiometricAuthenticator.Result.Failure) cont.resume(it) }
|
||||
}
|
||||
}
|
||||
when (result) {
|
||||
|
@ -193,7 +199,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
|
|||
}
|
||||
AuthMode.OpenKeychain -> registerAuthProviders(SshAuthMethod.OpenKeychain(authActivity))
|
||||
AuthMode.Password -> {
|
||||
val httpsCredentialProvider = HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password))
|
||||
val httpsCredentialProvider =
|
||||
HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password))
|
||||
registerAuthProviders(SshAuthMethod.Password(authActivity), httpsCredentialProvider)
|
||||
}
|
||||
AuthMode.None -> {}
|
||||
|
|
|
@ -7,7 +7,8 @@ package dev.msfjarvis.aps.util.git.operation
|
|||
import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity
|
||||
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>> =
|
||||
arrayOf(
|
||||
|
|
|
@ -7,7 +7,8 @@ package dev.msfjarvis.aps.util.git.operation
|
|||
import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity
|
||||
import org.eclipse.jgit.api.ResetCommand
|
||||
|
||||
class ResetToRemoteOperation(callingActivity: ContinuationContainerActivity) : GitOperation(callingActivity) {
|
||||
class ResetToRemoteOperation(callingActivity: ContinuationContainerActivity) :
|
||||
GitOperation(callingActivity) {
|
||||
|
||||
override val commands =
|
||||
arrayOf(
|
||||
|
|
|
@ -106,7 +106,8 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
|
|||
val response = sshPublicKeyResponse.response as SshPublicKeyResponse
|
||||
val sshPublicKey = response.sshPublicKey!!
|
||||
publicKey =
|
||||
parseSshPublicKey(sshPublicKey) ?: throw IllegalStateException("OpenKeychain API returned invalid SSH key")
|
||||
parseSshPublicKey(sshPublicKey)
|
||||
?: throw IllegalStateException("OpenKeychain API returned invalid SSH key")
|
||||
}
|
||||
is ApiResponse.NoSuchKey ->
|
||||
if (isRetry) {
|
||||
|
@ -122,13 +123,17 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
|
|||
|
||||
private suspend fun selectKey() {
|
||||
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.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" }
|
||||
val result =
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -141,7 +146,11 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
|
|||
}
|
||||
|
||||
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 -> {
|
||||
ApiResponse.Success(
|
||||
when (request) {
|
||||
|
@ -153,20 +162,27 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
|
|||
)
|
||||
}
|
||||
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 =
|
||||
withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { cont ->
|
||||
activity.stashedCont = cont
|
||||
activity.continueAfterUserInteraction.launch(IntentSenderRequest.Builder(pendingIntent).build())
|
||||
activity.continueAfterUserInteraction.launch(
|
||||
IntentSenderRequest.Builder(pendingIntent).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
executeApiRequest(request, resultOfUserInteraction)
|
||||
}
|
||||
else -> {
|
||||
val error = result.getParcelableExtra<SshAuthenticationApiError>(SshAuthenticationApi.EXTRA_ERROR)
|
||||
val error =
|
||||
result.getParcelableExtra<SshAuthenticationApiError>(SshAuthenticationApi.EXTRA_ERROR)
|
||||
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) {
|
||||
SshAuthenticationApiError.NO_AUTH_KEY, SshAuthenticationApiError.NO_SUCH_KEY ->
|
||||
ApiResponse.NoSuchKey(exception)
|
||||
|
@ -181,7 +197,9 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
|
|||
privateKey =
|
||||
object : OpenKeychainPrivateKey {
|
||||
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.GeneralError -> throw signingResponse.exception
|
||||
is ApiResponse.NoSuchKey -> throw signingResponse.exception
|
||||
|
|
|
@ -28,7 +28,8 @@ class OpenKeychainWrappedKeyAlgorithmFactory(private val factory: Factory.Named<
|
|||
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 =
|
||||
when (keyAlgorithm.keyAlgorithm) {
|
||||
|
@ -39,11 +40,14 @@ class OpenKeychainWrappedKeyAlgorithm(private val keyAlgorithm: KeyAlgorithm) :
|
|||
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) :
|
||||
Signature by wrappedSignature {
|
||||
class OpenKeychainWrappedSignature(
|
||||
private val wrappedSignature: Signature,
|
||||
private val hashAlgorithm: Int
|
||||
) : Signature by wrappedSignature {
|
||||
|
||||
private val data = ByteArrayOutputStream()
|
||||
|
||||
|
|
|
@ -115,7 +115,8 @@ object SshKey {
|
|||
|
||||
private var type: 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) {
|
||||
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(
|
||||
KeyProperties.KEY_ALGORITHM_RSA,
|
||||
{
|
||||
setKeySize(3072)
|
||||
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(
|
||||
|
@ -163,7 +171,9 @@ object SshKey {
|
|||
private fun delete() {
|
||||
androidKeystore.deleteEntry(KEYSTORE_ALIAS)
|
||||
// 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) {
|
||||
privateKeyFile.delete()
|
||||
}
|
||||
|
@ -177,7 +187,8 @@ object SshKey {
|
|||
fun import(uri: Uri) {
|
||||
// First check whether the content at uri is likely an SSH private key.
|
||||
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.moveToFirst()
|
||||
cursor.getInt(0)
|
||||
|
@ -186,7 +197,9 @@ object SshKey {
|
|||
|
||||
// We assume that an SSH key's ideal size is > 0 bytes && < 100 kilobytes.
|
||||
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 =
|
||||
context.contentResolver.openInputStream(uri)
|
||||
|
@ -199,7 +212,9 @@ object SshKey {
|
|||
!Regex("BEGIN .* PRIVATE KEY").containsMatchIn(lines.first()) ||
|
||||
!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
|
||||
// key and delete the old key.
|
||||
|
@ -249,7 +264,9 @@ object SshKey {
|
|||
val encryptedPrivateKeyFile = getOrCreateWrappedPrivateKeyFile(requireAuthentication)
|
||||
// Generate the ed25519 key pair and encrypt the private key.
|
||||
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.
|
||||
publicKeyFile.writeText(toSshPublicKey(keyPair.public))
|
||||
|
@ -288,7 +305,8 @@ object SshKey {
|
|||
|
||||
fun provide(client: SSHClient, passphraseFinder: InteractivePasswordFinder): KeyProvider? =
|
||||
when (type) {
|
||||
Type.LegacyGenerated, Type.Imported -> client.loadKeys(privateKeyFile.absolutePath, passphraseFinder)
|
||||
Type.LegacyGenerated, Type.Imported ->
|
||||
client.loadKeys(privateKeyFile.absolutePath, passphraseFinder)
|
||||
Type.KeystoreNative -> KeystoreNativeKeyProvider
|
||||
Type.KeystoreWrappedEd25519 -> KeystoreWrappedEd25519KeyProvider
|
||||
null -> null
|
||||
|
@ -305,7 +323,10 @@ object SshKey {
|
|||
override fun getPrivate(): PrivateKey =
|
||||
runCatching { androidKeystore.sshPrivateKey!! }.getOrElse { 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)
|
||||
|
@ -326,7 +347,9 @@ object SshKey {
|
|||
// for `requireAuthentication` is not used as the key already exists at this point.
|
||||
val encryptedPrivateKeyFile = runBlocking { getOrCreateWrappedPrivateKeyFile(false) }
|
||||
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 ->
|
||||
e(error)
|
||||
|
|
|
@ -37,7 +37,8 @@ fun setUpBouncyCastleForSshj() {
|
|||
// Replace the Android BC provider with the Java BouncyCastle provider since the former does
|
||||
// not include all the required algorithms.
|
||||
// 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) {
|
||||
// No Android BC found, install Java BC at lowest priority.
|
||||
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(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, 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)
|
||||
|
||||
|
@ -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(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, 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)
|
||||
|
||||
|
@ -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(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, 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)
|
||||
|
||||
|
@ -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(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, 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)
|
||||
|
||||
|
@ -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(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, 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)
|
||||
}
|
||||
|
@ -148,7 +159,8 @@ object TimberLoggerFactory : LoggerFactory {
|
|||
|
||||
// Replace slf4j's "{}" format string style with standard Java's "%s".
|
||||
// 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?) {
|
||||
Timber.tag(name).v(t, message.fix(), *args)
|
||||
|
|
|
@ -52,7 +52,10 @@ abstract class InteractivePasswordFinder : PasswordFinder {
|
|||
abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean)
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
?: SshjSession(uri, uri.user, authMethod, hostKeyFile).connect().also {
|
||||
d { "New SSH connection created" }
|
||||
|
@ -81,7 +90,9 @@ private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier {
|
|||
if (!hostKeyFile.exists()) {
|
||||
return HostKeyVerifier { _, _, key ->
|
||||
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)
|
||||
val digestData = digest.digest()
|
||||
val hostKeyEntry = "SHA256:${Base64.encodeToString(digestData, Base64.NO_WRAP)}"
|
||||
|
@ -115,7 +126,9 @@ private class SshjSession(
|
|||
val userPlusHost = "${uri.user}@${uri.host}"
|
||||
val realUser = userPlusHost.substringBeforeLast('@')
|
||||
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 {
|
||||
uri
|
||||
}
|
||||
|
@ -131,7 +144,8 @@ private class SshjSession(
|
|||
ssh.auth(username, passwordAuth)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
command.join(timeout, TimeUnit.SECONDS)
|
||||
|
|
|
@ -42,7 +42,10 @@ object PasswordGenerator {
|
|||
*/
|
||||
fun setPrefs(ctx: Context, options: List<PasswordOption>, targetLength: Int): Boolean {
|
||||
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)
|
||||
}
|
||||
return true
|
||||
|
@ -82,7 +85,9 @@ object PasswordGenerator {
|
|||
} else {
|
||||
// The No* options are false, so the respective character category will be included.
|
||||
when (option) {
|
||||
PasswordOption.NoDigits, PasswordOption.NoUppercaseLetters, PasswordOption.NoLowercaseLetters -> {
|
||||
PasswordOption.NoDigits,
|
||||
PasswordOption.NoUppercaseLetters,
|
||||
PasswordOption.NoLowercaseLetters -> {
|
||||
numCharacterCategories++
|
||||
}
|
||||
PasswordOption.NoAmbiguousCharacters,
|
||||
|
@ -98,7 +103,9 @@ object PasswordGenerator {
|
|||
throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_no_chars_error))
|
||||
}
|
||||
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)) {
|
||||
phonemes = false
|
||||
|
@ -114,7 +121,9 @@ object PasswordGenerator {
|
|||
var iterations = 0
|
||||
do {
|
||||
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 =
|
||||
if (phonemes) {
|
||||
RandomPhonemesGenerator.generate(length, pwgenFlags)
|
||||
|
|
|
@ -36,7 +36,9 @@ object RandomPasswordGenerator {
|
|||
var password = ""
|
||||
while (password.length < targetLength) {
|
||||
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
|
||||
}
|
||||
password += candidate
|
||||
|
|
|
@ -100,7 +100,9 @@ object RandomPhonemesGenerator {
|
|||
if (!candidate.flags.hasFlag(nextBasicType) ||
|
||||
(isStartOfPart && candidate.flags hasFlag NOT_FIRST) ||
|
||||
// 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.
|
||||
(password.length + candidate.length > targetLength) ||
|
||||
(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
|
||||
// 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
|
||||
do {
|
||||
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
|
||||
// Begin a new pronounceable part after every digit.
|
||||
|
@ -143,11 +149,15 @@ object RandomPhonemesGenerator {
|
|||
continue
|
||||
}
|
||||
|
||||
if (!isStartOfPart && pwFlags hasFlag PasswordGenerator.SYMBOLS && secureRandomBiasedBoolean(20)) {
|
||||
if (!isStartOfPart &&
|
||||
pwFlags hasFlag PasswordGenerator.SYMBOLS &&
|
||||
secureRandomBiasedBoolean(20)
|
||||
) {
|
||||
var randomSymbol: Char
|
||||
do {
|
||||
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
|
||||
// Continue the password generation as if nothing was added.
|
||||
}
|
||||
|
@ -157,8 +167,9 @@ object RandomPhonemesGenerator {
|
|||
nextBasicType =
|
||||
when {
|
||||
candidate.flags.hasFlag(CONSONANT) -> VOWEL
|
||||
previousFlags.hasFlag(VOWEL) || candidate.flags.hasFlag(DIPHTHONG) || secureRandomBiasedBoolean(60) ->
|
||||
CONSONANT
|
||||
previousFlags.hasFlag(VOWEL) ||
|
||||
candidate.flags.hasFlag(DIPHTHONG) ||
|
||||
secureRandomBiasedBoolean(60) -> CONSONANT
|
||||
else -> VOWEL
|
||||
}
|
||||
previousFlags = candidate.flags
|
||||
|
|
|
@ -109,7 +109,9 @@ class PasswordBuilder(ctx: Context) {
|
|||
}
|
||||
else candidate
|
||||
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.As_iS -> candidate
|
||||
}
|
||||
|
|
|
@ -30,7 +30,10 @@ class XkpwdDictionary(context: Context) {
|
|||
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 {
|
||||
|
|
|
@ -140,7 +140,10 @@ class ClipboardService : Service() {
|
|||
}
|
||||
|
||||
@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)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText(getString(R.string.tap_clear_clipboard))
|
||||
|
@ -157,7 +160,11 @@ class ClipboardService : Service() {
|
|||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
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>()
|
||||
if (manager != null) {
|
||||
manager.createNotificationChannel(serviceChannel)
|
||||
|
|
|
@ -60,7 +60,11 @@ class OreoAutofillService : AutofillService() {
|
|||
cachePublicSuffixList(applicationContext)
|
||||
}
|
||||
|
||||
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback) {
|
||||
override fun onFillRequest(
|
||||
request: FillRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: FillCallback
|
||||
) {
|
||||
val structure =
|
||||
request.fillContexts.lastOrNull()?.structure
|
||||
?: run {
|
||||
|
@ -93,7 +97,8 @@ class OreoAutofillService : AutofillService() {
|
|||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Api30AutofillResponseBuilder(formToFill).fillCredentials(this, request.inlineSuggestionsRequest, callback)
|
||||
Api30AutofillResponseBuilder(formToFill)
|
||||
.fillCredentials(this, request.inlineSuggestionsRequest, callback)
|
||||
} else {
|
||||
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> {
|
||||
return sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)?.splitToSequence('\n')?.filter {
|
||||
it.isNotBlank() && it.first() != '.' && it.last() != '.'
|
||||
}
|
||||
return sharedPrefs
|
||||
.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)
|
||||
?.splitToSequence('\n')
|
||||
?.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' }
|
||||
?: emptySequence()
|
||||
}
|
||||
|
|
|
@ -137,7 +137,11 @@ class PasswordExportService : Service() {
|
|||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
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>()
|
||||
if (manager != null) {
|
||||
manager.createNotificationChannel(serviceChannel)
|
||||
|
|
|
@ -25,7 +25,8 @@ enum class Protocol(val pref: String) {
|
|||
|
||||
private val map = values().associateBy(Protocol::pref)
|
||||
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)
|
||||
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 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) {
|
||||
Application.instance.getEncryptedGitPrefs()
|
||||
}
|
||||
private val proxySettings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.getEncryptedProxyPrefs() }
|
||||
private val hostKeyPath by lazy(LazyThreadSafetyMode.NONE) { "${Application.instance.filesDir}/.host_key" }
|
||||
private val proxySettings by lazy(LazyThreadSafetyMode.PUBLICATION) {
|
||||
Application.instance.getEncryptedProxyPrefs()
|
||||
}
|
||||
private val hostKeyPath by lazy(LazyThreadSafetyMode.NONE) {
|
||||
"${Application.instance.filesDir}/.host_key"
|
||||
}
|
||||
|
||||
var authMode
|
||||
get() = AuthMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH))
|
||||
|
|
|
@ -60,7 +60,8 @@ private fun migrateToGitUrlBasedConfig(sharedPrefs: SharedPreferences) {
|
|||
val url =
|
||||
when {
|
||||
urlWithFreeEntryScheme.startsWith("https://") -> urlWithFreeEntryScheme
|
||||
urlWithFreeEntryScheme.startsWith("http://") -> urlWithFreeEntryScheme.replaceFirst("http", "https")
|
||||
urlWithFreeEntryScheme.startsWith("http://") ->
|
||||
urlWithFreeEntryScheme.replaceFirst("http", "https")
|
||||
else -> "https://$urlWithFreeEntryScheme"
|
||||
}
|
||||
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) {
|
||||
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.
|
||||
// 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.
|
||||
|
|
|
@ -18,10 +18,15 @@ enum class PasswordSortOrder(val comparator: java.util.Comparator<PasswordItem>)
|
|||
(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(
|
||||
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 timeP2 = recentHistory.getString(p2.file.absolutePath.base64())
|
||||
when {
|
||||
|
|
|
@ -44,7 +44,8 @@ class UriTotpFinder @Inject constructor() : TotpFinder {
|
|||
|
||||
override fun findAlgorithm(content: String): String {
|
||||
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")!!
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,8 +75,14 @@ private fun PasswordItem.Companion.makeComparator(
|
|||
PasswordSortOrder.FILE_FIRST -> compareByDescending { it.type }
|
||||
PasswordSortOrder.RECENTLY_USED -> PasswordSortOrder.RECENTLY_USED.comparator
|
||||
}
|
||||
.then(compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getIdentifierFor(it.file) })
|
||||
.then(compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getUsernameFor(it.file) })
|
||||
.then(
|
||||
compareBy(nullsLast(CaseInsensitiveComparator)) {
|
||||
directoryStructure.getIdentifierFor(it.file)
|
||||
}
|
||||
)
|
||||
.then(
|
||||
compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getUsernameFor(it.file) }
|
||||
)
|
||||
}
|
||||
|
||||
val PasswordItem.stableId: String
|
||||
|
@ -179,7 +185,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
|||
.mapLatest { searchAction ->
|
||||
val listResultFlow =
|
||||
when (searchAction.searchMode) {
|
||||
SearchMode.RecursivelyInSubdirectories -> listFilesRecursively(searchAction.baseDirectory)
|
||||
SearchMode.RecursivelyInSubdirectories ->
|
||||
listFilesRecursively(searchAction.baseDirectory)
|
||||
SearchMode.InCurrentDirectoryOnly -> listFiles(searchAction.baseDirectory)
|
||||
}
|
||||
val prefilteredResultFlow =
|
||||
|
@ -188,9 +195,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
|||
ListMode.DirectoriesOnly -> listResultFlow.filter { it.isDirectory }
|
||||
ListMode.AllEntries -> listResultFlow
|
||||
}
|
||||
val filterModeToUse = if (searchAction.filter == "") FilterMode.NoFilter else searchAction.filterMode
|
||||
val passwordList =
|
||||
when (filterModeToUse) {
|
||||
when (if (searchAction.filter == "") FilterMode.NoFilter else searchAction.filterMode) {
|
||||
FilterMode.NoFilter -> {
|
||||
prefilteredResultFlow.map { it.toPasswordItem() }.toList().sortedWith(itemComparator)
|
||||
}
|
||||
|
@ -201,7 +207,9 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
|||
val regex = generateStrictDomainRegex(searchAction.filter)
|
||||
if (regex != null) {
|
||||
prefilteredResultFlow
|
||||
.filter { absoluteFile -> regex.containsMatchIn(absoluteFile.relativeTo(root).path) }
|
||||
.filter { absoluteFile ->
|
||||
regex.containsMatchIn(absoluteFile.relativeTo(root).path)
|
||||
}
|
||||
.map { it.toPasswordItem() }
|
||||
.toList()
|
||||
.sortedWith(itemComparator)
|
||||
|
@ -218,7 +226,9 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
|||
.filter { it.first > 0 }
|
||||
.toList()
|
||||
.sortedWith(
|
||||
compareByDescending<Pair<Int, PasswordItem>> { it.first }.thenBy(itemComparator) { it.second }
|
||||
compareByDescending<Pair<Int, PasswordItem>> { it.first }.thenBy(itemComparator) {
|
||||
it.second
|
||||
}
|
||||
)
|
||||
.map { it.second }
|
||||
}
|
||||
|
@ -387,7 +397,9 @@ open class SearchableRepositoryAdapter<T : RecyclerView.ViewHolder>(
|
|||
addObserver(
|
||||
object : SelectionTracker.SelectionObserver<String>() {
|
||||
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
|
||||
open fun onItemClicked(listener: (holder: T, item: PasswordItem) -> Unit): SearchableRepositoryAdapter<T> {
|
||||
check(onItemClickedListener == null) { "Only a single listener can be registered for onItemClicked" }
|
||||
open fun onItemClicked(
|
||||
listener: (holder: T, item: PasswordItem) -> Unit
|
||||
): SearchableRepositoryAdapter<T> {
|
||||
check(onItemClickedListener == null) {
|
||||
"Only a single listener can be registered for onItemClicked"
|
||||
}
|
||||
onItemClickedListener = listener
|
||||
return this
|
||||
}
|
||||
|
||||
private var onSelectionChangedListener: ((selection: Selection<String>) -> Unit)? = null
|
||||
open fun onSelectionChanged(listener: (selection: Selection<String>) -> Unit): SearchableRepositoryAdapter<T> {
|
||||
check(onSelectionChangedListener == null) { "Only a single listener can be registered for onSelectionChanged" }
|
||||
open fun onSelectionChanged(
|
||||
listener: (selection: Selection<String>) -> Unit
|
||||
): SearchableRepositoryAdapter<T> {
|
||||
check(onSelectionChangedListener == null) {
|
||||
"Only a single listener can be registered for onSelectionChanged"
|
||||
}
|
||||
onSelectionChangedListener = listener
|
||||
return this
|
||||
}
|
||||
|
|
|
@ -96,4 +96,3 @@
|
|||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
|
|
@ -77,7 +77,12 @@ class AutofillSmsActivity : AppCompatActivity() {
|
|||
|
||||
fun makeFillOtpFromSmsIntentSender(context: Context): IntentSender {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -122,8 +127,10 @@ class AutofillSmsActivity : AppCompatActivity() {
|
|||
|
||||
private suspend fun waitForSms() {
|
||||
val smsClient = SmsCodeRetriever.getAutofillClient(this@AutofillSmsActivity)
|
||||
runCatching { withContext(Dispatchers.IO) { smsClient.startSmsCodeRetriever().suspendableAwait() } }.onFailure { e
|
||||
->
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) { smsClient.startSmsCodeRetriever().suspendableAwait() }
|
||||
}
|
||||
.onFailure { e ->
|
||||
if (e is ResolvableApiException) {
|
||||
e.startResolutionForResult(this@AutofillSmsActivity, 1)
|
||||
} else {
|
||||
|
@ -144,7 +151,10 @@ class AutofillSmsActivity : AppCompatActivity() {
|
|||
clientState,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,8 @@ public sealed class FormOrigin(public open val identifier: String) {
|
|||
when (this) {
|
||||
is Web -> identifier
|
||||
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)
|
||||
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
|
||||
// is null, but we encountered web origins elsewhere in the AssistStructure, the
|
||||
// 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? {
|
||||
val form = AutofillFormParser(context, structure, isManualRequest, customSuffixes)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,14 +49,19 @@ public fun computeCertificatesHash(context: Context, appPackage: String): String
|
|||
// hashes comparable between versions and hence default to using the deprecated API.
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
@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() })
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
val info = context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNING_CERTIFICATES)
|
||||
val signaturesNew = info.signingInfo.signingCertificateHistory ?: info.signingInfo.apkContentsSigners
|
||||
val info =
|
||||
context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNING_CERTIFICATES)
|
||||
val signaturesNew =
|
||||
info.signingInfo.signingCertificateHistory ?: info.signingInfo.apkContentsSigners
|
||||
val stableHashNew = stableHash(signaturesNew.map { it.toByteArray() })
|
||||
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
|
||||
}
|
||||
|
@ -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)
|
||||
for (i in 0 until node.childCount) {
|
||||
visitViewNode(node.getChildAt(i), block)
|
||||
|
@ -114,7 +122,9 @@ private fun visitViewNode(node: AssistStructure.ViewNode, block: (AssistStructur
|
|||
}
|
||||
|
||||
@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
|
||||
visitViewNodes(this) { if (it.autofillId == autofillId) node = it }
|
||||
return node
|
||||
|
|
|
@ -56,9 +56,15 @@ public sealed class AutofillScenario<out T : Any> {
|
|||
username = clientState.getParcelable(BUNDLE_KEY_USERNAME_ID)
|
||||
fillUsername = clientState.getBoolean(BUNDLE_KEY_FILL_USERNAME)
|
||||
otp = clientState.getParcelable(BUNDLE_KEY_OTP_ID)
|
||||
currentPassword.addAll(clientState.getParcelableArrayList(BUNDLE_KEY_CURRENT_PASSWORD_IDS) ?: emptyList())
|
||||
newPassword.addAll(clientState.getParcelableArrayList(BUNDLE_KEY_NEW_PASSWORD_IDS) ?: emptyList())
|
||||
genericPassword.addAll(clientState.getParcelableArrayList(BUNDLE_KEY_GENERIC_PASSWORD_IDS) ?: emptyList())
|
||||
currentPassword.addAll(
|
||||
clientState.getParcelableArrayList(BUNDLE_KEY_CURRENT_PASSWORD_IDS) ?: emptyList()
|
||||
)
|
||||
newPassword.addAll(
|
||||
clientState.getParcelableArrayList(BUNDLE_KEY_NEW_PASSWORD_IDS) ?: emptyList()
|
||||
)
|
||||
genericPassword.addAll(
|
||||
clientState.getParcelableArrayList(BUNDLE_KEY_GENERIC_PASSWORD_IDS) ?: emptyList()
|
||||
)
|
||||
}
|
||||
.build()
|
||||
} 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>()
|
||||
builder.username = username?.let(transform)
|
||||
builder.fillUsername = fillUsername
|
||||
|
@ -253,7 +261,10 @@ internal fun AutofillScenario<AutofillId>.toBundle(): Bundle =
|
|||
putParcelable(AutofillScenario.BUNDLE_KEY_USERNAME_ID, username)
|
||||
putBoolean(AutofillScenario.BUNDLE_KEY_FILL_USERNAME, fillUsername)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
@ -262,7 +273,10 @@ internal fun AutofillScenario<AutofillId>.toBundle(): Bundle =
|
|||
putParcelable(AutofillScenario.BUNDLE_KEY_USERNAME_ID, username)
|
||||
putBoolean(AutofillScenario.BUNDLE_KEY_FILL_USERNAME, fillUsername)
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,14 @@ import androidx.annotation.RequiresApi
|
|||
import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Certain
|
||||
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
|
||||
|
@ -32,7 +35,8 @@ internal val autofillStrategy = strategy {
|
|||
}
|
||||
currentPassword(optional = true) {
|
||||
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.
|
||||
// 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
|
||||
|
@ -109,7 +113,9 @@ internal val autofillStrategy = strategy {
|
|||
rule(applyInSingleOriginMode = true) {
|
||||
newPassword { takeSingle { hasHintNewPassword && isFocused } }
|
||||
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) {
|
||||
currentPassword { takeSingle { hasAutocompleteHintCurrentPassword && isFocused } }
|
||||
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) {
|
||||
genericPassword { takeSingle { passwordCertainty >= Likely && isFocused } }
|
||||
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 {
|
||||
username { takeSingle { hasHintUsername && isFocused } }
|
||||
currentPassword(matchHidden = true) {
|
||||
takeSingle { alreadyMatched -> directlyFollows(alreadyMatched.singleOrNull()) && couldBeTwoStepHiddenPassword }
|
||||
takeSingle { alreadyMatched ->
|
||||
directlyFollows(alreadyMatched.singleOrNull()) && couldBeTwoStepHiddenPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
// focused at the time the rule runs, the fill suggestion will only show if it ever receives
|
||||
// focus.
|
||||
rule(applyInSingleOriginMode = true) { currentPassword { takeSingle { hasAutocompleteHintCurrentPassword } } }
|
||||
rule(applyInSingleOriginMode = true) {
|
||||
currentPassword { takeSingle { hasAutocompleteHintCurrentPassword } }
|
||||
}
|
||||
|
||||
// See above.
|
||||
rule(applyInSingleOriginMode = true) { genericPassword { takeSingle { true } } }
|
||||
|
@ -171,10 +187,14 @@ internal val autofillStrategy = strategy {
|
|||
rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) {
|
||||
genericPassword { takeSingle { isFocused } }
|
||||
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.
|
||||
rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) { username { takeSingle { isFocused } } }
|
||||
rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) {
|
||||
username { takeSingle { isFocused } }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,28 +20,41 @@ internal interface FieldMatcher {
|
|||
class Builder {
|
||||
|
||||
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 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 }) {
|
||||
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
|
||||
}
|
||||
|
||||
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" }
|
||||
tieBreakersSingle.add(block)
|
||||
}
|
||||
|
||||
fun takePair(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" }
|
||||
fun takePair(
|
||||
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
|
||||
}
|
||||
|
||||
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(takeSingle == null) { "takeSingle cannot be mixed with breakTieOnPair" }
|
||||
tieBreakersPair.add(block)
|
||||
|
@ -69,7 +82,8 @@ internal class SingleFieldMatcher(
|
|||
class Builder {
|
||||
|
||||
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 }) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -180,7 +196,10 @@ private constructor(
|
|||
}
|
||||
|
||||
@AutofillDsl
|
||||
class Builder(private val applyInSingleOriginMode: Boolean, private val applyOnManualRequestOnly: Boolean) {
|
||||
class Builder(
|
||||
private val applyInSingleOriginMode: Boolean,
|
||||
private val applyOnManualRequestOnly: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
|
@ -286,9 +305,13 @@ private constructor(
|
|||
"Rules with applyInSingleOriginMode set to true must not fill into hidden fields"
|
||||
}
|
||||
}
|
||||
return AutofillRule(matchers, applyInSingleOriginMode, applyOnManualRequestOnly, name ?: "Rule #$ruleId").also {
|
||||
ruleId++
|
||||
}
|
||||
return AutofillRule(
|
||||
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()
|
||||
|
|
|
@ -63,7 +63,10 @@ private val TRUSTED_BROWSER_CERTIFICATE_HASH =
|
|||
"com.chrome.canary" to arrayOf("IBnfofsj779wxbzRRDxb6rBPPy/0Nm6aweNFdjmiTPw="),
|
||||
"com.chrome.dev" to arrayOf("kETuX+5LvF4h3URmVDHE6x8fcaMnFqC8knvLs5Izyr8="),
|
||||
"com.duckduckgo.mobile.android" to
|
||||
arrayOf("u3uzHFc8RqHaf8XFKKas9DIQhFb+7FCBDH8zaU6z0tQ=", "8HB9AhwL8+b43MEbo/VwBCXVl9yjAaMeIQVWk067Gwo="),
|
||||
arrayOf(
|
||||
"u3uzHFc8RqHaf8XFKKas9DIQhFb+7FCBDH8zaU6z0tQ=",
|
||||
"8HB9AhwL8+b43MEbo/VwBCXVl9yjAaMeIQVWk067Gwo="
|
||||
),
|
||||
"com.microsoft.emmx" to arrayOf("AeGZlxCoLCdJtNUMRF3IXWcLYTYInQp2anOCfIKh6sk="),
|
||||
"com.opera.mini.native" 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.torproject.torbrowser" to arrayOf("IAYfBF5zfGc3XBd5TP7bQ2oDzsa6y3y5+WZCIFyizsg="),
|
||||
"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="),
|
||||
)
|
||||
|
||||
|
@ -162,19 +166,30 @@ private val BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY =
|
|||
|
||||
private fun isNoAccessibilityServiceEnabled(context: Context): Boolean {
|
||||
// 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()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun getBrowserSaveFlag(context: Context, appPackage: String): Int? =
|
||||
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)
|
||||
internal fun getBrowserAutofillSupportInfoIfTrusted(context: Context, appPackage: String): BrowserAutofillSupportInfo? {
|
||||
internal fun getBrowserAutofillSupportInfoIfTrusted(
|
||||
context: Context,
|
||||
appPackage: String
|
||||
): BrowserAutofillSupportInfo? {
|
||||
if (!isTrustedBrowser(context, appPackage)) return null
|
||||
return BrowserAutofillSupportInfo(
|
||||
multiOriginMethod = getBrowserMultiOriginMethod(appPackage),
|
||||
|
@ -197,14 +212,18 @@ public enum class BrowserAutofillSupportLevel {
|
|||
}
|
||||
|
||||
@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)
|
||||
return when {
|
||||
browserInfo == null -> BrowserAutofillSupportLevel.None
|
||||
appPackage in FLAKY_BROWSERS -> BrowserAutofillSupportLevel.FlakyFill
|
||||
appPackage in BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY ->
|
||||
BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility
|
||||
browserInfo.multiOriginMethod == BrowserMultiOriginMethod.None -> BrowserAutofillSupportLevel.PasswordFill
|
||||
browserInfo.multiOriginMethod == BrowserMultiOriginMethod.None ->
|
||||
BrowserAutofillSupportLevel.PasswordFill
|
||||
browserInfo.saveFlags == null -> BrowserAutofillSupportLevel.GeneralFill
|
||||
else -> BrowserAutofillSupportLevel.GeneralFillAndSave
|
||||
}.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
|
||||
// with native Autofill support offer full save support as well, we reuse the list of those
|
||||
// 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
|
||||
}
|
||||
|
@ -222,9 +242,15 @@ public fun getInstalledBrowsersWithAutofillSupportLevel(
|
|||
context: Context
|
||||
): List<Pair<String, BrowserAutofillSupportLevel>> {
|
||||
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
|
||||
.map { it to getBrowserAutofillSupportLevel(context, it.activityInfo.packageName) }
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,9 +108,14 @@ internal class FormField(
|
|||
"text",
|
||||
)
|
||||
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 =
|
||||
listOf(
|
||||
"url_bar", // Chrome/Edge/Firefox address bar
|
||||
|
@ -214,7 +219,8 @@ internal class FormField(
|
|||
private val hasAutocompleteHintUsername = htmlAutocomplete == "username"
|
||||
val hasAutocompleteHintCurrentPassword = htmlAutocomplete == "current-password"
|
||||
private val hasAutocompleteHintNewPassword = htmlAutocomplete == "new-password"
|
||||
private val hasAutocompleteHintPassword = hasAutocompleteHintCurrentPassword || hasAutocompleteHintNewPassword
|
||||
private val hasAutocompleteHintPassword =
|
||||
hasAutocompleteHintCurrentPassword || hasAutocompleteHintNewPassword
|
||||
private val hasAutocompleteHintOtp = htmlAutocomplete == "one-time-code"
|
||||
|
||||
// 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
|
||||
// hint.
|
||||
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
|
||||
// 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
|
||||
|
||||
// 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.
|
||||
// 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)
|
||||
private val isCertainPasswordField = isPossiblePasswordField && hasHintPassword
|
||||
private val isLikelyPasswordField =
|
||||
isPossiblePasswordField && (isCertainPasswordField || PASSWORD_HEURISTIC_TERMS.anyMatchesFieldInfo)
|
||||
isPossiblePasswordField &&
|
||||
(isCertainPasswordField || PASSWORD_HEURISTIC_TERMS.anyMatchesFieldInfo)
|
||||
val passwordCertainty =
|
||||
if (isCertainPasswordField) CertaintyLevel.Certain
|
||||
else if (isLikelyPasswordField) CertaintyLevel.Likely
|
||||
|
@ -273,17 +283,20 @@ internal class FormField(
|
|||
isPossibleOtpField &&
|
||||
(isCertainOtpField ||
|
||||
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 =
|
||||
if (isCertainOtpField) CertaintyLevel.Certain
|
||||
else if (isLikelyOtpField) CertaintyLevel.Likely
|
||||
else if (isPossibleOtpField) CertaintyLevel.Possible else CertaintyLevel.Impossible
|
||||
|
||||
// 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 isLikelyUsernameField =
|
||||
isPossibleUsernameField && (isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.anyMatchesFieldInfo))
|
||||
isPossibleUsernameField &&
|
||||
(isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.anyMatchesFieldInfo))
|
||||
val usernameCertainty =
|
||||
if (isCertainUsernameField) CertaintyLevel.Certain
|
||||
else if (isLikelyUsernameField) CertaintyLevel.Likely
|
||||
|
|
|
@ -34,12 +34,18 @@ public fun cachePublicSuffixList(context: Context) {
|
|||
* Note: Invalid domains, such as IP addresses, are returned unchanged and thus never collide with
|
||||
* 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 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,
|
||||
// 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
|
||||
} else {
|
||||
getCanonicalSuffix(context, domain, customSuffixes)
|
||||
|
@ -60,7 +66,11 @@ private fun getSuffixPlusUpToOne(domain: String, suffix: String): String? {
|
|||
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 publicSuffixPlusOne = publicSuffixList.getPublicSuffixPlusOne(domain).await() ?: return domain
|
||||
var longestSuffix = publicSuffixPlusOne
|
||||
|
|
|
@ -58,7 +58,8 @@ internal class PublicSuffixList(
|
|||
fun getPublicSuffixPlusOne(domain: String): Deferred<String?> =
|
||||
scope.async {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,10 @@ import java.net.IDN
|
|||
import mozilla.components.lib.publicsuffixlist.ext.binarySearch
|
||||
|
||||
/** 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? {
|
||||
return rules.binarySearch(labels, labelIndex)
|
||||
|
|
|
@ -30,7 +30,12 @@ internal object PublicSuffixListLoader {
|
|||
|
||||
@Suppress("MagicNumber")
|
||||
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 {
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -115,11 +115,13 @@ constructor(
|
|||
.lineSequence()
|
||||
.filter { line ->
|
||||
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
|
||||
false
|
||||
}
|
||||
line.startsWith("otpauth://", ignoreCase = true) || line.startsWith("totp:", ignoreCase = true) -> {
|
||||
line.startsWith("otpauth://", ignoreCase = true) ||
|
||||
line.startsWith("totp:", ignoreCase = true) -> {
|
||||
false
|
||||
}
|
||||
else -> {
|
||||
|
@ -174,7 +176,8 @@ constructor(
|
|||
private fun findUsername(): String? {
|
||||
extraContentString.splitToSequence("\n").forEach { line ->
|
||||
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
|
||||
|
|
|
@ -23,7 +23,8 @@ internal object Otp {
|
|||
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 decodedSecret = BASE_32.decode(secret)
|
||||
val secretKey = SecretKeySpec(decodedSecret, algo)
|
||||
|
|
|
@ -19,7 +19,8 @@ import org.junit.Test
|
|||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
|
||||
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
|
||||
fun testGetPassword() {
|
||||
|
@ -49,7 +50,10 @@ internal class PasswordEntryTest {
|
|||
assertEquals("blubb", makeEntry("\nblubb").extraContentString)
|
||||
assertEquals("blubb", makeEntry("blubb\npassword: foo").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("").extraContentString)
|
||||
}
|
||||
|
|
|
@ -15,16 +15,34 @@ internal class OtpTest {
|
|||
|
||||
@Test
|
||||
fun testOtpGeneration6Digits() {
|
||||
assertEquals("953550", 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())
|
||||
assertEquals(
|
||||
"953550",
|
||||
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
|
||||
fun testOtpGeneration10Digits() {
|
||||
assertEquals("0740900914", 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())
|
||||
assertEquals(
|
||||
"0740900914",
|
||||
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
|
||||
|
@ -42,7 +60,10 @@ internal class OtpTest {
|
|||
"127764",
|
||||
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
|
||||
|
|
|
@ -15,7 +15,11 @@ public class AutocryptPeerUpdate() : Parcelable {
|
|||
private var effectiveDate: Date? = null
|
||||
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.effectiveDate = effectiveDate
|
||||
this.preferEncrypt = preferEncrypt
|
||||
|
|
|
@ -21,7 +21,11 @@ public class OpenPgpApi(private val context: Context, private val service: IOpen
|
|||
|
||||
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
|
||||
return try {
|
||||
if (inputStream != null) {
|
||||
|
@ -124,7 +128,8 @@ public class OpenPgpApi(private val context: Context, private val service: IOpen
|
|||
*
|
||||
* 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
|
||||
|
@ -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)
|
||||
* 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,
|
||||
|
@ -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
|
||||
* 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
|
||||
|
@ -217,7 +225,8 @@ public class OpenPgpApi(private val context: Context, private val service: IOpen
|
|||
*
|
||||
* 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)
|
||||
|
@ -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_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 */
|
||||
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_DECRYPTION_RESULT: String = "decryption_result"
|
||||
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_UPDATE: String = "autocrypt_peer_update"
|
||||
public const val EXTRA_AUTOCRYPT_PEER_GOSSIP_UPDATES: String = "autocrypt_peer_gossip_updates"
|
||||
|
|
|
@ -65,7 +65,12 @@ public class OpenPgpServiceConnection(context: Context, providerPackageName: Str
|
|||
val serviceIntent = Intent(OpenPgpApi.SERVICE_INTENT_2)
|
||||
// NOTE: setPackage is very important to restrict the intent to this provider only!
|
||||
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) {
|
||||
throw Exception("bindService() returned false!")
|
||||
}
|
||||
|
|
|
@ -13,7 +13,10 @@ import java.util.regex.Pattern
|
|||
public object OpenPgpUtils {
|
||||
|
||||
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 =
|
||||
Pattern.compile(
|
||||
".*?(-----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()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -22,7 +22,11 @@ public class OpenPgpDecryptionResult() : Parcelable {
|
|||
decryptedSessionKey = null
|
||||
}
|
||||
|
||||
private constructor(result: Int, sessionKey: ByteArray?, decryptedSessionKey: ByteArray?) : this() {
|
||||
private constructor(
|
||||
result: Int,
|
||||
sessionKey: ByteArray?,
|
||||
decryptedSessionKey: ByteArray?
|
||||
) : this() {
|
||||
this.result = result
|
||||
if (sessionKey == null != (decryptedSessionKey == null)) {
|
||||
throw AssertionError("sessionkey must be null iff decryptedSessionKey is null")
|
||||
|
|
|
@ -32,7 +32,12 @@ public class OpenPgpMetadata() : Parcelable {
|
|||
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.mimeType = mimeType
|
||||
this.modificationTime = modificationTime
|
||||
|
|
|
@ -58,7 +58,8 @@ public class OpenPgpSignatureResult : Parcelable {
|
|||
}
|
||||
// backward compatibility for this exact version
|
||||
if (version > 2) {
|
||||
senderStatusResult = readEnumWithNullAndFallback(source, SenderStatusResult.values(), SenderStatusResult.UNKNOWN)
|
||||
senderStatusResult =
|
||||
readEnumWithNullAndFallback(source, SenderStatusResult.values(), SenderStatusResult.UNKNOWN)
|
||||
confirmedUserIds = source.createStringArrayList()
|
||||
} else {
|
||||
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(
|
||||
result,
|
||||
primaryUserId,
|
||||
|
@ -253,18 +256,55 @@ public class OpenPgpSignatureResult : Parcelable {
|
|||
}
|
||||
|
||||
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 {
|
||||
return OpenPgpSignatureResult(RESULT_KEY_MISSING, null, keyId, null, null, null, null, signatureTimestamp, null)
|
||||
public fun createWithKeyMissing(
|
||||
keyId: Long,
|
||||
signatureTimestamp: Date?
|
||||
): OpenPgpSignatureResult {
|
||||
return OpenPgpSignatureResult(
|
||||
RESULT_KEY_MISSING,
|
||||
null,
|
||||
keyId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
signatureTimestamp,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
if (valueOrdinal == -1) {
|
||||
return null
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue