From 6973a0b71ca0cde59e01d9dc5c26825cbfa432d8 Mon Sep 17 00:00:00 2001
From: mp-bw <59324545+mp-bw@users.noreply.github.com>
Date: Mon, 5 Dec 2022 12:49:34 -0500
Subject: [PATCH] [BEEEP] Support for automatic TOTP token copy via external
 autofill (Android) (#2220)

* Android: Support for automatic TOTP copy via external autofill

* update iOS autofill interface

* additional tweaks
---
 src/Android/Android.csproj                    | 16 +++----
 src/Android/Autofill/AutofillConstants.cs     | 10 +++++
 .../AutofillExternalSelectionActivity.cs      | 42 +++++++++++++++++++
 src/Android/Autofill/AutofillHelpers.cs       | 26 ++++++++----
 src/Android/Autofill/FilledItem.cs            |  2 +
 src/Android/MainActivity.cs                   |  7 ++--
 src/Android/Properties/AndroidManifest.xml    |  8 +---
 .../Receivers/ClearClipboardAlarmReceiver.cs  | 13 +++++-
 src/Android/Services/AutofillHandler.cs       |  6 +--
 src/Android/Services/ClipboardService.cs      | 13 +++---
 src/Android/Services/DeviceActionService.cs   | 15 ++++---
 11 files changed, 115 insertions(+), 43 deletions(-)
 create mode 100644 src/Android/Autofill/AutofillConstants.cs
 create mode 100644 src/Android/Autofill/AutofillExternalSelectionActivity.cs

diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index 4825654e1..73a19d10e 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -15,7 +15,7 @@
     <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
     <MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
     <MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
-    <TargetFrameworkVersion>v12.1</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v13.0</TargetFrameworkVersion>
     <AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
     <NuGetPackageImportStamp>
     </NuGetPackageImportStamp>
@@ -77,12 +77,12 @@
     <PackageReference Include="Portable.BouncyCastle">
       <Version>1.9.0</Version>
     </PackageReference>
-    <PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1" />
-    <PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.13" />
-    <PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.16" />
-    <PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0" />
-    <PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.14" />
-    <PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1" />
+    <PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1.1" />
+    <PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.14" />
+    <PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.17" />
+    <PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0.1" />
+    <PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.15" />
+    <PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1.1" />
     <PackageReference Include="Xamarin.Essentials">
       <Version>1.7.3</Version>
     </PackageReference>
@@ -103,8 +103,10 @@
     <Compile Include="Accessibility\Browser.cs" />
     <Compile Include="Accessibility\NodeList.cs" />
     <Compile Include="Accessibility\KnownUsernameField.cs" />
+    <Compile Include="Autofill\AutofillConstants.cs" />
     <Compile Include="Autofill\AutofillHelpers.cs" />
     <Compile Include="Autofill\AutofillService.cs" />
+    <Compile Include="Autofill\AutofillExternalSelectionActivity.cs" />
     <Compile Include="Autofill\Field.cs" />
     <Compile Include="Autofill\FieldCollection.cs" />
     <Compile Include="Autofill\FilledItem.cs" />
diff --git a/src/Android/Autofill/AutofillConstants.cs b/src/Android/Autofill/AutofillConstants.cs
new file mode 100644
index 000000000..9bb1782f7
--- /dev/null
+++ b/src/Android/Autofill/AutofillConstants.cs
@@ -0,0 +1,10 @@
+namespace Bit.Droid.Autofill
+{
+    public class AutofillConstants
+    {
+        public const string AutofillFramework = "autofillFramework";
+        public const string AutofillFrameworkFillType = "autofillFrameworkFillType";
+        public const string AutofillFrameworkUri = "autofillFrameworkUri";
+        public const string AutofillFrameworkCipherId = "autofillFrameworkCipherId";
+    }
+}
diff --git a/src/Android/Autofill/AutofillExternalSelectionActivity.cs b/src/Android/Autofill/AutofillExternalSelectionActivity.cs
new file mode 100644
index 000000000..c75d150be
--- /dev/null
+++ b/src/Android/Autofill/AutofillExternalSelectionActivity.cs
@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+using Bit.Core.Abstractions;
+using Bit.Core.Utilities;
+using Bit.Droid.Utilities;
+
+namespace Bit.Droid.Autofill
+{
+    [Activity(
+        NoHistory = true,
+        LaunchMode = LaunchMode.SingleTop)]
+    public class AutofillExternalSelectionActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
+    {
+        protected override void OnCreate(Bundle bundle)
+        {
+            Intent?.Validate();
+            base.OnCreate(bundle);
+
+            var cipherId = Intent?.GetStringExtra(AutofillConstants.AutofillFrameworkCipherId);
+            if (string.IsNullOrEmpty(cipherId))
+            {
+                SetResult(Result.Canceled);
+                Finish();
+                return;
+            }
+
+            GetCipherAndPerformAutofillAsync(cipherId).FireAndForget();
+        }
+
+        private async Task GetCipherAndPerformAutofillAsync(string cipherId)
+        {
+            var cipherService = ServiceContainer.Resolve<ICipherService>();
+            var cipher = await cipherService.GetAsync(cipherId);
+            var decCipher = await cipher.DecryptAsync();
+
+            var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
+            autofillHandler.Autofill(decCipher);
+        }
+    }
+}
diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs
index f1984dbff..4702af3ac 100644
--- a/src/Android/Autofill/AutofillHelpers.cs
+++ b/src/Android/Autofill/AutofillHelpers.cs
@@ -207,7 +207,7 @@ namespace Bit.Droid.Autofill
                         }
                     }
                     var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, items[i], 
