diff --git a/.gitignore b/.gitignore
index edfb7c624..f5edffcfa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -178,3 +178,4 @@ src/java/Keepass2AndroidPluginSDK2/build/generated/mockable-Google-Inc.-Google-A
/src/MegaTest
*.dtbcache.json
/src/keepass2android-app/AndroidManifest.xml
+*.DS_Store
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..682a7a532
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,14 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "C#: keepass2android-app Debug",
+ "type": "dotnet",
+ "request": "launch",
+ "projectPath": "${workspaceFolder}/src/keepass2android-app/keepass2android-app.csproj"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Kp2aBusinessLogic/IKp2aApp.cs b/src/Kp2aBusinessLogic/IKp2aApp.cs
index 60f93d6e2..0c37550ef 100644
--- a/src/Kp2aBusinessLogic/IKp2aApp.cs
+++ b/src/Kp2aBusinessLogic/IKp2aApp.cs
@@ -167,5 +167,11 @@ int WebDavChunkedUploadSize
/// Registers an action that should be executed when the context instance with the given id has been resumed.
///
void RegisterPendingActionForContextInstance(int contextInstanceId, ActionOnOperationFinished actionToPerformWhenContextIsResumed);
+
+ ///
+ /// Use an App level (singletone) AtomicInteger
+ /// for the request code of pending intent in the credential provider service.
+ ///
+ int RequestCodeForCredentialProvider { get; }
}
}
\ No newline at end of file
diff --git a/src/Kp2aBusinessLogic/database/edit/CreateDB.cs b/src/Kp2aBusinessLogic/database/edit/CreateDB.cs
index 5e03ab73d..3983ac9da 100644
--- a/src/Kp2aBusinessLogic/database/edit/CreateDB.cs
+++ b/src/Kp2aBusinessLogic/database/edit/CreateDB.cs
@@ -28,6 +28,7 @@ namespace keepass2android
public class CreateDb : OperationWithFinishHandler
{
+ public const string DefaultDbName = "Keepass2Android Password Database";
private readonly IOConnectionInfo _ioc;
private readonly bool _dontSave;
private readonly IKp2aApp _app;
@@ -68,7 +69,7 @@ public override void Run()
db.KpDatabase.New(_ioc, _key, _app.GetFileStorage(_ioc).GetFilenameWithoutPathAndExt(_ioc));
db.KpDatabase.KdfParameters = (new AesKdf()).GetDefaultParameters();
- db.KpDatabase.Name = "Keepass2Android Password Database";
+ db.KpDatabase.Name = DefaultDbName;
//re-set the name of the root group because the PwDatabase uses UrlUtil which is not appropriate for all file storages:
db.KpDatabase.RootGroup.Name = _app.GetFileStorage(_ioc).GetFilenameWithoutPathAndExt(_ioc);
diff --git a/src/keepass2android-app/Resources/values/strings.xml b/src/keepass2android-app/Resources/values/strings.xml
index c3246a357..e8f05879b 100644
--- a/src/keepass2android-app/Resources/values/strings.xml
+++ b/src/keepass2android-app/Resources/values/strings.xml
@@ -1350,5 +1350,10 @@
Periodic background synchronization time interval
Set the interval for background synchronization in minutes.
+
+ Password credential provider
+ Save Password in new entry
+ Open %1$s
+ Manage Credentials
\ No newline at end of file
diff --git a/src/keepass2android-app/Resources/xml/credentials_provider.xml b/src/keepass2android-app/Resources/xml/credentials_provider.xml
new file mode 100644
index 000000000..636c5d9cb
--- /dev/null
+++ b/src/keepass2android-app/Resources/xml/credentials_provider.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/keepass2android-app/app/App.cs b/src/keepass2android-app/app/App.cs
index 4123500a2..bc030ba62 100644
--- a/src/keepass2android-app/app/App.cs
+++ b/src/keepass2android-app/app/App.cs
@@ -66,6 +66,7 @@ You should have received a copy of the GNU General Public License
using Java.Interop;
using AndroidX.Lifecycle;
using keepass2android.services;
+using Java.Util.Concurrent.Atomic;
namespace keepass2android
@@ -1628,6 +1629,13 @@ public void PerformPendingActions(int instanceId)
}
}
}
+
+ private readonly AtomicInteger requestCodeForCredentialProvider = new();
+ public int RequestCodeForCredentialProvider
+ {
+ get => requestCodeForCredentialProvider.IncrementAndGet();
+ }
+
}
diff --git a/src/keepass2android-app/keepass2android-app.csproj b/src/keepass2android-app/keepass2android-app.csproj
index 768e6815d..94ea7eb17 100644
--- a/src/keepass2android-app/keepass2android-app.csproj
+++ b/src/keepass2android-app/keepass2android-app.csproj
@@ -745,6 +745,7 @@
+
diff --git a/src/keepass2android-app/services/Kp2aCredentialProvider/Kp2aCredentialLauncherActivity.cs b/src/keepass2android-app/services/Kp2aCredentialProvider/Kp2aCredentialLauncherActivity.cs
new file mode 100644
index 000000000..189ca23e3
--- /dev/null
+++ b/src/keepass2android-app/services/Kp2aCredentialProvider/Kp2aCredentialLauncherActivity.cs
@@ -0,0 +1,388 @@
+// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
+//
+// Keepass2Android is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Keepass2Android is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Keepass2Android. If not, see .
+
+using System.Diagnostics;
+using Android.Content;
+using Android.Content.PM;
+using Android.OS;
+using Android.Runtime;
+using Android.Views;
+using AndroidX.Core.App;
+using AndroidX.Credentials;
+using AndroidX.Credentials.Exceptions;
+using AndroidX.Credentials.Provider;
+using Java.Time;
+using Keepass2android.Pluginsdk;
+using KeePassLib;
+using KeePassLib.Utility;
+using Org.Json;
+
+namespace keepass2android.services.Kp2aCredentialProvider
+{
+ [Activity(
+ Label = AppNames.AppName,
+ ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden,
+ Theme = "@style/Kp2aTheme_ActionBar",
+ WindowSoftInputMode = SoftInput.AdjustResize,
+ Permission = "keepass2android." + AppNames.PackagePart + ".permission.Kp2aChooseAutofill"
+ )]
+ public class Kp2aCredentialLauncherActivity : AndroidX.AppCompat.App.AppCompatActivity
+ {
+ public const string CredentialRequestTypeKey = "credential_request_type";
+ public const int CredentialRequestTypeCreatePassword = 1;
+ public const int CredentialRequestTypeUnlock = 2;
+ public const int CredentialRequestTypeGetPasswordForEntry = 3;
+ private const int CreateEntryRequestCode = 100;
+ private const int UnlockRequestCode = 300;
+
+ protected override void OnCreate(Bundle? savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+
+ var intent = Intent;
+ if (intent == null)
+ {
+ // should never happen
+ SetResult(Result.Canceled);
+ Finish();
+ }
+ else
+ {
+ var requestType = intent.GetIntExtra(CredentialRequestTypeKey, 0);
+ switch (requestType)
+ {
+ case CredentialRequestTypeCreatePassword:
+ HandleCreatePasswordRequest(intent);
+ break;
+ case CredentialRequestTypeUnlock:
+ HandleUnlockRequest(intent);
+ break;
+ case CredentialRequestTypeGetPasswordForEntry:
+ HandleGetPasswordForEntryRequest(intent);
+ break;
+
+ default:
+ // should never happen
+ SetResult(Result.Canceled);
+ Finish();
+ break;
+ }
+ }
+ }
+
+ private void HandleCreatePasswordRequest(Intent requestIntent)
+ {
+ var createRequest = PendingIntentHandler.RetrieveProviderCreateCredentialRequest(
+ requestIntent
+ );
+ if (createRequest != null && createRequest?.CallingRequest is CreatePasswordRequest)
+ {
+ if (createRequest.CallingRequest is not CreatePasswordRequest request)
+ {
+ SetUpFailureResponseForCreateAndFinish("Unable to extract request from intent");
+ }
+ else
+ {
+ var callingPackage = createRequest.CallingAppInfo.PackageName;
+
+ var forwardIntent = new Intent(this, typeof(SelectCurrentDbActivity));
+
+ Dictionary outputFields = [];
+ if (callingPackage != null)
+ {
+ outputFields.TryAdd(PwDefs.UrlField, $"{KeePass.AndroidAppScheme}{callingPackage}");
+ }
+
+ outputFields.TryAdd(PwDefs.UserNameField, request.Id);
+ outputFields.TryAdd(PwDefs.PasswordField, request.Password);
+
+ JSONObject jsonOutput = new(outputFields);
+ var jsonOutputStr = jsonOutput.ToString();
+ forwardIntent.PutExtra(Strings.ExtraEntryOutputData, jsonOutputStr);
+
+ JSONArray jsonProtectedFields = new(
+ (System.Collections.ICollection)Array.Empty()
+ );
+ forwardIntent.PutExtra(Strings.ExtraProtectedFieldsList, jsonProtectedFields.ToString());
+
+ forwardIntent.PutExtra(AppTask.AppTaskKey, "CreateEntryThenCloseTask");
+ forwardIntent.PutExtra(CreateEntryThenCloseTask.ShowUserNotificationsKey, "false");
+ StartActivityForResult(forwardIntent, CreateEntryRequestCode);
+ }
+ }
+ else
+ {
+ SetUpFailureResponseForCreateAndFinish("Unable to extract request from intent");
+ }
+ }
+
+ private void HandleUnlockRequest(Intent requestIntent)
+ {
+ var getRequest = PendingIntentHandler.RetrieveBeginGetCredentialRequest(requestIntent);
+ var callingPackage = getRequest?.CallingAppInfo?.PackageName;
+
+ if (getRequest == null || callingPackage == null)
+ {
+ //should never happen
+ return;
+ }
+
+ var query = $"{KeePass.AndroidAppScheme}{callingPackage}";
+ //launch SelectCurrentDbActivity (which is root of the stack (exception: we're even below!)) with the appropriate task.
+ //will return the results later
+ Intent forwardIntent = new(this, typeof(SelectCurrentDbActivity));
+ //don't show user notifications when an entry is opened.
+ var task = new SearchUrlTask() { UrlToSearchFor = query };
+ task.ToIntent(forwardIntent);
+ StartActivityForResult(forwardIntent, UnlockRequestCode);
+ }
+
+ private void HandleGetPasswordForEntryRequest(Intent requestIntent)
+ {
+ var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(requestIntent);
+ var options = getRequest?.CredentialOptions;
+ if (options == null || options.Count == 0)
+ {
+ SetUpFailureResponseForGetAndFinish();
+ return;
+ }
+
+ var option = options.First();
+ if (option is GetPasswordOption)
+ {
+ var entryId = requestIntent.GetStringExtra(Strings.ExtraEntryId);
+ if (string.IsNullOrEmpty(entryId))
+ {
+ SetUpFailureResponseForGetAndFinish();
+ return;
+ }
+ var entryUuid = new PwUuid(MemUtil.HexStringToByteArray(entryId));
+
+ var lastOpenedEntry = App.Kp2a.LastOpenedEntry;
+ if (lastOpenedEntry != null && entryUuid.Equals(lastOpenedEntry.Uuid))
+ {
+ SetupGetCredentialResponseForEntryAndFinish(lastOpenedEntry.Entry);
+ }
+ else
+ {
+ foreach (Database db in App.Kp2a.OpenDatabases)
+ {
+ if (db.EntriesById.TryGetValue(entryUuid, out var resultEntry))
+ {
+ SetupGetCredentialResponseForEntryAndFinish(resultEntry);
+ return;
+ }
+ }
+ //nothing found in open databases
+ SetUpNoCredentialResponseForGetAndFinish();
+ }
+ }
+ else
+ {
+ SetUpFailureResponseForGetAndFinish();
+ }
+ }
+
+ protected override void OnActivityResult(
+ int requestCode,
+ [GeneratedEnum] Result resultCode,
+ Intent? data
+ )
+ {
+ base.OnActivityResult(requestCode, resultCode, data);
+ switch (requestCode)
+ {
+ case CreateEntryRequestCode:
+ SetupCreateCredentialResponseAndFinish(resultCode);
+ break;
+
+ case UnlockRequestCode:
+ SetupUnlockResponseAndFinish(resultCode);
+ break;
+ }
+ }
+
+ ///
+ /// Saves the password and sets the response back to the calling app.
+ ///
+ /// The result code of forward intent.
+ private void SetupCreateCredentialResponseAndFinish([GeneratedEnum] Result resultCode)
+ {
+ var result = new Intent();
+ if (resultCode == KeePass.ExitCloseAfterTaskComplete)
+ {
+ PendingIntentHandler.SetCreateCredentialResponse(result, new CreatePasswordResponse());
+ SetResult(Result.Ok, result);
+ }
+ else
+ {
+ PendingIntentHandler.SetCreateCredentialException(
+ result,
+ new CreateCredentialCancellationException()
+ );
+ SetResult(Result.Canceled, result);
+ }
+ if (!IsFinishing)
+ {
+ Finish();
+ }
+ }
+
+ ///
+ /// Sets and returns the credential list from the unlocked database
+ ///
+ /// The result code of forward intent.
+ private void SetupUnlockResponseAndFinish([GeneratedEnum] Result resultCode)
+ {
+ var thisIntent = Intent;
+ if (thisIntent != null)
+ {
+ var getRequest = PendingIntentHandler.RetrieveBeginGetCredentialRequest(thisIntent);
+ var options = getRequest?.BeginGetCredentialOptions;
+ if (options != null && options.Count >= 0)
+ {
+ var option = options.First();
+ if (option != null && option is BeginGetPasswordOption)
+ {
+ var result = new Intent();
+ if (resultCode == KeePass.ExitCloseAfterTaskComplete)
+ {
+ var response = new BeginGetCredentialResponse.Builder();
+ var entry = App.Kp2a.LastOpenedEntry;
+ var username = entry?.Entry.Strings.ReadSafe(PwDefs.UserNameField);
+ if (entry != null && !string.IsNullOrEmpty(username))
+ {
+ var lastAccessTime =
+ Build.VERSION.SdkInt >= BuildVersionCodes.O
+ ? Instant.OfEpochMilli(
+ new DateTimeOffset(entry.Entry.LastAccessTime).ToUnixTimeMilliseconds()
+ )
+ : null;
+
+ response.AddCredentialEntry(
+ new PasswordCredentialEntry.Builder(
+ this,
+ username,
+ PendingIntentCompat.GetActivity(
+ this,
+ App.Kp2a.RequestCodeForCredentialProvider,
+ new Intent(this, typeof(Kp2aCredentialLauncherActivity))
+ .PutExtra(
+ CredentialRequestTypeKey,
+ CredentialRequestTypeGetPasswordForEntry
+ )
+ .PutExtra(Strings.ExtraEntryId, entry.Entry.Uuid.ToHexString()),
+ (int)PendingIntentFlags.UpdateCurrent,
+ true
+ )!,
+ option.JavaCast()
+ )
+ .SetDisplayName(entry.Entry.Strings.ReadSafe(PwDefs.TitleField))
+ .SetAffiliatedDomain(entry.Entry.ParentGroup?.Name)
+ .SetLastUsedTime(lastAccessTime)
+ //TODO SetIcon() of the entry
+ .Build()
+ );
+ }
+ PendingIntentHandler.SetBeginGetCredentialResponse(result, response.Build());
+ SetResult(Result.Ok, result);
+ }
+ }
+ }
+ }
+
+ if (!IsFinishing)
+ {
+ Finish();
+ }
+ }
+
+ private void SetupGetCredentialResponseForEntryAndFinish(PwEntry entry)
+ {
+ var username = entry.Strings.ReadSafe(PwDefs.UserNameField);
+ var password = entry.Strings.ReadSafe(PwDefs.PasswordField);
+ if (username == null || string.IsNullOrEmpty(password))
+ {
+ SetUpNoCredentialResponseForGetAndFinish();
+ return;
+ }
+
+ var result = new Intent();
+ PendingIntentHandler.SetGetCredentialResponse(
+ result,
+ new GetCredentialResponse(new PasswordCredential(username, password))
+ );
+ SetResult(Result.Ok, result);
+
+ if (!IsFinishing)
+ {
+ Finish();
+ }
+ }
+
+ ///
+ /// If the request is null, send an unknown exception to client and finish the flow.
+ ///
+ /// The error message to send to the client.
+ private void SetUpFailureResponseForCreateAndFinish(string? message = null)
+ {
+ var result = new Intent();
+ PendingIntentHandler.SetCreateCredentialException(
+ result,
+ new CreateCredentialUnknownException(message)
+ );
+ SetResult(Result.Ok, result);
+
+ if (!IsFinishing)
+ {
+ Finish();
+ }
+ }
+
+ ///
+ /// Sets up a failure response for get request and finishes the activity.
+ ///
+ /// The error message to include in the response.
+ private void SetUpFailureResponseForGetAndFinish(string? message = null)
+ {
+ var result = new Intent();
+ PendingIntentHandler.SetGetCredentialException(
+ result,
+ new GetCredentialUnknownException(message)
+ );
+ SetResult(Result.Ok, result);
+
+ if (!IsFinishing)
+ {
+ Finish();
+ }
+ }
+
+ ///
+ /// Sets up a no credential response for get request and finishes the activity.
+ ///
+ private void SetUpNoCredentialResponseForGetAndFinish()
+ {
+ var result = new Intent();
+ PendingIntentHandler.SetGetCredentialException(result, new NoCredentialException());
+ SetResult(Result.Ok, result);
+
+ if (!IsFinishing)
+ {
+ Finish();
+ }
+ }
+ }
+}
diff --git a/src/keepass2android-app/services/Kp2aCredentialProvider/Kp2aCredentialProviderService.cs b/src/keepass2android-app/services/Kp2aCredentialProvider/Kp2aCredentialProviderService.cs
new file mode 100644
index 000000000..d018c3212
--- /dev/null
+++ b/src/keepass2android-app/services/Kp2aCredentialProvider/Kp2aCredentialProviderService.cs
@@ -0,0 +1,217 @@
+// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
+//
+// Keepass2Android is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Keepass2Android is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Keepass2Android. If not, see .
+
+using System.Runtime.Versioning;
+using Android.Content;
+using Android.Graphics;
+using Android.Graphics.Drawables;
+using Android.OS;
+using AndroidX.Core.App;
+using AndroidX.Credentials.Exceptions;
+using AndroidX.Credentials.Provider;
+using Java.Interop;
+using Java.Time;
+using Keepass2android.Pluginsdk;
+using KeePassLib;
+
+namespace keepass2android.services.Kp2aCredentialProvider
+{
+ [Service(
+ Enabled = true,
+ Exported = true,
+ Permission = "android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
+ )]
+ [IntentFilter(actions: ["android.service.credentials.CredentialProviderService"])]
+ [MetaData(name: "android.credentials.provider", Resource = "@xml/credentials_provider")]
+ [SupportedOSPlatform("android31.0")]
+ public class Kp2aCredentialProviderService : CredentialProviderService
+ {
+ public override void OnBeginCreateCredentialRequest(
+ BeginCreateCredentialRequest request,
+ CancellationSignal cancellationSignal,
+ IOutcomeReceiver callback
+ )
+ {
+ Kp2aLog.Log("Kp2aCredentialProviderService:onBeginCreateCredentialRequest called");
+ if (request is BeginCreatePasswordCredentialRequest)
+ {
+ var currentDb = App.Kp2a.CurrentDb;
+ var accountName = currentDb?.KpDatabase?.Name ?? CreateDb.DefaultDbName;
+ var blendMode = BlendMode.Dst;
+ var icon =
+ blendMode == null
+ ? null
+ : Icon.CreateWithResource(this, AppNames.LauncherIcon)?.SetTintBlendMode(blendMode);
+
+ callback.OnResult(
+ new BeginCreateCredentialResponse.Builder()
+ .AddCreateEntry(
+ new CreateEntry.Builder(
+ accountName,
+ PendingIntentCompat.GetActivity(
+ this,
+ App.Kp2a.RequestCodeForCredentialProvider,
+ new Intent(this, typeof(Kp2aCredentialLauncherActivity)).PutExtra(
+ Kp2aCredentialLauncherActivity.CredentialRequestTypeKey,
+ Kp2aCredentialLauncherActivity.CredentialRequestTypeCreatePassword
+ ),
+ (int)PendingIntentFlags.UpdateCurrent,
+ true
+ )!
+ )
+ .SetIcon(icon)
+ .SetDescription(
+ GetString(Resource.String.credential_provider_password_creation_description)
+ )
+ // Set the last used time to "now"
+ // so the active account is the default option in the system prompt.
+ .SetLastUsedTime(currentDb == null ? null : Instant.Now())
+ .Build()
+ )
+ .Build()
+ );
+ }
+ else
+ {
+ callback.OnError(new CreateCredentialUnsupportedException().JavaCast());
+ }
+ }
+
+ public override void OnBeginGetCredentialRequest(
+ BeginGetCredentialRequest request,
+ CancellationSignal cancellationSignal,
+ IOutcomeReceiver callback
+ )
+ {
+ Kp2aLog.Log("Kp2aCredentialProviderService:OnBeginGetCredentialRequest called");
+
+ var callingPackage = request.CallingAppInfo?.PackageName;
+ if (callingPackage == null)
+ {
+ callback.OnError(new NoCredentialException().JavaCast());
+ }
+
+ var appLocked = !App.Kp2a.DatabaseIsUnlocked;
+ var responseBuilder = new BeginGetCredentialResponse.Builder();
+
+ // Note that if your credentials are locked, you can immediately set an AuthenticationAction on the response and invoke the callback.
+ if (appLocked)
+ {
+ callback.OnResult(
+ responseBuilder
+ .AddAuthenticationAction(
+ new AuthenticationAction(
+ // Providers that require unlocking the credentials before returning any credentialEntries,
+ // must set up a pending intent that navigates the user to the app's unlock flow.
+ GetString(AppNames.AppNameResource),
+ PendingIntentCompat.GetActivity(
+ this,
+ App.Kp2a.RequestCodeForCredentialProvider,
+ new Intent(this, typeof(Kp2aCredentialLauncherActivity)).PutExtra(
+ Kp2aCredentialLauncherActivity.CredentialRequestTypeKey,
+ Kp2aCredentialLauncherActivity.CredentialRequestTypeUnlock
+ ),
+ (int)PendingIntentFlags.UpdateCurrent,
+ true
+ )!
+ )
+ )
+ .Build()
+ );
+ return;
+ }
+
+ foreach (var option in request.BeginGetCredentialOptions)
+ {
+ if (option is BeginGetPasswordOption)
+ {
+ var query = $"{KeePass.AndroidAppScheme}{callingPackage}";
+ var foundEntries = ShareUrlResults.GetSearchResultsForUrl(query)?.Entries ?? [];
+
+ var lastOpenedEntry = App.Kp2a.LastOpenedEntry;
+ if (lastOpenedEntry != null && lastOpenedEntry?.SearchUrl == query)
+ {
+ foundEntries.Clear();
+ foundEntries.Add(lastOpenedEntry.Entry);
+ }
+
+ foreach (var entry in foundEntries)
+ {
+ var username = entry.Strings.ReadSafe(PwDefs.UserNameField);
+ if (string.IsNullOrEmpty(username))
+ {
+ continue;
+ }
+
+ responseBuilder.AddCredentialEntry(
+ new PasswordCredentialEntry.Builder(
+ this,
+ username,
+ PendingIntentCompat.GetActivity(
+ this,
+ App.Kp2a.RequestCodeForCredentialProvider,
+ new Intent(this, typeof(Kp2aCredentialLauncherActivity))
+ .PutExtra(
+ Kp2aCredentialLauncherActivity.CredentialRequestTypeKey,
+ Kp2aCredentialLauncherActivity.CredentialRequestTypeGetPasswordForEntry
+ )
+ .PutExtra(Strings.ExtraEntryId, entry.Uuid.ToHexString()),
+ (int)PendingIntentFlags.UpdateCurrent,
+ true
+ )!,
+ option.JavaCast()
+ )
+ .SetDisplayName(entry.Strings.ReadSafe(PwDefs.TitleField))
+ .SetAffiliatedDomain(entry.ParentGroup?.Name)
+ .SetLastUsedTime(
+ Instant.OfEpochMilli(
+ new DateTimeOffset(entry.LastAccessTime).ToUnixTimeMilliseconds()
+ )
+ )
+ //TODO SetIcon() of the entry
+ .Build()
+ );
+ }
+ }
+ }
+
+ responseBuilder.AddAction(
+ new AndroidX.Credentials.Provider.Action(
+ GetString(Resource.String.open_app_name, GetString(AppNames.AppNameResource)!),
+ PendingIntentCompat.GetActivity(
+ this,
+ App.Kp2a.RequestCodeForCredentialProvider,
+ new Intent(this, typeof(KeePass)),
+ (int)PendingIntentFlags.UpdateCurrent,
+ true
+ )!,
+ GetString(Resource.String.manage_credentials)
+ )
+ );
+
+ callback.OnResult(responseBuilder.Build());
+ }
+
+ public override void OnClearCredentialStateRequest(
+ ProviderClearCredentialStateRequest request,
+ CancellationSignal cancellationSignal,
+ IOutcomeReceiver callback
+ )
+ {
+ Kp2aLog.Log("Kp2aCredentialProviderService:OnClearCredentialStateRequest called");
+ //no-op
+ }
+ }
+}