From 311d3dd635ed14854e79c2b6ab8d5bced774efee Mon Sep 17 00:00:00 2001
From: Matt Portune <59324545+mportune-bw@users.noreply.github.com>
Date: Tue, 10 Nov 2020 17:24:24 -0500
Subject: [PATCH] Android 11 inline autofill (#1145)
* Inline autofill support for Android 11 - initial commit
* null check intent before getting bool extra
* Updated xamarin androidx autofill
* fixed broken overlay fallback
* fixed filename
* auto-compat-check cleanup
* simplification
---
src/Android/Android.csproj | 3 +-
src/Android/Autofill/AutofillHelpers.cs | 184 ++++++++++++++++--
src/Android/Autofill/AutofillService.cs | 2 +-
src/Android/Autofill/FilledItem.cs | 2 +-
src/Android/Properties/AndroidManifest.xml | 2 +-
src/Android/Resources/xml/autofillservice.xml | 3 +-
src/Android/Services/DeviceActionService.cs | 4 +-
...del.cs => AutofillServicePageViewModel.cs} | 0
8 files changed, 177 insertions(+), 23 deletions(-)
rename src/App/Pages/Settings/{AutofillServiceServicePageViewModel.cs => AutofillServicePageViewModel.cs} (100%)
diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index ae832cfb1..076e761fa 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -15,7 +15,7 @@
Properties\AndroidManifest.xml
Resources
Assets
- v10.0
+ v11.0
Xamarin.Android.Net.AndroidClientHandler
@@ -79,6 +79,7 @@
1.8.6.7
+
1.5.3.2
diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs
index cb9008b4c..742fd302b 100644
--- a/src/Android/Autofill/AutofillHelpers.cs
+++ b/src/Android/Autofill/AutofillHelpers.cs
@@ -1,14 +1,24 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Android.Content;
using Android.Service.Autofill;
using Android.Widget;
using System.Linq;
using Android.App;
using System.Threading.Tasks;
+using Android.App.Slices;
+using Android.Graphics;
+using Android.Graphics.Drawables;
+using Android.OS;
+using Android.Runtime;
+using Android.Widget.Inline;
using Bit.App.Resources;
using Bit.Core.Enums;
using Android.Views.Autofill;
+using AndroidX.AutoFill.Inline;
+using AndroidX.AutoFill.Inline.V1;
using Bit.Core.Abstractions;
+using SaveFlags = Android.Service.Autofill.SaveFlags;
namespace Bit.Droid.Autofill
{
@@ -132,14 +142,49 @@ namespace Bit.Droid.Autofill
return new List();
}
- public static FillResponse BuildFillResponse(Parser parser, List items, bool locked)
+ public static FillResponse BuildFillResponse(Parser parser, List items, bool locked,
+ FillRequest fillRequest = null)
{
+ // Acquire inline presentation specs on Android 11+
+ IList inlinePresentationSpecs = null;
+ var inlinePresentationSpecsCount = 0;
+ var inlineMaxSuggestedCount = 0;
+ if (fillRequest != null && (int)Build.VERSION.SdkInt >= 30)
+ {
+ var inlineSuggestionsRequest = fillRequest.InlineSuggestionsRequest;
+ inlineMaxSuggestedCount = inlineSuggestionsRequest?.MaxSuggestionCount ?? 0;
+ inlinePresentationSpecs = inlineSuggestionsRequest?.InlinePresentationSpecs;
+ inlinePresentationSpecsCount = inlinePresentationSpecs?.Count ?? 0;
+ }
+
+ // Build response
var responseBuilder = new FillResponse.Builder();
if (items != null && items.Count > 0)
{
- foreach (var item in items)
+ var maxItems = items.Count;
+ if (inlineMaxSuggestedCount > 0)
{
- var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, item);
+ // -1 to adjust for 'open vault' option
+ maxItems = Math.Min(maxItems, inlineMaxSuggestedCount - 1);
+ }
+ for (int i = 0; i < maxItems; i++)
+ {
+ InlinePresentationSpec inlinePresentationSpec = null;
+ if (inlinePresentationSpecs != null)
+ {
+ if (i < inlinePresentationSpecsCount)
+ {
+ inlinePresentationSpec = inlinePresentationSpecs[i];
+ }
+ else
+ {
+ // If the max suggestion count is larger than the number of specs in the list, then
+ // the last spec is used for the remainder of the suggestions
+ inlinePresentationSpec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1];
+ }
+ }
+ var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, items[i],
+ inlinePresentationSpec);
if (dataset != null)
{
responseBuilder.AddDataset(dataset);
@@ -147,16 +192,34 @@ namespace Bit.Droid.Autofill
}
}
responseBuilder.AddDataset(BuildVaultDataset(parser.ApplicationContext, parser.FieldCollection,
- parser.Uri, locked));
- AddSaveInfo(parser, responseBuilder, parser.FieldCollection);
+ parser.Uri, locked, inlinePresentationSpecs));
+ AddSaveInfo(parser, fillRequest, responseBuilder, parser.FieldCollection);
responseBuilder.SetIgnoredIds(parser.FieldCollection.IgnoreAutofillIds.ToArray());
return responseBuilder.Build();
}
- public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem)
+ public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem,
+ InlinePresentationSpec inlinePresentationSpec = null)
{
- var datasetBuilder = new Dataset.Builder(
- BuildListView(filledItem.Name, filledItem.Subtitle, filledItem.Icon, context));
+ var overlayPresentation = BuildOverlayPresentation(
+ filledItem.Name,
+ filledItem.Subtitle,
+ filledItem.Icon,
+ context);
+
+ var inlinePresentation = BuildInlinePresentation(
+ inlinePresentationSpec,
+ filledItem.Name,
+ filledItem.Subtitle,
+ filledItem.Icon,
+ null,
+ context);
+
+ var datasetBuilder = new Dataset.Builder(overlayPresentation);
+ if (inlinePresentation != null)
+ {
+ datasetBuilder.SetInlinePresentation(inlinePresentation);
+ }
if (filledItem.ApplyToFields(fields, datasetBuilder))
{
return datasetBuilder.Build();
@@ -164,7 +227,8 @@ namespace Bit.Droid.Autofill
return null;
}
- public static Dataset BuildVaultDataset(Context context, FieldCollection fields, string uri, bool locked)
+ public static Dataset BuildVaultDataset(Context context, FieldCollection fields, string uri, bool locked,
+ IList inlinePresentationSpecs = null)
{
var intent = new Intent(context, typeof(MainActivity));
intent.PutExtra("autofillFramework", true);
@@ -188,14 +252,26 @@ namespace Bit.Droid.Autofill
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
PendingIntentFlags.CancelCurrent);
- var view = BuildListView(
+ var overlayPresentation = BuildOverlayPresentation(
AppResources.AutofillWithBitwarden,
locked ? AppResources.VaultIsLocked : AppResources.GoToMyVault,
Resource.Drawable.icon,
context);
- var datasetBuilder = new Dataset.Builder(view);
- datasetBuilder.SetAuthentication(pendingIntent.IntentSender);
+ var inlinePresentation = BuildInlinePresentation(
+ inlinePresentationSpecs?.Last(),
+ AppResources.Bitwarden,
+ locked ? AppResources.VaultIsLocked : AppResources.MyVault,
+ Resource.Drawable.icon,
+ pendingIntent,
+ context);
+
+ var datasetBuilder = new Dataset.Builder(overlayPresentation);
+ if (inlinePresentation != null)
+ {
+ datasetBuilder.SetInlinePresentation(inlinePresentation);
+ }
+ datasetBuilder.SetAuthentication(pendingIntent?.IntentSender);
// Dataset must have a value set. We will reset this in the main activity when the real item is chosen.
foreach (var autofillId in fields.AutofillIds)
@@ -205,7 +281,7 @@ namespace Bit.Droid.Autofill
return datasetBuilder.Build();
}
- public static RemoteViews BuildListView(string text, string subtext, int iconId, Context context)
+ public static RemoteViews BuildOverlayPresentation(string text, string subtext, int iconId, Context context)
{
var packageName = context.PackageName;
var view = new RemoteViews(packageName, Resource.Layout.autofill_listitem);
@@ -215,11 +291,87 @@ namespace Bit.Droid.Autofill
return view;
}
- public static void AddSaveInfo(Parser parser, FillResponse.Builder responseBuilder, FieldCollection fields)
+ 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;
+ }
+ if (pendingIntent == null)
+ {
+ // InlinePresentation requires nonNull pending intent (even though we only utilize one for the
+ // "my vault" presentation) so we're including an empty one here
+ 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 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 = Icon.CreateWithResource(context, iconId);
+ if (icon != null)
+ {
+ if (iconId == Resource.Drawable.icon)
+ {
+ // Don't tint our logo
+ icon.SetTintBlendMode(BlendMode.Dst);
+ }
+ contentBuilder.SetStartIcon(icon);
+ }
+ }
+ return contentBuilder.Build().JavaCast()?.Slice;
+ }
+
+ public static void AddSaveInfo(Parser parser, FillRequest fillRequest, FillResponse.Builder responseBuilder,
+ FieldCollection fields)
{
// Docs state that password fields cannot be reliably saved in Compat mode since they will show as
// masked values.
- var compatBrowser = CompatBrowsers.Contains(parser.PackageName);
+ bool? compatRequest = null;
+ if (Build.VERSION.SdkInt >= BuildVersionCodes.Q && fillRequest != null)
+ {
+ // Attempt to automatically establish compat request mode on Android 10+
+ compatRequest = (fillRequest.Flags | FillRequest.FlagCompatibilityModeRequest) == fillRequest.Flags;
+ }
+ var compatBrowser = compatRequest ?? CompatBrowsers.Contains(parser.PackageName);
if (compatBrowser && fields.SaveType == SaveDataType.Password)
{
return;
diff --git a/src/Android/Autofill/AutofillService.cs b/src/Android/Autofill/AutofillService.cs
index cbbcb2642..4c36fb223 100644
--- a/src/Android/Autofill/AutofillService.cs
+++ b/src/Android/Autofill/AutofillService.cs
@@ -64,7 +64,7 @@ namespace Bit.Droid.Autofill
}
// build response
- var response = AutofillHelpers.BuildFillResponse(parser, items, locked);
+ var response = AutofillHelpers.BuildFillResponse(parser, items, locked, request);
callback.OnSuccess(response);
}
diff --git a/src/Android/Autofill/FilledItem.cs b/src/Android/Autofill/FilledItem.cs
index d4414fa65..3964f37bd 100644
--- a/src/Android/Autofill/FilledItem.cs
+++ b/src/Android/Autofill/FilledItem.cs
@@ -221,4 +221,4 @@ namespace Bit.Droid.Autofill
return null;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Android/Properties/AndroidManifest.xml b/src/Android/Properties/AndroidManifest.xml
index 2136e1077..f062abfe0 100644
--- a/src/Android/Properties/AndroidManifest.xml
+++ b/src/Android/Properties/AndroidManifest.xml
@@ -7,7 +7,7 @@
android:installLocation="internalOnly"
package="com.x8bit.bitwarden">
-
+
diff --git a/src/Android/Resources/xml/autofillservice.xml b/src/Android/Resources/xml/autofillservice.xml
index 45ca538a7..b75f649a8 100644
--- a/src/Android/Resources/xml/autofillservice.xml
+++ b/src/Android/Resources/xml/autofillservice.xml
@@ -9,7 +9,8 @@
- ... to keep this list in sync with values in AccessibilityHelpers.SupportedBrowsers [Section A], too.
-->
-
+
diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs
index a1193f220..8e0a44555 100644
--- a/src/Android/Services/DeviceActionService.cs
+++ b/src/Android/Services/DeviceActionService.cs
@@ -525,7 +525,7 @@ namespace Bit.Droid.Services
{
return;
}
- if (activity.Intent.GetBooleanExtra("autofillFramework", false))
+ if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
{
if (cipher == null)
{
@@ -596,7 +596,7 @@ namespace Bit.Droid.Services
public void Background()
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
- if (activity.Intent.GetBooleanExtra("autofillFramework", false))
+ if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
{
activity.SetResult(Result.Canceled);
activity.Finish();
diff --git a/src/App/Pages/Settings/AutofillServiceServicePageViewModel.cs b/src/App/Pages/Settings/AutofillServicePageViewModel.cs
similarity index 100%
rename from src/App/Pages/Settings/AutofillServiceServicePageViewModel.cs
rename to src/App/Pages/Settings/AutofillServicePageViewModel.cs