From fcb1b5ae6b9fdd4b211a8e10e72d74163ba3f677 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 16 Dec 2020 15:25:04 +0100 Subject: [PATCH] First inline code --- app/build.gradle | 2 + .../activities/AutofillLauncherActivity.kt | 3 +- .../keepass/activities/EntryEditActivity.kt | 2 +- .../keepass/activities/GroupActivity.kt | 4 +- .../keepass/autofill/AutofillHelper.kt | 115 ++++++++++++++---- .../keepass/autofill/KeeAutofillService.kt | 25 ++-- .../keepass/autofill/StructureParser.kt | 4 +- app/src/main/res/xml/dataset_service.xml | 4 +- 8 files changed, 115 insertions(+), 44 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cf10312de..67ff86a2a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -110,6 +110,8 @@ dependencies { // Database implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" + // Autofill + implementation "androidx.autofill:autofill:1.1.0-rc01" // Crypto implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01' // Time diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt index bfa581810..c572dbea9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt @@ -40,7 +40,6 @@ import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.LOCK_ACTION -import com.kunzisoft.keepass.utils.UriUtil @RequiresApi(api = Build.VERSION_CODES.O) class AutofillLauncherActivity : AppCompatActivity() { @@ -105,7 +104,7 @@ class AutofillLauncherActivity : AppCompatActivity() { searchInfo, { items -> // Items found - AutofillHelper.buildResponse(this, items) + AutofillHelper.buildResponseAndSetResult(this, items) finish() }, { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index 5134f1ffb..4cc92c781 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -361,7 +361,7 @@ class EntryEditActivity : LockingActivity(), // Build Autofill response with the entry selected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mDatabase?.let { database -> - AutofillHelper.buildResponse(this@EntryEditActivity, + AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity, entry.getEntryInfo(database)) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index d47e65241..4e4c48cb4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -660,7 +660,7 @@ class GroupActivity : LockingActivity(), // Build response with the entry selected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) { mDatabase?.let { database -> - AutofillHelper.buildResponse(this, + AutofillHelper.buildResponseAndSetResult(this, entry.getEntryInfo(database)) } } @@ -1427,7 +1427,7 @@ class GroupActivity : LockingActivity(), searchInfo, { items -> // Response is build - AutofillHelper.buildResponse(activity, items) + AutofillHelper.buildResponseAndSetResult(activity, items) onValidateSpecialMode() }, { diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt index 5a1a64d1d..9a3601b37 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt @@ -19,18 +19,26 @@ */ package com.kunzisoft.keepass.autofill +import android.annotation.SuppressLint import android.app.Activity +import android.app.PendingIntent import android.app.assist.AssistStructure import android.content.Context import android.content.Intent +import android.graphics.BlendMode +import android.graphics.drawable.Icon import android.os.Build import android.service.autofill.Dataset import android.service.autofill.FillResponse +import android.service.autofill.InlinePresentation import android.util.Log import android.view.autofill.AutofillManager import android.view.autofill.AutofillValue +import android.view.inputmethod.InlineSuggestionsRequest import android.widget.RemoteViews import androidx.annotation.RequiresApi +import androidx.autofill.inline.UiVersions +import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.core.content.ContextCompat import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper @@ -68,26 +76,10 @@ object AutofillHelper { return "" } - internal fun addHeader(responseBuilder: FillResponse.Builder, - packageName: String, - webDomain: String?, - applicationId: String?) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - if (webDomain != null) { - responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply { - setTextViewText(R.id.autofill_web_domain_text, webDomain) - }) - } else if (applicationId != null) { - responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply { - setTextViewText(R.id.autofill_app_id_text, applicationId) - }) - } - } - } - - internal fun buildDataset(context: Context, + private fun buildDataset(context: Context, entryInfo: EntryInfo, - struct: StructureParser.Result): Dataset? { + struct: StructureParser.Result, + inlinePresentation: InlinePresentation?): Dataset? { val title = makeEntryTitle(entryInfo) val views = newRemoteViews(context, title, entryInfo.icon) val builder = Dataset.Builder(views) @@ -100,6 +92,12 @@ object AutofillHelper { builder.setValue(password, AutofillValue.forText(entryInfo.password)) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + inlinePresentation?.let { + builder.setInlinePresentation(it) + } + } + return try { builder.build() } catch (e: IllegalArgumentException) { @@ -108,17 +106,81 @@ object AutofillHelper { } } + @SuppressLint("RestrictedApi") + private fun buildInlinePresentation(context: Context, + inlineSuggestionsRequest: InlineSuggestionsRequest, + positionItem: Int, + entryInfo: EntryInfo): InlinePresentation? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs + val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount + + if (positionItem <= maxSuggestion-1 + && inlinePresentationSpecs.size > positionItem) { + val inlinePresentationSpec = inlinePresentationSpecs[positionItem] + + // Make sure that the IME spec claims support for v1 UI template. + val imeStyle = inlinePresentationSpec.style + if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) + return null + + // Build the content for IME UI + val pendingIntent = PendingIntent.getActivity(context, 4596, Intent(), 0) + return InlinePresentation( + InlineSuggestionUi.newContentBuilder(pendingIntent).apply { + setContentDescription(context.getString(R.string.autofill_sign_in_prompt)) + setTitle(entryInfo.title) + setSubtitle(entryInfo.username) + setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply { + setTintBlendMode(BlendMode.DST) + }) + }.build().slice, inlinePresentationSpec, false) + } + } + return null + } + + fun buildResponse(context: Context, + entriesInfo: List, + parseResult: StructureParser.Result, + inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse { + val responseBuilder = FillResponse.Builder() + // Add Header + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val packageName = context.packageName + parseResult.webDomain?.let { webDomain -> + responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply { + setTextViewText(R.id.autofill_web_domain_text, webDomain) + }) + } ?: kotlin.run { + parseResult.applicationId?.let { applicationId -> + responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply { + setTextViewText(R.id.autofill_app_id_text, applicationId) + }) + } + } + } + // Add inline suggestion for new IME and dataset + entriesInfo.forEachIndexed { index, entryInfo -> + val inlinePresentation = inlineSuggestionsRequest?.let { + buildInlinePresentation(context, inlineSuggestionsRequest, index, entryInfo) + } + responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation)) + } + return responseBuilder.build() + } + /** * Build the Autofill response for one entry */ - fun buildResponse(activity: Activity, entryInfo: EntryInfo) { - buildResponse(activity, ArrayList().apply { add(entryInfo) }) + fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) { + buildResponseAndSetResult(activity, ArrayList().apply { add(entryInfo) }) } /** * Build the Autofill response for many entry */ - fun buildResponse(activity: Activity, entriesInfo: List) { + fun buildResponseAndSetResult(activity: Activity, entriesInfo: List) { if (entriesInfo.isEmpty()) { activity.setResult(Activity.RESULT_CANCELED) } else { @@ -128,15 +190,14 @@ object AutofillHelper { activity.intent?.getParcelableExtra(ASSIST_STRUCTURE)?.let { structure -> StructureParser(structure).parse()?.let { result -> // New Response - val responseBuilder = FillResponse.Builder() - entriesInfo.forEach { - responseBuilder.addDataset(buildDataset(activity, it, result)) - } + // TODO inline suggestion + val inlineSuggestionsRequest: InlineSuggestionsRequest? = null + val response = buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest) val mReplyIntent = Intent() Log.d(activity.javaClass.name, "Successed Autofill auth.") mReplyIntent.putExtra( AutofillManager.EXTRA_AUTHENTICATION_RESULT, - responseBuilder.build()) + response) setResultOk = true activity.setResult(Activity.RESULT_OK, mReplyIntent) } diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt index 70801c75c..15b91f640 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt @@ -24,6 +24,7 @@ import android.os.CancellationSignal import android.service.autofill.* import android.util.Log import android.view.autofill.AutofillId +import android.view.inputmethod.InlineSuggestionsRequest import android.widget.RemoteViews import androidx.annotation.RequiresApi import com.kunzisoft.keepass.R @@ -33,9 +34,9 @@ import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.utils.UriUtil import java.util.concurrent.atomic.AtomicBoolean + @RequiresApi(api = Build.VERSION_CODES.O) class KeeAutofillService : AutofillService() { @@ -75,7 +76,15 @@ class KeeAutofillService : AutofillService() { } SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain -> searchInfo.webDomain = webDomainWithoutSubDomain - launchSelection(searchInfo, parseResult, callback) + val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + request.inlineSuggestionsRequest + } else { + null + } + launchSelection(searchInfo, + parseResult, + inlineSuggestionsRequest, + callback) } } } @@ -84,18 +93,16 @@ class KeeAutofillService : AutofillService() { private fun launchSelection(searchInfo: SearchInfo, parseResult: StructureParser.Result, + inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) { SearchHelper.checkAutoSearchInfo(this, Database.getInstance(), searchInfo, { items -> - val responseBuilder = FillResponse.Builder() - AutofillHelper.addHeader(responseBuilder, packageName, - parseResult.webDomain, parseResult.applicationId) - items.forEach { - responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult)) - } - callback.onSuccess(responseBuilder.build()) + callback.onSuccess( + AutofillHelper.buildResponse(this, + items, parseResult, inlineSuggestionsRequest) + ) }, { // Show UI if no search result diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt index dd6ee4cb4..c93cb4a36 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt @@ -33,7 +33,7 @@ import java.util.* * Parse AssistStructure and guess username and password fields. */ @RequiresApi(api = Build.VERSION_CODES.O) -internal class StructureParser(private val structure: AssistStructure) { +class StructureParser(private val structure: AssistStructure) { private var result: Result? = null private var usernameNeeded = true @@ -274,7 +274,7 @@ internal class StructureParser(private val structure: AssistStructure) { } @RequiresApi(api = Build.VERSION_CODES.O) - internal class Result { + class Result { var applicationId: String? = null var webDomain: String? = null diff --git a/app/src/main/res/xml/dataset_service.xml b/app/src/main/res/xml/dataset_service.xml index ea2597ec7..6d595b7b5 100644 --- a/app/src/main/res/xml/dataset_service.xml +++ b/app/src/main/res/xml/dataset_service.xml @@ -23,7 +23,9 @@ Settings Activity. This is pointed to in the service's meta-data in the applicat + android:settingsActivity="com.kunzisoft.keepass.settings.AutofillSettingsActivity" + android:supportsInlineSuggestions="true" + tools:ignore="UnusedAttribute">