-                        inlinePresentationSpec);
+                        true, inlinePresentationSpec);
                     if (dataset != null)
                     {
                         responseBuilder.AddDataset(dataset);
@@ -221,7 +221,7 @@ namespace Bit.Droid.Autofill
         }
 
         public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem,
-            InlinePresentationSpec inlinePresentationSpec = null)
+            bool includeAuthIntent, InlinePresentationSpec inlinePresentationSpec = null)
         {
             var overlayPresentation = BuildOverlayPresentation(
                 filledItem.Name,
@@ -242,6 +242,15 @@ namespace Bit.Droid.Autofill
             {
                 datasetBuilder.SetInlinePresentation(inlinePresentation);
             }
+            if (includeAuthIntent)
+            {
+                var intent = new Intent(context, typeof(AutofillExternalSelectionActivity));
+                intent.PutExtra(AutofillConstants.AutofillFramework, true);
+                intent.PutExtra(AutofillConstants.AutofillFrameworkCipherId, filledItem.Id);
+                var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
+                AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
+                datasetBuilder.SetAuthentication(pendingIntent?.IntentSender);
+            }
             if (filledItem.ApplyToFields(fields, datasetBuilder))
             {
                 return datasetBuilder.Build();
@@ -253,25 +262,26 @@ namespace Bit.Droid.Autofill
             IList<InlinePresentationSpec> inlinePresentationSpecs = null)
         {
             var intent = new Intent(context, typeof(MainActivity));
-            intent.PutExtra("autofillFramework", true);
+            intent.PutExtra(AutofillConstants.AutofillFramework, true);
             if (fields.FillableForLogin)
             {
-                intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Login);
+                intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Login);
             }
             else if (fields.FillableForCard)
             {
-                intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Card);
+                intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Card);
             }
             else if (fields.FillableForIdentity)
             {
-                intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Identity);
+                intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Identity);
             }
             else
             {
                 return null;
             }
-            intent.PutExtra("autofillFrameworkUri", uri);
-            var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
+            intent.PutExtra(AutofillConstants.AutofillFrameworkUri, uri);
+            var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
+                AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
 
             var overlayPresentation = BuildOverlayPresentation(
                 AppResources.AutofillWithBitwarden,
diff --git a/src/Android/Autofill/FilledItem.cs b/src/Android/Autofill/FilledItem.cs
index 3964f37bd..46b0436fc 100644
--- a/src/Android/Autofill/FilledItem.cs
+++ b/src/Android/Autofill/FilledItem.cs
@@ -23,6 +23,7 @@ namespace Bit.Droid.Autofill
 
         public FilledItem(CipherView cipher)
         {
+            Id = cipher.Id;
             Name = cipher.Name;
             Type = cipher.Type;
             Subtitle = cipher.SubTitle;
@@ -55,6 +56,7 @@ namespace Bit.Droid.Autofill
             }
         }
 
