mirror of
https://github.com/bitwarden/android.git
synced 2024-12-25 18:38:27 +03:00
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:
parent
e80b3e4542
commit
311d3dd635
8 changed files with 177 additions and 23 deletions
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue