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
This commit is contained in:
Matt Portune 2020-11-10 17:24:24 -05:00 committed by GitHub
parent e80b3e4542
commit 311d3dd635
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 177 additions and 23 deletions

View file

@ -15,7 +15,7 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest> <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix> <MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix> <MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion> <TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType> <AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<NuGetPackageImportStamp> <NuGetPackageImportStamp>
</NuGetPackageImportStamp> </NuGetPackageImportStamp>
@ -79,6 +79,7 @@
<PackageReference Include="Portable.BouncyCastle"> <PackageReference Include="Portable.BouncyCastle">
<Version>1.8.6.7</Version> <Version>1.8.6.7</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.3-beta01" />
<PackageReference Include="Xamarin.Essentials"> <PackageReference Include="Xamarin.Essentials">
<Version>1.5.3.2</Version> <Version>1.5.3.2</Version>
</PackageReference> </PackageReference>

View file

@ -1,14 +1,24 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Android.Content; using Android.Content;
using Android.Service.Autofill; using Android.Service.Autofill;
using Android.Widget; using Android.Widget;
using System.Linq; using System.Linq;
using Android.App; using Android.App;
using System.Threading.Tasks; 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.App.Resources;
using Bit.Core.Enums; using Bit.Core.Enums;
using Android.Views.Autofill; using Android.Views.Autofill;
using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using SaveFlags = Android.Service.Autofill.SaveFlags;
namespace Bit.Droid.Autofill namespace Bit.Droid.Autofill
{ {
@ -132,14 +142,49 @@ namespace Bit.Droid.Autofill
return new List<FilledItem>(); return new List<FilledItem>();
} }
public static FillResponse BuildFillResponse(Parser parser, List<FilledItem> items, bool locked) public static FillResponse BuildFillResponse(Parser parser, List<FilledItem> items, bool locked,
FillRequest fillRequest = null)
{ {
// Acquire inline presentation specs on Android 11+
IList<InlinePresentationSpec> 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(); var responseBuilder = new FillResponse.Builder();
if (items != null && items.Count > 0) 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) if (dataset != null)
{ {
responseBuilder.AddDataset(dataset); responseBuilder.AddDataset(dataset);
@ -147,16 +192,34 @@ namespace Bit.Droid.Autofill
} }
} }
responseBuilder.AddDataset(BuildVaultDataset(parser.ApplicationContext, parser.FieldCollection, responseBuilder.AddDataset(BuildVaultDataset(parser.ApplicationContext, parser.FieldCollection,
parser.Uri, locked)); parser.Uri, locked, inlinePresentationSpecs));
AddSaveInfo(parser, responseBuilder, parser.FieldCollection); AddSaveInfo(parser, fillRequest, responseBuilder, parser.FieldCollection);
responseBuilder.SetIgnoredIds(parser.FieldCollection.IgnoreAutofillIds.ToArray()); responseBuilder.SetIgnoredIds(parser.FieldCollection.IgnoreAutofillIds.ToArray());
return responseBuilder.Build(); 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( var overlayPresentation = BuildOverlayPresentation(
BuildListView(filledItem.Name, filledItem.Subtitle, filledItem.Icon, context)); 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)) if (filledItem.ApplyToFields(fields, datasetBuilder))
{ {
return datasetBuilder.Build(); return datasetBuilder.Build();
@ -164,7 +227,8 @@ namespace Bit.Droid.Autofill
return null; 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<InlinePresentationSpec> inlinePresentationSpecs = null)
{ {
var intent = new Intent(context, typeof(MainActivity)); var intent = new Intent(context, typeof(MainActivity));
intent.PutExtra("autofillFramework", true); intent.PutExtra("autofillFramework", true);
@ -188,14 +252,26 @@ namespace Bit.Droid.Autofill
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
PendingIntentFlags.CancelCurrent); PendingIntentFlags.CancelCurrent);
var view = BuildListView( var overlayPresentation = BuildOverlayPresentation(
AppResources.AutofillWithBitwarden, AppResources.AutofillWithBitwarden,
locked ? AppResources.VaultIsLocked : AppResources.GoToMyVault, locked ? AppResources.VaultIsLocked : AppResources.GoToMyVault,
Resource.Drawable.icon, Resource.Drawable.icon,
context); context);
var datasetBuilder = new Dataset.Builder(view); var inlinePresentation = BuildInlinePresentation(
datasetBuilder.SetAuthentication(pendingIntent.IntentSender); 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. // 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) foreach (var autofillId in fields.AutofillIds)
@ -205,7 +281,7 @@ namespace Bit.Droid.Autofill
return datasetBuilder.Build(); 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 packageName = context.PackageName;
var view = new RemoteViews(packageName, Resource.Layout.autofill_listitem); var view = new RemoteViews(packageName, Resource.Layout.autofill_listitem);
@ -215,11 +291,87 @@ namespace Bit.Droid.Autofill
return view; 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<InlineSuggestionUi.Content>()?.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 // Docs state that password fields cannot be reliably saved in Compat mode since they will show as
// masked values. // 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) if (compatBrowser && fields.SaveType == SaveDataType.Password)
{ {
return; return;

View file

@ -64,7 +64,7 @@ namespace Bit.Droid.Autofill
} }
// build response // build response
var response = AutofillHelpers.BuildFillResponse(parser, items, locked); var response = AutofillHelpers.BuildFillResponse(parser, items, locked, request);
callback.OnSuccess(response); callback.OnSuccess(response);
} }

View file

@ -221,4 +221,4 @@ namespace Bit.Droid.Autofill
return null; return null;
} }
} }
} }

View file

@ -7,7 +7,7 @@
android:installLocation="internalOnly" android:installLocation="internalOnly"
package="com.x8bit.bitwarden"> package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />

View file

@ -9,7 +9,8 @@
- ... to keep this list in sync with values in AccessibilityHelpers.SupportedBrowsers [Section A], too. - ... to keep this list in sync with values in AccessibilityHelpers.SupportedBrowsers [Section A], too.
--> -->
<autofill-service xmlns:android="http://schemas.android.com/apk/res/android"> <autofill-service xmlns:android="http://schemas.android.com/apk/res/android"
android:supportsInlineSuggestions="true">
<compatibility-package <compatibility-package
android:name="com.amazon.cloud9" android:name="com.amazon.cloud9"
android:maxLongVersionCode="10000000000"/> android:maxLongVersionCode="10000000000"/>

View file

@ -525,7 +525,7 @@ namespace Bit.Droid.Services
{ {
return; return;
} }
if (activity.Intent.GetBooleanExtra("autofillFramework", false)) if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
{ {
if (cipher == null) if (cipher == null)
{ {
@ -596,7 +596,7 @@ namespace Bit.Droid.Services
public void Background() public void Background()
{ {
var activity = (MainActivity)CrossCurrentActivity.Current.Activity; var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
if (activity.Intent.GetBooleanExtra("autofillFramework", false)) if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
{ {
activity.SetResult(Result.Canceled); activity.SetResult(Result.Canceled);
activity.Finish(); activity.Finish();