+        public string Id { get; set; }
         public string Name { get; set; }
         public string Subtitle { get; set; } = string.Empty;
         public int Icon { get; set; } = Resource.Drawable.login;
diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs
index cffc12ac0..2e25bc1cc 100644
--- a/src/Android/MainActivity.cs
+++ b/src/Android/MainActivity.cs
@@ -18,6 +18,7 @@ using Bit.Core;
 using Bit.Core.Abstractions;
 using Bit.Core.Enums;
 using Bit.Core.Utilities;
+using Bit.Droid.Autofill;
 using Bit.Droid.Receivers;
 using Bit.Droid.Utilities;
 using Newtonsoft.Json;
@@ -322,13 +323,13 @@ namespace Bit.Droid
         {
             var options = new AppOptions
             {
-                Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra("autofillFrameworkUri"),
+                Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri),
                 MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
                 GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
-                FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false),
+                FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false),
                 CreateSend = GetCreateSendRequest(Intent)
             };
-            var fillType = Intent.GetIntExtra("autofillFrameworkFillType", 0);
+            var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0);
             if (fillType > 0)
             {
                 options.FillType = (CipherType)fillType;
diff --git a/src/Android/Properties/AndroidManifest.xml b/src/Android/Properties/AndroidManifest.xml
index ea7066845..54ed96ab6 100644
--- a/src/Android/Properties/AndroidManifest.xml
+++ b/src/Android/Properties/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.11.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
-	<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32" />
+	<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
 	<uses-permission android:name="android.permission.INTERNET" />
 	<uses-permission android:name="android.permission.NFC" />
 	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -40,10 +40,4 @@
 			</intent-filter>
 		</activity>
 	</application>
-	<!-- Package visibility (for Android 11+) -->
-	<queries>
-		<intent>
-			<action android:name="*" />
-		</intent>
-	</queries>
 </manifest>
\ No newline at end of file
diff --git a/src/Android/Receivers/ClearClipboardAlarmReceiver.cs b/src/Android/Receivers/ClearClipboardAlarmReceiver.cs
index ff1566134..6e57cedb7 100644
--- a/src/Android/Receivers/ClearClipboardAlarmReceiver.cs
+++ b/src/Android/Receivers/ClearClipboardAlarmReceiver.cs
@@ -1,4 +1,5 @@
 using Android.Content;
+using Android.OS;
 
 namespace Bit.Droid.Receivers
 {
@@ -8,7 +9,17 @@ namespace Bit.Droid.Receivers
         public override void OnReceive(Context context, Intent intent)
         {
             var clipboardManager = context.GetSystemService(Context.ClipboardService) as ClipboardManager;
-            clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", " ");
+            if (clipboardManager == null)
+            {
+                return;
+            }
+            // ClearPrimaryClip is supported down to API 28 with mixed results, so we're requiring 33+ instead
+            if ((int)Build.VERSION.SdkInt < 33)
+            {
+                clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", " ");
+                return;
+            }
+            clipboardManager.ClearPrimaryClip();
         }
     }
 }
diff --git a/src/Android/Services/AutofillHandler.cs b/src/Android/Services/AutofillHandler.cs
index 9cfa5ec9e..ad1a0ad5d 100644
--- a/src/Android/Services/AutofillHandler.cs
+++ b/src/Android/Services/AutofillHandler.cs
@@ -73,12 +73,12 @@ namespace Bit.Droid.Services
 
         public void Autofill(CipherView cipher)
         {
-            var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
+            var activity = CrossCurrentActivity.Current.Activity as Xamarin.Forms.Platform.Android.FormsAppCompatActivity;
             if (activity == null)
             {
                 return;
             }
-            if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
+            if (activity.Intent?.GetBooleanExtra(AutofillConstants.AutofillFramework, false) ?? false)
             {
                 if (cipher == null)
                 {
@@ -103,7 +103,7 @@ namespace Bit.Droid.Services
                     return;
                 }
                 var task = CopyTotpAsync(cipher);
-                var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher));
+                var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher), false);
                 var replyIntent = new Intent();
                 replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
                 activity.SetResult(Result.Ok, replyIntent);
