This commit is contained in:
Philipp Crocoll
2021-11-10 13:02:35 +01:00
parent 39579b0183
commit 8af2f51eae
9 changed files with 210 additions and 49 deletions

View File

@@ -51,6 +51,7 @@
<!-- Preference settings -->
<string name="AutoReturnFromQuery_key">AutoReturnFromQuery_key</string>
<string name="NoDalVerification_key">NoDalVerification_key</string>
<string name="InlineSuggestions_key">InlineSuggestions_key</string>
<string name="algorithm_key">algorithm</string>
<string name="app_key">app</string>
<string name="app_timeout_key">app_timeout_key</string>

View File

@@ -317,6 +317,10 @@
<string name="RememberRecentFiles_summary">Remember recently opened databases and show them in the Open database screen.</string>
<string name="NoDalVerification_title">No DAL verification</string>
<string name="NoDalVerification_summary">Disables check if domain and app package match</string>
<string name="InlineSuggestions_title">Integrate with keyboard</string>
<string name="InlineSuggestions_summary">Shows the autofill suggestions as inline options in the keyboard (if supported by the input method)</string>
<string name="requires_android11">Requires Android 11 or later</string>
<string name="kp2a_findUrl">Find password</string>
<string name="excludeExpiredEntries">Exclude expired entries</string>

View File

@@ -38,7 +38,7 @@
<item name="android:textSize">12sp</item>
</style>
<style name="InfoHeader">
<item name="android:drawableBottom">@drawable/section_header</item>
<item name="android:drawableBottom">@drawable/section_header</item>
<item name="android:drawablePadding">2dp</item>
<item name="android:layout_marginLeft">0dip</item>
<item name="android:layout_marginRight">12dip</item>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<autofill-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:supportsInlineSuggestions="true"
>
<compatibility-package
android:name="com.android.chrome"

View File

@@ -404,11 +404,6 @@
</PreferenceScreen>
<Preference android:title="@string/AccServiceAutoFill_prefs" >
<intent android:action="android.intent.action.VIEW"
android:data="https://philippc.github.io/keepass2android/AccServiceAutoFill.html" />
</Preference>
<PreferenceScreen
android:key="@string/AutoFill_prefs_screen_key"
android:title="@string/AutoFill_prefs"
@@ -439,6 +434,14 @@
android:title="@string/NoDalVerification_title"
android:key="@string/NoDalVerification_key" />
<CheckBoxPreference
android:enabled="true"
android:persistent="true"
android:summary="@string/InlineSuggestions_summary"
android:defaultValue="true"
android:title="@string/InlineSuggestions_title"
android:key="@string/InlineSuggestions_key" />
<keepass2android.AutofillDisabledQueriesPreference
android:title="@string/AutofillDisabledQueriesPreference_title"
android:summary="@string/AutofillDisabledQueriesPreference_summary"

View File

