Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,50 @@ import com.hegocre.nextcloudpasswords.BuildConfig
/**
* Parser used to get the needed information from an assist structure to reply to an Autofill request.
*
* @param assistStructure The assist structure provided by the Autofill Request, containing all autofill
* fields information
* @param assistStructures The list assist structures provided by the Autofill Request, containing all autofill
* fields information, the list must be non-empty
*/
@RequiresApi(Build.VERSION_CODES.O)
class AssistStructureParser(assistStructure: AssistStructure) {
val usernameAutofillIds = mutableListOf<AutofillId>()
val passwordAutofillIds = mutableListOf<AutofillId>()
val usernameAutofillContent = mutableListOf<String?>()
val passwordAutofillContent = mutableListOf<String?>()
private var lastTextAutofillId: AutofillId? = null
private var lastTextAutofillContent: String? = null
private var candidateTextAutofillId: AutofillId? = null

val structure = assistStructure
class AssistStructureParser(assistStructures: List<AssistStructure>) {
val usernameAutofillData = mutableListOf<Pair<AutofillId, String?>>()
val passwordAutofillData = mutableListOf<Pair<AutofillId, String?>>()
private var lastTextAutofillData: Pair<AutofillId, String?>? = null
private var candidateTextAutofillData: Pair<AutofillId, String?>? = null

val structures = assistStructures

private val webDomains = HashMap<String, Int>()

val packageName = assistStructure.activityComponent.flattenToShortString().substringBefore("/")
val packageName = assistStructures.last().activityComponent.flattenToShortString().substringBefore("/")

// Get the most repeated domain on the fields (there may be more than one)
val webDomain: String?
get() = webDomains.toList().filter { it.first != "localhost" }
.maxByOrNull { (_, value) -> value }?.first

val usernameAutofillIds: List<AutofillId>
get() = usernameAutofillData.map { it.first }

val passwordAutofillIds: List<AutofillId>
get() = passwordAutofillData.map { it.first }

val usernameAutofillContent: List<String?>
get() = usernameAutofillData.map { it.second }

val passwordAutofillContent: List<String?>
get() = passwordAutofillData.map { it.second }

init {
for (i in 0 until assistStructure.windowNodeCount) {
val windowNode = assistStructure.getWindowNodeAt(i)
windowNode.rootViewNode?.let { parseNode(it) }
// parse the structures from the most recent one
assistStructures.reversed().forEach { assistStructure ->
for (i in 0 until assistStructure.windowNodeCount) {
val windowNode = assistStructure.getWindowNodeAt(i)
windowNode.rootViewNode?.let { parseNode(it) }
}
}
if (usernameAutofillIds.isEmpty())
candidateTextAutofillId?.let {
usernameAutofillIds.add(it)
usernameAutofillContent.add(lastTextAutofillContent)
if (usernameAutofillData.isEmpty())
candidateTextAutofillData?.let {
usernameAutofillData.add(it)
}
}

Expand All @@ -60,19 +71,20 @@ class AssistStructureParser(assistStructure: AssistStructure) {
if (fieldType != null) {
when (fieldType) {
FIELD_TYPE_USERNAME -> {
usernameAutofillIds.add(autofillId)
usernameAutofillContent.add(node.text.toString())
if (!usernameAutofillIds.contains(autofillId)) {
usernameAutofillData.add(Pair(autofillId, node.autofillValue?.textValue.toString()))
}
}
FIELD_TYPE_PASSWORD -> {
passwordAutofillIds.add(autofillId)
passwordAutofillContent.add(node.text.toString())
// We save the autofillId of the field above the password field,
// in case we don't find any explicit username field
candidateTextAutofillId = lastTextAutofillId
if (!passwordAutofillIds.contains(autofillId)) {
passwordAutofillData.add(Pair(autofillId, node.autofillValue?.textValue.toString()))
// We save the autofillId of the field above the password field,
// in case we don't find any explicit username field
candidateTextAutofillData = lastTextAutofillData
}
}
FIELD_TYPE_TEXT -> {
lastTextAutofillId = autofillId
lastTextAutofillContent = node.text.toString()
lastTextAutofillData = Pair(autofillId, node.autofillValue?.textValue.toString())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,42 +30,56 @@ import com.hegocre.nextcloudpasswords.utils.PasswordAutofillData
object AutofillHelper {
fun buildDataset(
context: Context,
password: PasswordAutofillData?,
helper: AssistStructureParser,
inlinePresentationSpec: InlinePresentationSpec?,
intent: IntentSender? = null,
needsAppLock: Boolean = false,
datasetIdx: Int = 0
password: PasswordAutofillData?,
intent: IntentSender?,
needsAppLock: Boolean,
intentIdx: Int = 0
): Dataset {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (inlinePresentationSpec != null) {
buildInlineDataset(
context,
password,
helper,
inlinePresentationSpec,
password,
intent,
needsAppLock,
datasetIdx
intentIdx
)
} else {
buildPresentationDataset(context, password, helper, intent, needsAppLock, datasetIdx)
buildPresentationDataset(
context,
helper,
password,
intent,
needsAppLock,
intentIdx
)
}
} else {
buildPresentationDataset(context, password, helper, intent, needsAppLock, datasetIdx)
buildPresentationDataset(
context,
helper,
password,
intent,
needsAppLock,
intentIdx
)
}
}

@RequiresApi(Build.VERSION_CODES.P)
fun buildSaveInfo(helper: AssistStructureParser): Pair<SaveInfo, Bundle?>? {
fun buildSaveInfo(helper: AssistStructureParser, searchHint: String): Pair<SaveInfo, Bundle?>? {
val requiredIds = mutableListOf<AutofillId>()
val optionalIds = mutableListOf<AutofillId>()

Log.d(NCPAutofillService.TAG, "Building SaveInfo, usernameAutofillIds: ${helper.usernameAutofillIds}, passwordAutofillIds: ${helper.passwordAutofillIds}")

if (helper.passwordAutofillIds.size == 1) requiredIds += helper.passwordAutofillIds[0]
else optionalIds += helper.passwordAutofillIds

if (helper.usernameAutofillIds.size == 1) requiredIds += helper.usernameAutofillIds[0]
else optionalIds += helper.usernameAutofillIds

Expand All @@ -85,13 +99,13 @@ object AutofillHelper {

// if there are only username views but no password views, then delay the save on supported devices
if(helper.usernameAutofillIds.isNotEmpty() && helper.passwordAutofillIds.isEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.d(NCPAutofillService.TAG, "Delaying save because only username views are detected")
return Pair(
builder.apply {
setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE or SaveInfo.FLAG_DELAY_SAVE)
if (optionalIds.isNotEmpty()) setOptionalIds(optionalIds.toTypedArray())
}.build(),
Bundle().apply {
putCharSequence(USERNAME, helper.usernameAutofillContent.firstOrNull() ?: "")
putCharSequence(SEARCH_HINT, searchHint)
}
)
} else if (helper.passwordAutofillIds.isNotEmpty()) {
Expand All @@ -111,12 +125,12 @@ object AutofillHelper {
@RequiresApi(Build.VERSION_CODES.R)
private fun buildInlineDataset(
context: Context,
password: PasswordAutofillData?,
helper: AssistStructureParser,
inlinePresentationSpec: InlinePresentationSpec,
password: PasswordAutofillData?,
intent: IntentSender? = null,
needsAppLock: Boolean = false,
datasetIdx: Int
intentIdx: Int
): Dataset {
// build redacted dataset when app lock is needed
return if (needsAppLock && password?.id != null) {
Expand All @@ -125,40 +139,44 @@ object AutofillHelper {
addInlineAutofillValue(
context,
autofillId,
inlinePresentationSpec,
password.label,
null,
inlinePresentationSpec
null
)
}
helper.passwordAutofillIds.forEach { autofillId ->
addInlineAutofillValue(
context,
autofillId,
inlinePresentationSpec,
password.label,
null,
inlinePresentationSpec
null
)
}
setAuthentication(buildIntent(context, 1005+datasetIdx, AutofillData.FromId(id=password.id, structure=helper.structure)))
setAuthentication(buildIntent(
context,
1005+intentIdx,
AutofillData.FromId(id=password.id, structures=helper.structures)
))
}.build()
} else {
Dataset.Builder().apply {
helper.usernameAutofillIds.forEach { autofillId ->
addInlineAutofillValue(
context,
autofillId,
inlinePresentationSpec,
password?.label,
password?.username,
inlinePresentationSpec
password?.username
)
}
helper.passwordAutofillIds.forEach { autofillId ->
addInlineAutofillValue(
context,
autofillId,
inlinePresentationSpec,
password?.label,
password?.password,
inlinePresentationSpec
password?.password
)
}
intent?.let { setAuthentication(it) }
Expand All @@ -168,11 +186,11 @@ object AutofillHelper {

private fun buildPresentationDataset(
context: Context,
password: PasswordAutofillData?,
helper: AssistStructureParser,
password: PasswordAutofillData?,
intent: IntentSender? = null,
needsAppLock: Boolean = false,
datasetIdx: Int
intentIdx: Int
): Dataset {
// build redacted dataset when app lock is needed
return if (needsAppLock && password?.id != null) {
Expand All @@ -183,7 +201,11 @@ object AutofillHelper {
helper.passwordAutofillIds.forEach { autofillId ->
addAutofillValue(context, autofillId, password.label, null)
}
setAuthentication(buildIntent(context, 1005+datasetIdx, AutofillData.FromId(id=password.id, structure=helper.structure)))
setAuthentication(buildIntent(
context,
1005+intentIdx,
AutofillData.FromId(id=password.id, structures=helper.structures)
))
}.build()
} else {
Dataset.Builder().apply {
Expand Down Expand Up @@ -242,9 +264,9 @@ object AutofillHelper {
private fun Dataset.Builder.addInlineAutofillValue(
context: Context,
autofillId: AutofillId,
inlinePresentationSpec: InlinePresentationSpec,
label: String?,
value: String?,
inlinePresentationSpec: InlinePresentationSpec,
) {
val autofillLabel = label ?: context.getString(R.string.app_name)

Expand Down Expand Up @@ -319,5 +341,5 @@ object AutofillHelper {
}

private const val AUTOFILL_INTENT_ID = "com.hegocre.nextcloudpasswords.intents.autofill"
const val USERNAME = "username"
const val SEARCH_HINT = "search_hint"
}
Loading