mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
First inline code
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user