@@ -1,9 +1,16 @@
using System;
using System.Collections.Generic;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Service.Autofill;
using Android.Util;
using Android.Views;
using Android.Widget;
using Android.Widget.Inline;
using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1;
using FilledAutofillFieldCollection = keepass2android.services.AutofillBase.model.FilledAutofillFieldCollection;
namespace keepass2android.services.AutofillBase
@@ -13,43 +20,127 @@ namespace keepass2android.services.AutofillBase
/// </summary>
public class AutofillHelper
{
/// <summary>
/// Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the
/// client View.
/// </summary>
/// <returns>The dataset.</returns>
/// <param name="context">Context.</param>
/// <param name="autofillFields">Autofill fields.</param>
/// <param name="filledAutofillFieldCollection">Filled autofill field collection.</param>
public static Dataset NewDataset(Context context,
AutofillFieldMetadataCollection autofillFields, FilledAutofillFieldCollection filledAutofillFieldCollection, IAutofillIntentBuilder intentBuilder)
{
var datasetName = filledAutofillFieldCollection.DatasetName ?? "[noname]";
var datasetBuilder = new Dataset.Builder(NewRemoteViews(context.PackageName, datasetName, intentBuilder.AppIconResource));
public static InlinePresentation BuildInlinePresentation(InlinePresentationSpec inlinePresentationSpec,
string text, string subtext, int iconId, PendingIntent pendingIntent, Context context)
{
if ((int)Build.VERSION.SdkInt < 30 || inlinePresentationSpec == null)
{
return null;
}
//make sure we have a pendingIntent always not null
pendingIntent ??= PendingIntent.GetService(context, 0, new Intent(),
PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent);
var slice = CreateInlinePresentationSlice(
inlinePresentationSpec,
text,
subtext,
iconId,
"Autofill option",
pendingIntent,
context);
if (slice != null)
{
return new InlinePresentation(slice, inlinePresentationSpec, false);
}
return null;
}
private static Android.App.Slices.Slice CreateInlinePresentationSlice(
InlinePresentationSpec inlinePresentationSpec,
string text,
string subtext,
int iconId,
string contentDescription,
PendingIntent pendingIntent,
Context context)
{
var imeStyle = inlinePresentationSpec.Style;
if (!UiVersions.GetVersions(imeStyle).Contains(UiVersions.InlineUiVersion1))
{
return null;
}
var contentBuilder = InlineSuggestionUi.NewContentBuilder(pendingIntent)
.SetContentDescription(contentDescription);
if (!string.IsNullOrWhiteSpace(text))
{
contentBuilder.SetTitle(text);
}
if (!string.IsNullOrWhiteSpace(subtext))
{
contentBuilder.SetSubtitle(subtext);
}
if (iconId > 0)
{
var icon = Android.Graphics.Drawables.Icon.CreateWithResource(context, iconId);
if (icon != null)
{
if (iconId == AppNames.LauncherIcon)
{
// Don't tint our logo
icon.SetTintBlendMode(Android.Graphics.BlendMode.Dst);
}
contentBuilder.SetStartIcon(icon);
}
}
return contentBuilder.Build().JavaCast<InlineSuggestionUi.Content>()?.Slice;
}
/// <summary>
/// Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the
/// client View.
/// </summary>
/// <returns>The dataset.</returns>
/// <param name="context">Context.</param>
/// <param name="autofillFields">Autofill fields.</param>
/// <param name="filledAutofillFieldCollection">Filled autofill field collection.</param>
public static Dataset NewDataset(Context context,
AutofillFieldMetadataCollection autofillFields,
FilledAutofillFieldCollection filledAutofillFieldCollection,
IAutofillIntentBuilder intentBuilder,
Android.Widget.Inline.InlinePresentationSpec inlinePresentationSpec)
{
var datasetName = filledAutofillFieldCollection.DatasetName ?? "[noname]";
var datasetBuilder = new Dataset.Builder(NewRemoteViews(context.PackageName, datasetName, intentBuilder.AppIconResource));
datasetBuilder.SetId(datasetName);
var setValueAtLeastOnce = filledAutofillFieldCollection.ApplyToFields(autofillFields, datasetBuilder);
var setValueAtLeastOnce = filledAutofillFieldCollection.ApplyToFields(autofillFields, datasetBuilder);
AddInlinePresentation(context, inlinePresentationSpec, datasetName, datasetBuilder, intentBuilder.AppIconResource);
if (setValueAtLeastOnce)
{
return datasetBuilder.Build();
}
if (setValueAtLeastOnce)
{
return datasetBuilder.Build();
}
else
{
Kp2aLog.Log("Failed to set at least one value. #fields="+autofillFields.GetAutofillIds().Length + " " + autofillFields.FocusedAutofillCanonicalHints);
Kp2aLog.Log("Failed to set at least one value. #fields=" + autofillFields.GetAutofillIds().Length + " " + autofillFields.FocusedAutofillCanonicalHints);
}
return null;
}
public static RemoteViews NewRemoteViews(string packageName, string remoteViewsText,int drawableId)
return null;
}
public static void AddInlinePresentation(Context context, InlinePresentationSpec inlinePresentationSpec, string datasetName, Dataset.Builder datasetBuilder, int iconId)
{
if (inlinePresentationSpec != null)
{
var inlinePresentation = BuildInlinePresentation(inlinePresentationSpec, datasetName, "", iconId, null, context);
datasetBuilder.SetInlinePresentation(inlinePresentation);
}
}
public static RemoteViews NewRemoteViews(string packageName, string remoteViewsText,int drawableId)
{
RemoteViews presentation = new RemoteViews(packageName, Resource.Layout.autofill_service_list_item);
presentation.SetTextViewText(Resource.Id.text, remoteViewsText);
presentation.SetImageViewResource(Resource.Id.icon, drawableId);
return presentation;
}
}
internal static InlinePresentationSpec ExtractSpec(IList<InlinePresentationSpec> inlinePresentationSpecs, int index)
{
return inlinePresentationSpecs == null ? null : inlinePresentationSpecs[Math.Min(index, inlinePresentationSpecs.Count - 1)];
}
}
}