diff --git a/src/Android/Services/ClipboardService.cs b/src/Android/Services/ClipboardService.cs
index 33585a274..fbb7a1ffb 100644
--- a/src/Android/Services/ClipboardService.cs
+++ b/src/Android/Services/ClipboardService.cs
@@ -6,7 +6,6 @@ using Android.OS;
 using Bit.Core.Abstractions;
 using Bit.Droid.Receivers;
 using Bit.Droid.Utilities;
-using Plugin.CurrentActivity;
 using Xamarin.Essentials;
 
 namespace Bit.Droid.Services
@@ -21,9 +20,9 @@ namespace Bit.Droid.Services
             _stateService = stateService;
 
             _clearClipboardPendingIntent = new Lazy<PendingIntent>(() =>
-                PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity,
+                PendingIntent.GetBroadcast(Application.Context,
                                            0,
-                                           new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)),
+                                           new Intent(Application.Context, typeof(ClearClipboardAlarmReceiver)),
                                            AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false)));
         }
 
@@ -45,7 +44,7 @@ namespace Bit.Droid.Services
             }
             catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to"))
             {
-                // #1962 Just ignore, the content is copied either way but there is some app interfiering in the process
+                // #1962 Just ignore, the content is copied either way but there is some app interfering in the process
                 // that the OS catches and just throws this exception.
             }
         }
@@ -58,9 +57,7 @@ namespace Bit.Droid.Services
 
         private void CopyToClipboard(string text, bool isSensitive = true)
         {
-            var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
-            var clipboardManager = activity.GetSystemService(
-                Context.ClipboardService) as Android.Content.ClipboardManager;
+            var clipboardManager = Application.Context.GetSystemService(Context.ClipboardService) as ClipboardManager;
             var clipData = ClipData.NewPlainText("bitwarden", text);
             if (isSensitive)
             {
@@ -87,7 +84,7 @@ namespace Bit.Droid.Services
                 return;
             }
             var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + clearMs;
-            var alarmManager = CrossCurrentActivity.Current.Activity.GetSystemService(Context.AlarmService) as AlarmManager;
+            var alarmManager = Application.Context.GetSystemService(Context.AlarmService) as AlarmManager;
             alarmManager.Set(AlarmType.Rtc, triggerMs, _clearClipboardPendingIntent.Value);
         }
     }
diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs
index c50558280..f189c77ce 100644
--- a/src/Android/Services/DeviceActionService.cs
+++ b/src/Android/Services/DeviceActionService.cs
@@ -69,14 +69,17 @@ namespace Bit.Droid.Services
 
         public bool LaunchApp(string appName)
         {
+            if ((int)Build.VERSION.SdkInt < 33)
+            {
+                // API 33 required to avoid using wildcard app visibility or dangerous permissions
+                // https://developer.android.com/reference/android/content/pm/PackageManager#getLaunchIntentSenderForPackage(java.lang.String)
+                return false;
+            }
             var activity = CrossCurrentActivity.Current.Activity;
             appName = appName.Replace("androidapp://", string.Empty);
-            var launchIntent = activity.PackageManager.GetLaunchIntentForPackage(appName);
-            if (launchIntent != null)
-            {
-                activity.StartActivity(launchIntent);
-            }
-            return launchIntent != null;
+            var launchIntentSender = activity?.PackageManager?.GetLaunchIntentSenderForPackage(appName);
+            launchIntentSender?.SendIntent(activity, Result.Ok, null, null, null);
+            return launchIntentSender != null;
         }
 
         public async Task ShowLoadingAsync(string text)