First inline code

This commit is contained in:
J-Jamet
2020-12-16 15:25:04 +01:00
parent de980d030a
commit fcb1b5ae6b
8 changed files with 115 additions and 44 deletions

View File

@@ -110,6 +110,8 @@ dependencies {
// Database // Database
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
// Autofill
implementation "androidx.autofill:autofill:1.1.0-rc01"
// Crypto // Crypto
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01' implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
// Time // Time

View File

@@ -40,7 +40,6 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.UriUtil
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : AppCompatActivity() { class AutofillLauncherActivity : AppCompatActivity() {
@@ -105,7 +104,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
searchInfo, searchInfo,
{ items -> { items ->
// Items found // Items found
AutofillHelper.buildResponse(this, items) AutofillHelper.buildResponseAndSetResult(this, items)
finish() finish()
}, },
{ {

View File

@@ -361,7 +361,7 @@ class EntryEditActivity : LockingActivity(),
// Build Autofill response with the entry selected // Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mDatabase?.let { database -> mDatabase?.let { database ->
AutofillHelper.buildResponse(this@EntryEditActivity, AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
entry.getEntryInfo(database)) entry.getEntryInfo(database))
} }
} }

View File

@@ -660,7 +660,7 @@ class GroupActivity : LockingActivity(),
// Build response with the entry selected // Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
mDatabase?.let { database -> mDatabase?.let { database ->
AutofillHelper.buildResponse(this, AutofillHelper.buildResponseAndSetResult(this,
entry.getEntryInfo(database)) entry.getEntryInfo(database))
} }
} }
@@ -1427,7 +1427,7 @@ class GroupActivity : LockingActivity(),
searchInfo, searchInfo,
{ items -> { items ->
// Response is build // Response is build
AutofillHelper.buildResponse(activity, items) AutofillHelper.buildResponseAndSetResult(activity, items)
onValidateSpecialMode() onValidateSpecialMode()
}, },
{ {

View File

@@ -19,18 +19,26 @@
*/ */
package com.kunzisoft.keepass.autofill package com.kunzisoft.keepass.autofill
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.PendingIntent
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.service.autofill.Dataset import android.service.autofill.Dataset
import android.service.autofill.FillResponse import android.service.autofill.FillResponse
import android.service.autofill.InlinePresentation
import android.util.Log import android.util.Log
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
@@ -68,26 +76,10 @@ object AutofillHelper {
return "" return ""
} }
internal fun addHeader(responseBuilder: FillResponse.Builder, private fun buildDataset(context: Context,
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,
entryInfo: EntryInfo, entryInfo: EntryInfo,
struct: StructureParser.Result): Dataset? { struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? {
val title = makeEntryTitle(entryInfo) val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, title, entryInfo.icon) val views = newRemoteViews(context, title, entryInfo.icon)
val builder = Dataset.Builder(views) val builder = Dataset.Builder(views)
@@ -100,6 +92,12 @@ object AutofillHelper {
builder.setValue(password, AutofillValue.forText(entryInfo.password)) builder.setValue(password, AutofillValue.forText(entryInfo.password))
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
return try { return try {
builder.build() builder.build()
} catch (e: IllegalArgumentException) { } 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<EntryInfo>,
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 * Build the Autofill response for one entry
*/ */
fun buildResponse(activity: Activity, entryInfo: EntryInfo) { fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) {
buildResponse(activity, ArrayList<EntryInfo>().apply { add(entryInfo) }) buildResponseAndSetResult(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
} }
/** /**
* Build the Autofill response for many entry * Build the Autofill response for many entry
*/ */
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) { fun buildResponseAndSetResult(activity: Activity, entriesInfo: List<EntryInfo>) {
if (entriesInfo.isEmpty()) { if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED) activity.setResult(Activity.RESULT_CANCELED)
} else { } else {
@@ -128,15 +190,14 @@ object AutofillHelper {
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure -> activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
StructureParser(structure).parse()?.let { result -> StructureParser(structure).parse()?.let { result ->
// New Response // New Response
val responseBuilder = FillResponse.Builder() // TODO inline suggestion
entriesInfo.forEach { val inlineSuggestionsRequest: InlineSuggestionsRequest? = null
responseBuilder.addDataset(buildDataset(activity, it, result)) val response = buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
}
val mReplyIntent = Intent() val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.") Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra( mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT, AutofillManager.EXTRA_AUTHENTICATION_RESULT,
responseBuilder.build()) response)
setResultOk = true setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent) activity.setResult(Activity.RESULT_OK, mReplyIntent)
} }

View File

@@ -24,6 +24,7 @@ import android.os.CancellationSignal
import android.service.autofill.* import android.service.autofill.*
import android.util.Log import android.util.Log
import android.view.autofill.AutofillId import android.view.autofill.AutofillId
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R 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.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() { class KeeAutofillService : AutofillService() {
@@ -75,7 +76,15 @@ class KeeAutofillService : AutofillService() {
} }
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain -> SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
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, private fun launchSelection(searchInfo: SearchInfo,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(), Database.getInstance(),
searchInfo, searchInfo,
{ items -> { items ->
val responseBuilder = FillResponse.Builder() callback.onSuccess(
AutofillHelper.addHeader(responseBuilder, packageName, AutofillHelper.buildResponse(this,
parseResult.webDomain, parseResult.applicationId) items, parseResult, inlineSuggestionsRequest)
items.forEach { )
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
}
callback.onSuccess(responseBuilder.build())
}, },
{ {
// Show UI if no search result // Show UI if no search result

View File

@@ -33,7 +33,7 @@ import java.util.*
* Parse AssistStructure and guess username and password fields. * Parse AssistStructure and guess username and password fields.
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @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 result: Result? = null
private var usernameNeeded = true private var usernameNeeded = true
@@ -274,7 +274,7 @@ internal class StructureParser(private val structure: AssistStructure) {
} }
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
internal class Result { class Result {
var applicationId: String? = null var applicationId: String? = null
var webDomain: String? = null var webDomain: String? = null

View File

@@ -23,7 +23,9 @@ Settings Activity. This is pointed to in the service's meta-data in the applicat
<autofill-service xmlns:android="http://schemas.android.com/apk/res/android" <autofill-service xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:targetApi="p" tools:targetApi="p"
android:settingsActivity="com.kunzisoft.keepass.settings.AutofillSettingsActivity" > android:settingsActivity="com.kunzisoft.keepass.settings.AutofillSettingsActivity"
android:supportsInlineSuggestions="true"
tools:ignore="UnusedAttribute">
<compatibility-package <compatibility-package
android:name="com.amazon.cloud9" android:name="com.amazon.cloud9"
android:maxLongVersionCode="10000000000"/> android:maxLongVersionCode="10000000000"/>