View File

@@ -1,15 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Android.App;
using Android.App.Slices;
using Android.Content;
using Android.Content.PM;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Preferences;
using Android.Runtime;
using Android.Service.Autofill;
using Android.Util;
using Android.Views.Autofill;
using Android.Views.InputMethods;
using Android.Widget;
using Android.Widget.Inline;
using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1;
using Java.Util.Concurrent.Atomic;
using keepass2android.services.AutofillBase.model;
@@ -125,6 +133,15 @@ namespace keepass2android.services.AutofillBase
}
AutofillFieldMetadataCollection autofillFields = parser.AutofillFields;
InlineSuggestionsRequest inlineSuggestionsRequest = null;
IList<InlinePresentationSpec> inlinePresentationSpecs = null;
if (((int) Build.VERSION.SdkInt >= 30)
&& (PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(GetString(Resource.String.InlineSuggestions_key), true)))
{
inlineSuggestionsRequest = request.InlineSuggestionsRequest;
inlinePresentationSpecs = inlineSuggestionsRequest?.InlinePresentationSpecs;
}
var autofillIds = autofillFields.GetAutofillIds();
@@ -134,13 +151,27 @@ namespace keepass2android.services.AutofillBase
bool hasEntryDataset = false;
IList<Dataset> entryDatasets = new List<Dataset>();
if (query.IncompatiblePackageAndDomain == false)
{
Kp2aLog.Log("AF: (query.IncompatiblePackageAndDomain == false)");
//domain and package are compatible. Use Domain if available and package otherwise. Can fill without warning.
foreach (var entryDataset in BuildEntryDatasets(query.DomainOrPackage, query.WebDomain,
query.PackageName,
autofillIds, parser, DisplayWarning.None).Where(ds => ds != null)
entryDatasets = BuildEntryDatasets(query.DomainOrPackage, query.WebDomain,
query.PackageName,
autofillIds, parser, DisplayWarning.None,
inlinePresentationSpecs
).Where(ds => ds != null).ToList();
if (entryDatasets.Count > inlineSuggestionsRequest?.MaxSuggestionCount - 2 /*disable dataset and query*/)
{
//we have too many elements. disable inline suggestions
inlinePresentationSpecs = null;
entryDatasets = BuildEntryDatasets(query.DomainOrPackage, query.WebDomain,
query.PackageName,
autofillIds, parser, DisplayWarning.None,
null
).Where(ds => ds != null).ToList();
}
foreach (var entryDataset in entryDatasets
)
{
Kp2aLog.Log("AF: Got EntryDataset " + (entryDataset == null));
@@ -158,14 +189,16 @@ namespace keepass2android.services.AutofillBase
isManual, autofillIds, responseBuilder, !hasEntryDataset,
query.IncompatiblePackageAndDomain
? DisplayWarning.FillDomainInUntrustedApp
: DisplayWarning.None);
: DisplayWarning.None,
AutofillHelper.ExtractSpec(inlinePresentationSpecs, entryDatasets.Count));
else
AddQueryDataset(query.PackageNameWithPseudoSchema,
query.WebDomain, query.PackageName,
isManual, autofillIds, responseBuilder, !hasEntryDataset, DisplayWarning.None);
isManual, autofillIds, responseBuilder, !hasEntryDataset, DisplayWarning.None,
AutofillHelper.ExtractSpec(inlinePresentationSpecs, entryDatasets.Count));
}
AddDisableDataset(query.DomainOrPackage, autofillIds, responseBuilder, isManual);
AddDisableDataset(query.DomainOrPackage, autofillIds, responseBuilder, isManual, AutofillHelper.ExtractSpec(inlinePresentationSpecs, entryDatasets.Count));
if (PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean(GetString(Resource.String.OfferSaveCredentials_key), true))
@@ -192,20 +225,26 @@ namespace keepass2android.services.AutofillBase
Kp2aLog.Log("Ignoring onFillRequest as there is another request going on.");
}
}
private List<Dataset> BuildEntryDatasets(string query, string queryDomain, string queryPackage, AutofillId[] autofillIds, StructureParser parser,
DisplayWarning warning)
DisplayWarning warning, IList<InlinePresentationSpec> inlinePresentationSpecs)
{
List<Dataset> result = new List<Dataset>();
Kp2aLog.Log("AF: BuildEntryDatasets");
var suggestedEntries = GetSuggestedEntries(query).ToDictionary(e => e.DatasetName, e => e);
Kp2aLog.Log("AF: BuildEntryDatasets found " + suggestedEntries.Count + " entries");
int count = 0;
foreach (var filledAutofillFieldCollection in suggestedEntries.Values)
{
if (filledAutofillFieldCollection == null)
continue;
var inlinePresentationSpec = AutofillHelper.ExtractSpec(inlinePresentationSpecs, count);
if (warning == DisplayWarning.None)
{
@@ -214,12 +253,13 @@ namespace keepass2android.services.AutofillBase
Kp2aLog.Log("AF: Add dataset");
result.Add(AutofillHelper.NewDataset(this, parser.AutofillFields, partitionData, IntentBuilder));
result.Add(AutofillHelper.NewDataset(this, parser.AutofillFields, partitionData, IntentBuilder,
inlinePresentationSpec));
}
else
{
//return an "auth" dataset (actually for just warning the user in case domain/package dont match)
var sender =
IntentSender sender =
IntentBuilder.GetAuthIntentSenderForWarning(this, query, queryDomain, queryPackage, warning);
var datasetName = filledAutofillFieldCollection.DatasetName;
if (datasetName == null)
@@ -233,6 +273,9 @@ namespace keepass2android.services.AutofillBase
var datasetBuilder = new Dataset.Builder(presentation);
datasetBuilder.SetAuthentication(sender);
AutofillHelper.AddInlinePresentation(this, inlinePresentationSpec, datasetName, datasetBuilder, AppNames.LauncherIcon);
//need to add placeholders so we can directly fill after ChooseActivity
foreach (var autofillId in autofillIds)
{
@@ -241,6 +284,7 @@ namespace keepass2android.services.AutofillBase
Kp2aLog.Log("AF: Add auth dataset");
result.Add(datasetBuilder.Build());
}
count++;
}
return result;
@@ -257,11 +301,12 @@ namespace keepass2android.services.AutofillBase
}
private void AddQueryDataset(string query, string queryDomain, string queryPackage, bool isManual, AutofillId[] autofillIds, FillResponse.Builder responseBuilder, bool autoReturnFromQuery, DisplayWarning warning)
private void AddQueryDataset(string query, string queryDomain, string queryPackage, bool isManual, AutofillId[] autofillIds, FillResponse.Builder responseBuilder, bool autoReturnFromQuery, DisplayWarning warning, InlinePresentationSpec inlinePresentationSpec)
{
var sender = IntentBuilder.GetAuthIntentSenderForResponse(this, query, queryDomain, queryPackage, isManual, autoReturnFromQuery, warning);
RemoteViews presentation = AutofillHelper.NewRemoteViews(PackageName,
GetString(Resource.String.autofill_sign_in_prompt), AppNames.LauncherIcon);
string text = GetString(Resource.String.autofill_sign_in_prompt);
RemoteViews presentation = AutofillHelper.NewRemoteViews(base.PackageName,
text, AppNames.LauncherIcon);
var datasetBuilder = new Dataset.Builder(presentation);
datasetBuilder.SetAuthentication(sender);
@@ -271,6 +316,9 @@ namespace keepass2android.services.AutofillBase
datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));
}
AutofillHelper.AddInlinePresentation(this, inlinePresentationSpec, text, datasetBuilder, AppNames.LauncherIcon);
responseBuilder.AddDataset(datasetBuilder.Build());
}
public static string GetDisplayNameForQuery(string str, Context Context)
@@ -303,20 +351,23 @@ namespace keepass2android.services.AutofillBase
return displayName;
}
private void AddDisableDataset(string query, AutofillId[] autofillIds, FillResponse.Builder responseBuilder, bool isManual)
private void AddDisableDataset(string query, AutofillId[] autofillIds, FillResponse.Builder responseBuilder, bool isManual, InlinePresentationSpec inlinePresentationSpec)
{
bool isQueryDisabled = IsQueryDisabled(query);
if (isQueryDisabled && !isManual)
return;
bool isForDisable = !isQueryDisabled;
var sender = IntentBuilder.GetDisableIntentSenderForResponse(this, query, isManual, isForDisable);
RemoteViews presentation = AutofillHelper.NewRemoteViews(PackageName,
GetString(isForDisable ? Resource.String.autofill_disable : Resource.String.autofill_enable_for, new Java.Lang.Object[] { GetDisplayNameForQuery(query, this)}), Resource.Drawable.ic_menu_close_grey);
string text = GetString(isForDisable ? Resource.String.autofill_disable : Resource.String.autofill_enable_for, new Java.Lang.Object[] { GetDisplayNameForQuery(query, this) });
RemoteViews presentation = AutofillHelper.NewRemoteViews(base.PackageName,
text, Resource.Drawable.ic_menu_close_grey);
var datasetBuilder = new Dataset.Builder(presentation);
datasetBuilder.SetAuthentication(sender);
AutofillHelper.AddInlinePresentation(this, inlinePresentationSpec, text, datasetBuilder, Resource.Drawable.ic_menu_close_grey);
foreach (var autofillId in autofillIds)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));

View File

@@ -183,7 +183,7 @@ namespace keepass2android.services.AutofillBase
ReplyIntent = new Intent();
SetDatasetIntent(AutofillHelper.NewDataset(this, autofillFields, partitionData, IntentBuilder));
SetDatasetIntent(AutofillHelper.NewDataset(this, autofillFields, partitionData, IntentBuilder, null /*TODO can we get the inlinePresentationSpec here?*/));
SetResult(Result.Ok, ReplyIntent);
}

View File

@@ -320,6 +320,8 @@ namespace keepass2android
var autofillPref = FindPreference(GetString(Resource.String.AutoFill_prefs_key));
var autofillDisabledPref = FindPreference(GetString(Resource.String.AutofillDisabledQueriesPreference_key));
var autofillSavePref = FindPreference(GetString(Resource.String.OfferSaveCredentials_key));
var autofillInlineSuggestions = FindPreference(GetString(Resource.String.InlineSuggestions_key));
var autofillNoDalVerification = FindPreference(GetString(Resource.String.NoDalVerification_key));
if (autofillPref == null)
return;
if ((Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.O) ||
@@ -337,17 +339,25 @@ namespace keepass2android
{
autofillDisabledPref.Enabled = true;
autofillSavePref.Enabled = true;
autofillNoDalVerification.Enabled = true;
autofillInlineSuggestions.Enabled = true;
autofillPref.Summary = Activity.GetString(Resource.String.plugin_enabled);
autofillPref.Intent = new Intent(Intent.ActionView);
autofillPref.Intent.SetData(Android.Net.Uri.Parse("https://philippc.github.io/keepass2android/OreoAutoFill.html"));
}
else
{
autofillNoDalVerification.Enabled = false;
autofillDisabledPref.Enabled = false;
autofillSavePref.Enabled = false;
autofillInlineSuggestions.Enabled = false;
autofillPref.Summary = Activity.GetString(Resource.String.not_enabled);
}
if ((int)Android.OS.Build.VERSION.SdkInt < 30)
{
autofillInlineSuggestions.Summary = Activity.GetString(Resource.String.requires_android11);
}
}
}