rename folder keepass2android => keepass2android-app

This commit is contained in:
Philipp Crocoll
2025-01-07 11:20:08 +01:00
parent 738d59dbda
commit 409f6b9981
783 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Android.App.Assist;
using Android.Service.Autofill;
using Android.Views;
using Android.Views.Autofill;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase
{
/// <summary>
/// A stripped down version of a {@link ViewNode} that contains only autofill-relevant metadata. It
/// also contains a {@code mSaveType} flag that is calculated based on the {@link ViewNode}]'s
/// autofill hints.
/// </summary>
public class AutofillFieldMetadata
{
public SaveDataType SaveType { get; set; }
public string[] AutofillCanonicalHints { get; set; }
public AutofillId AutofillId { get; }
public AutofillType AutofillType { get; }
string[] AutofillOptions { get; }
public bool Focused { get; }
public AutofillFieldMetadata(AssistStructure.ViewNode view)
: this(view, view.GetAutofillHints())
{
}
public AutofillFieldMetadata(AssistStructure.ViewNode view, string[] autofillHints)
{
AutofillId = view.AutofillId;
AutofillType = view.AutofillType;
AutofillOptions = view.GetAutofillOptions();
Focused = view.IsFocused;
var supportedHints = AutofillHintsHelper.FilterForSupportedHints(autofillHints);
var canonicalHints = AutofillHintsHelper.ConvertToCanonicalLowerCaseHints(supportedHints);
SetHints(canonicalHints.ToArray());
}
void SetHints(string[] value)
{
AutofillCanonicalHints = value;
UpdateSaveTypeFromHints();
}
/// <summary>
/// When the ViewNode is a list that the user needs to choose a string from (i.e. a
/// spinner), this is called to return the index of a specific item in the list.
/// </summary>
/// <returns>The autofill option index.</returns>
/// <param name="value">Value.</param>
public int GetAutofillOptionIndex(String value)
{
for (int i = 0; i < AutofillOptions.Length; i++)
{
if (AutofillOptions[i].Equals(value))
{
return i;
}
}
return -1;
}
static readonly HashSet<string> _creditCardHints = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
View.AutofillHintCreditCardExpirationDate,
View.AutofillHintCreditCardExpirationDay,
View.AutofillHintCreditCardExpirationMonth,
View.AutofillHintCreditCardExpirationYear,
View.AutofillHintCreditCardNumber,
View.AutofillHintCreditCardSecurityCode
};
static readonly HashSet<string> _addressHints = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
View.AutofillHintPostalAddress,
View.AutofillHintPostalCode
};
void UpdateSaveTypeFromHints()
{
SaveType = 0;
if (AutofillCanonicalHints == null)
{
return;
}
if (AutofillCanonicalHints.Any(h => _creditCardHints.Contains(h)))
{
SaveType |= SaveDataType.CreditCard;
}
if (AutofillCanonicalHints.Any(h => h.Equals(View.AutofillHintEmailAddress, StringComparison.OrdinalIgnoreCase)))
SaveType |= SaveDataType.EmailAddress;
if (AutofillCanonicalHints.Any(h => _addressHints.Contains(h)))
{
SaveType |= SaveDataType.Address;
}
if (AutofillCanonicalHints.Any(h => h.Equals(View.AutofillHintUsername, StringComparison.OrdinalIgnoreCase)))
SaveType |= SaveDataType.Username;
if (AutofillCanonicalHints.Any(h => h.Equals(View.AutofillHintPassword, StringComparison.OrdinalIgnoreCase)))
{
SaveType |= SaveDataType.Password;
SaveType &= ~SaveDataType.EmailAddress;
SaveType &= ~SaveDataType.Username;
}
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Android.Service.Autofill;
using Android.Views.Autofill;
namespace keepass2android.services.AutofillBase
{
/// <summary>
/// Data structure that stores a collection of AutofillFieldMetadatas. Contains all of the
/// client's View hierarchy autofill-relevant metadata.
/// </summary>
public class AutofillFieldMetadataCollection
{
readonly List<AutofillId> AutofillIds = new List<AutofillId>();
readonly Dictionary<string, List<AutofillFieldMetadata>> AutofillCanonicalHintsToFieldsMap = new Dictionary<string, List<AutofillFieldMetadata>>();
public List<string> AllAutofillCanonicalHints { get; }
public List<string> FocusedAutofillCanonicalHints { get; }
int Size = 0;
public SaveDataType SaveType { get; set; }
public bool Empty
{
get { return Size == 0; }
}
public AutofillFieldMetadataCollection()
{
SaveType = 0;
FocusedAutofillCanonicalHints = new List<string>();
AllAutofillCanonicalHints = new List<string>();
}
public void Add(AutofillFieldMetadata autofillFieldMetadata)
{
var hintsList = autofillFieldMetadata.AutofillCanonicalHints;
if (!hintsList.Any())
return;
if (AutofillIds.Contains(autofillFieldMetadata.AutofillId))
return;
SaveType |= autofillFieldMetadata.SaveType;
Size++;
AutofillIds.Add(autofillFieldMetadata.AutofillId);
AllAutofillCanonicalHints.AddRange(hintsList);
if (autofillFieldMetadata.Focused)
{
FocusedAutofillCanonicalHints.AddRange(hintsList);
}
foreach (var hint in hintsList)
{
if (!AutofillCanonicalHintsToFieldsMap.ContainsKey(hint))
{
AutofillCanonicalHintsToFieldsMap.Add(hint, new List<AutofillFieldMetadata>());
}
AutofillCanonicalHintsToFieldsMap[hint].Add(autofillFieldMetadata);
}
}
public AutofillId[] GetAutofillIds()
{
return AutofillIds.ToArray();
}
/// <summary>
/// returns the fields for the given hint or an empty list.
/// </summary>
public List<AutofillFieldMetadata> GetFieldsForHint(String canonicalHint)
{
List<AutofillFieldMetadata> result;
if (!AutofillCanonicalHintsToFieldsMap.TryGetValue(canonicalHint, out result))
{
result = new List<AutofillFieldMetadata>();
}
return result;
}
}
}

View File

@@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Service.Autofill;
using Android.Util;
using Android.Views;
using Android.Views.Autofill;
using Android.Widget;
using Android.Widget.Inline;
using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1;
using keepass2android;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase
{
/// <summary>
/// This is a class containing helper methods for building Autofill Datasets and Responses.
/// </summary>
public class AutofillHelper
{
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;
}
//make sure we have a pendingIntent always not null
pendingIntent ??= PendingIntent.GetService(context, 0, new Intent(),
Util.AddMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, PendingIntentFlags.Mutable));
var slice = CreateInlinePresentationSlice(
inlinePresentationSpec,
text,
subtext,
iconId,
"Autofill option",
pendingIntent,
context);
if (slice != null)
{
return new InlinePresentation(slice, inlinePresentationSpec, false);
}
return null;
}
private static Android.App.Slices.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 = Android.Graphics.Drawables.Icon.CreateWithResource(context, iconId);
if (icon != null)
{
if (iconId == AppNames.LauncherIcon)
{
// Don't tint our logo
icon.SetTintBlendMode(Android.Graphics.BlendMode.Dst);
}
contentBuilder.SetStartIcon(icon);
}
}
return contentBuilder.Build().JavaCast<InlineSuggestionUi.Content>()?.Slice;
}
/// <summary>
/// Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the
/// client View.
/// </summary>
public static Dataset NewDataset(Context context,
AutofillFieldMetadataCollection autofillFields,
FilledAutofillFieldCollection<ViewNodeInputField> filledAutofillFieldCollection,
IAutofillIntentBuilder intentBuilder,
Android.Widget.Inline.InlinePresentationSpec inlinePresentationSpec)
{
var datasetName = filledAutofillFieldCollection.DatasetName ?? "[noname]";
var datasetBuilder = new Dataset.Builder(NewRemoteViews(context.PackageName, datasetName, intentBuilder.AppIconResource));
datasetBuilder.SetId(datasetName);
var setValueAtLeastOnce = ApplyToFields(filledAutofillFieldCollection, autofillFields, datasetBuilder);
AddInlinePresentation(context, inlinePresentationSpec, datasetName, datasetBuilder, intentBuilder.AppIconResource, null);
if (setValueAtLeastOnce)
{
return datasetBuilder.Build();
}
/*else
{
Kp2aLog.Log("Failed to set at least one value. #fields=" + autofillFields.GetAutofillIds().Length + " " + autofillFields.FocusedAutofillCanonicalHints);
}*/
return null;
}
/// <summary>
/// Populates a Dataset.Builder with appropriate values for each AutofillId
/// in a AutofillFieldMetadataCollection.
///
/// In other words, it constructs an autofill Dataset.Builder
/// by applying saved values (from this FilledAutofillFieldCollection)
/// to Views specified in a AutofillFieldMetadataCollection, which represents the current
/// page the user is on.
/// </summary>
/// <returns><c>true</c>, if to fields was applyed, <c>false</c> otherwise.</returns>
/// <param name="filledAutofillFieldCollection"></param>
/// <param name="autofillFieldMetadataCollection">Autofill field metadata collection.</param>
/// <param name="datasetBuilder">Dataset builder.</param>
public static bool ApplyToFields(FilledAutofillFieldCollection<ViewNodeInputField> filledAutofillFieldCollection,
AutofillFieldMetadataCollection autofillFieldMetadataCollection, Dataset.Builder datasetBuilder)
{
bool setValueAtLeastOnce = false;
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
{
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection.GetFieldsForHint(hint))
{
FilledAutofillField filledAutofillField;
if (!filledAutofillFieldCollection.HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))
{
continue;
}
var autofillId = autofillFieldMetadata.AutofillId;
var autofillType = autofillFieldMetadata.AutofillType;
switch (autofillType)
{
case AutofillType.List:
var listValue = autofillFieldMetadata.GetAutofillOptionIndex(filledAutofillField.TextValue);
if (listValue != -1)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForList(listValue));
setValueAtLeastOnce = true;
}
break;
case AutofillType.Date:
var dateValue = filledAutofillField.DateValue;
datasetBuilder.SetValue(autofillId, AutofillValue.ForDate((long)dateValue));
setValueAtLeastOnce = true;
break;
case AutofillType.Text:
var textValue = filledAutofillField.TextValue;
if (textValue != null)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForText(textValue));
setValueAtLeastOnce = true;
}
break;
case AutofillType.Toggle:
var toggleValue = filledAutofillField.ToggleValue;
if (toggleValue != null)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForToggle(toggleValue.Value));
setValueAtLeastOnce = true;
}
break;
default:
Log.Warn(CommonUtil.Tag, "Invalid autofill type - " + autofillType);
break;
}
}
}
/*
if (!setValueAtLeastOnce)
{
Kp2aLog.Log("No value set. Hint keys : " + string.Join(",", HintMap.Keys));
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
{
Kp2aLog.Log("No value set. Hint = " + hint);
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection
.GetFieldsForHint(hint))
{
Kp2aLog.Log("No value set. fieldForHint = " + autofillFieldMetadata.AutofillId.ToString());
FilledAutofillField filledAutofillField;
if (!HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))
{
Kp2aLog.Log("No value set. Hint map does not contain value, " +
(filledAutofillField == null));
continue;
}
Kp2aLog.Log("autofill type=" + autofillFieldMetadata.AutofillType);
}
}
}*/
return setValueAtLeastOnce;
}
public static void AddInlinePresentation(Context context, InlinePresentationSpec inlinePresentationSpec,
string datasetName, Dataset.Builder datasetBuilder, int iconId, PendingIntent pendingIntent)
{
if (inlinePresentationSpec != null)
{
var inlinePresentation = BuildInlinePresentation(inlinePresentationSpec, datasetName, "", iconId, pendingIntent, context);
if (inlinePresentation != null)
datasetBuilder.SetInlinePresentation(inlinePresentation);
}
}
public static RemoteViews NewRemoteViews(string packageName, string remoteViewsText,int drawableId)
{
RemoteViews presentation = new RemoteViews(packageName, Resource.Layout.autofill_service_list_item);
presentation.SetTextViewText(Resource.Id.text, remoteViewsText);
presentation.SetImageViewResource(Resource.Id.icon, drawableId);
return presentation;
}
internal static InlinePresentationSpec ExtractSpec(IList<InlinePresentationSpec> inlinePresentationSpecs, int index)
{
return inlinePresentationSpecs == null ? null : inlinePresentationSpecs[Math.Min(index, inlinePresentationSpecs.Count - 1)];
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Util;
using Android.Views;
using Android.Widget;
using keepass2android.services.AutofillBase.model;
namespace keepass2android.services.AutofillBase
{
}

View File

@@ -0,0 +1,480 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Android.App;
using Android.App.Slices;
using Android.Content;
using Android.Content.PM;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Preferences;
using Android.Runtime;
using Android.Service.Autofill;
using Android.Util;
using Android.Views.Autofill;
using Android.Views.InputMethods;
using Android.Widget;
using Android.Widget.Inline;
using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1;
using Java.Util.Concurrent.Atomic;
using keepass2android.services.AutofillBase.model;
using keepass2android;
using KeePassLib;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase
{
public interface IAutofillIntentBuilder
{
PendingIntent GetAuthPendingIntentForResponse(Context context, string query, string queryDomain, string queryPackage,
bool isManualRequest, bool autoReturnFromQuery, AutofillServiceBase.DisplayWarning warning);
PendingIntent GetAuthPendingIntentForWarning(Context context, PwUuid entryUuid, AutofillServiceBase.DisplayWarning warning);
PendingIntent GetDisablePendingIntentForResponse(Context context, string query,
bool isManualRequest, bool isDisable);
Intent GetRestartAppIntent(Context context);
int AppIconResource { get; }
}
public abstract class AutofillServiceBase: AutofillService
{
private HashSet<string> _internal_blacklistedUris = null;
public HashSet<string> BlacklistedUris
{
get
{
if (_internal_blacklistedUris == null)
{
_internal_blacklistedUris = new HashSet<string>()
{
KeePass.AndroidAppScheme + "android",
KeePass.AndroidAppScheme + "com.android.settings",
KeePass.AndroidAppScheme + this.PackageName
};
}
return _internal_blacklistedUris;
}
}
protected override void AttachBaseContext(Context baseContext)
{
base.AttachBaseContext(LocaleManager.setLocale(baseContext));
}
//use a lock to avoid returning a response several times in buggy Firefox during one connection: this avoids flickering
//and disappearing of the autofill prompt.
//Instead of using a boolean lock, we use a "time-out lock" which is cleared after a few seconds
private DateTime _lockTime = DateTime.MinValue;
private TimeSpan _lockTimeout = TimeSpan.FromSeconds(2);
public AutofillServiceBase()
{
}
public AutofillServiceBase(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
public static HashSet<string> CompatBrowsers = new HashSet<string>
{
"org.mozilla.firefox",
"org.mozilla.firefox_beta",
"com.microsoft.emmx",
"com.android.chrome",
"com.chrome.beta",
"com.android.browser",
"com.brave.browser",
"com.opera.browser",
"com.opera.browser.beta",
"com.opera.mini.native",
"com.chrome.dev",
"com.chrome.canary",
"com.google.android.apps.chrome",
"com.google.android.apps.chrome_dev",
"com.yandex.browser",
"com.sec.android.app.sbrowser",
"com.sec.android.app.sbrowser.beta",
"org.codeaurora.swe.browser",
"com.amazon.cloud9",
"mark.via.gp",
"org.bromite.bromite",
"org.chromium.chrome",
"com.kiwibrowser.browser",
"com.ecosia.android",
"com.opera.mini.native.beta",
"org.mozilla.fennec_aurora",
"org.mozilla.fennec_fdroid",
"com.qwant.liberty",
"com.opera.touch",
"org.mozilla.fenix",
"org.mozilla.fenix.nightly",
"org.mozilla.reference.browser",
"org.mozilla.rocket",
"org.torproject.torbrowser",
"com.vivaldi.browser",
};
public override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback)
{
bool isManual = (request.Flags & FillRequest.FlagManualRequest) != 0;
CommonUtil.logd( "onFillRequest " + (isManual ? "manual" : "auto"));
var structure = request.FillContexts.Last().Structure;
if (_lockTime + _lockTimeout < DateTime.Now)
{
_lockTime = DateTime.Now;
//TODO support package signature verification as soon as this is supported in Keepass storage
var clientState = request.ClientState;
CommonUtil.logd("onFillRequest(): data=" + CommonUtil.BundleToString(clientState));
cancellationSignal.CancelEvent += (sender, e) =>
{
Kp2aLog.Log("Cancel autofill not implemented yet.");
_lockTime = DateTime.MinValue;
};
// Parse AutoFill data in Activity
StructureParser.AutofillTargetId query = null;
var parser = new StructureParser(this, structure);
try
{
query = parser.ParseForFill(isManual);
}
catch (Java.Lang.SecurityException e)
{
Log.Warn(CommonUtil.Tag, "Security exception handling request");
callback.OnFailure(e.Message);
return;
}
InlineSuggestionsRequest inlineSuggestionsRequest = null;
IList<InlinePresentationSpec> inlinePresentationSpecs = null;
if (((int) Build.VERSION.SdkInt >= 30)
&& (PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(GetString(Resource.String.InlineSuggestions_key), true)))
{
inlineSuggestionsRequest = request.InlineSuggestionsRequest;
inlinePresentationSpecs = inlineSuggestionsRequest?.InlinePresentationSpecs;
}
var autofillIds = parser.AutofillFields.GetAutofillIds();
if (autofillIds.Length != 0 && CanAutofill(query, isManual))
{
var responseBuilder = new FillResponse.Builder();
bool hasEntryDataset = false;
IList<Dataset> entryDatasets = new List<Dataset>();
if (query.IncompatiblePackageAndDomain == false)
{
Kp2aLog.Log("AF: (query.IncompatiblePackageAndDomain == false)");
//domain and package are compatible. Use Domain if available and package otherwise. Can fill without warning.
entryDatasets = BuildEntryDatasets(query.DomainOrPackage, query.WebDomain,
query.PackageName,
autofillIds, parser, DisplayWarning.None,
inlinePresentationSpecs
).Where(ds => ds != null).ToList();
if (entryDatasets.Count > inlineSuggestionsRequest?.MaxSuggestionCount - 2 /*disable dataset and query*/)
{
//we have too many elements. disable inline suggestions
inlinePresentationSpecs = null;
entryDatasets = BuildEntryDatasets(query.DomainOrPackage, query.WebDomain,
query.PackageName,
autofillIds, parser, DisplayWarning.None,
null
).Where(ds => ds != null).ToList();
}
foreach (var entryDataset in entryDatasets
)
{
Kp2aLog.Log("AF: Got EntryDataset " + (entryDataset == null));
responseBuilder.AddDataset(entryDataset);
hasEntryDataset = true;
}
}
{
if (query.WebDomain != null)
AddQueryDataset(query.WebDomain,
query.WebDomain, query.PackageName,
isManual, autofillIds, responseBuilder, !hasEntryDataset,
query.IncompatiblePackageAndDomain
? DisplayWarning.FillDomainInUntrustedApp
: DisplayWarning.None,
AutofillHelper.ExtractSpec(inlinePresentationSpecs, entryDatasets.Count));
else
AddQueryDataset(query.PackageNameWithPseudoSchema,
query.WebDomain, query.PackageName,
isManual, autofillIds, responseBuilder, !hasEntryDataset, DisplayWarning.None,
AutofillHelper.ExtractSpec(inlinePresentationSpecs, entryDatasets.Count));
}
if (!PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean(GetString(Resource.String.NoAutofillDisabling_key), false))
AddDisableDataset(query.DomainOrPackage, autofillIds, responseBuilder, isManual, AutofillHelper.ExtractSpec(inlinePresentationSpecs, entryDatasets.Count));
if (PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean(GetString(Resource.String.OfferSaveCredentials_key), true))
{
if (!CompatBrowsers.Contains(parser.PackageId))
{
responseBuilder.SetSaveInfo(new SaveInfo.Builder(parser.AutofillFields.SaveType,
parser.AutofillFields.GetAutofillIds()).Build());
}
}
Kp2aLog.Log("return autofill success");
callback.OnSuccess(responseBuilder.Build());
}
else
{
Kp2aLog.Log("cannot autofill");
callback.OnSuccess(null);
}
}
else
{
Kp2aLog.Log("Ignoring onFillRequest as there is another request going on.");
}
}
private List<Dataset> BuildEntryDatasets(string query, string queryDomain, string queryPackage, AutofillId[] autofillIds, StructureParser parser,
DisplayWarning warning, IList<InlinePresentationSpec> inlinePresentationSpecs)
{
List<Dataset> result = new List<Dataset>();
Kp2aLog.Log("AF: BuildEntryDatasets");
Dictionary<PwEntryOutput, FilledAutofillFieldCollection<ViewNodeInputField>> suggestedEntries = GetSuggestedEntries(query);
Kp2aLog.Log("AF: BuildEntryDatasets found " + suggestedEntries.Count + " entries");
int count = 0;
var totpHelper = new Kp2aTotp();
foreach (var kvp in suggestedEntries)
{
var filledAutofillFieldCollection = kvp.Value;
PwEntryOutput entry = kvp.Key;
if (filledAutofillFieldCollection == null)
continue;
var inlinePresentationSpec = AutofillHelper.ExtractSpec(inlinePresentationSpecs, count);
if ((warning == DisplayWarning.None)
&& (totpHelper.TryGetAdapter(entry) == null))
{
//no special dataset, we can immediately return the field collection
FilledAutofillFieldCollection<ViewNodeInputField> partitionData =
AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, parser.AutofillFields.FocusedAutofillCanonicalHints);
Kp2aLog.Log("AF: Add dataset");
result.Add(AutofillHelper.NewDataset(this, parser.AutofillFields, partitionData, IntentBuilder,
inlinePresentationSpec));
}
else
{
//return an "auth" dataset (actually for just warning the user in case domain/package dont match and/or to make sure that we open the EntryActivity,
// thus opening the entry notification in case of TOTP)
PendingIntent pendingIntent =
IntentBuilder.GetAuthPendingIntentForWarning(this, entry.Uuid, warning);
var datasetName = filledAutofillFieldCollection.DatasetName;
if (datasetName == null)
{
Kp2aLog.Log("AF: dataset name is null");
continue;
}
RemoteViews presentation =
AutofillHelper.NewRemoteViews(PackageName, datasetName, AppNames.LauncherIcon);
var datasetBuilder = new Dataset.Builder(presentation);
datasetBuilder.SetAuthentication(pendingIntent?.IntentSender);
AutofillHelper.AddInlinePresentation(this, inlinePresentationSpec, datasetName, datasetBuilder, AppNames.LauncherIcon, null);
//need to add placeholders so we can directly fill after ChooseActivity
foreach (var autofillId in autofillIds)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));
}
Kp2aLog.Log("AF: Add auth dataset");
result.Add(datasetBuilder.Build());
}
count++;
}
return result;
}
protected abstract Dictionary<PwEntryOutput, FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query);
public enum DisplayWarning
{
None,
FillDomainInUntrustedApp, //display a warning that the user is filling credentials for a domain inside an app not marked as trusted browser
}
private void AddQueryDataset(string query, string queryDomain, string queryPackage, bool isManual, AutofillId[] autofillIds, FillResponse.Builder responseBuilder, bool autoReturnFromQuery, DisplayWarning warning, InlinePresentationSpec inlinePresentationSpec)
{
PendingIntent pendingIntent = IntentBuilder.GetAuthPendingIntentForResponse(this, query, queryDomain, queryPackage, isManual, autoReturnFromQuery, warning);
string text = GetString(Resource.String.autofill_sign_in_prompt);
RemoteViews overlayPresentation = AutofillHelper.NewRemoteViews(base.PackageName,
text, AppNames.LauncherIcon);
var datasetBuilder = new Dataset.Builder(overlayPresentation);
datasetBuilder.SetAuthentication(pendingIntent?.IntentSender);
//need to add placeholders so we can directly fill after ChooseActivity
foreach (var autofillId in autofillIds)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));
}
AutofillHelper.AddInlinePresentation(this, inlinePresentationSpec, text, datasetBuilder, AppNames.LauncherIcon, pendingIntent);
responseBuilder.AddDataset(datasetBuilder.Build());
}
public static string GetDisplayNameForQuery(string str, Context Context)
{
string displayName = str;
try
{
string appPrefix = KeePass.AndroidAppScheme;
if (str.StartsWith(appPrefix))
{
str = str.Substring(appPrefix.Length);
PackageManager pm = Context.PackageManager;
ApplicationInfo ai;
try
{
ai = pm.GetApplicationInfo(str, 0);
}
catch (PackageManager.NameNotFoundException e)
{
ai = null;
}
displayName = ai != null ? pm.GetApplicationLabel(ai) : str;
}
}
catch (Exception e)
{
Kp2aLog.LogUnexpectedError(e);
}
return displayName;
}
private void AddDisableDataset(string query, AutofillId[] autofillIds, FillResponse.Builder responseBuilder, bool isManual, InlinePresentationSpec inlinePresentationSpec)
{
bool isQueryDisabled = IsQueryDisabled(query);
if (isQueryDisabled && !isManual)
return;
bool isForDisable = !isQueryDisabled;
var pendingIntent = IntentBuilder.GetDisablePendingIntentForResponse(this, query, isManual, isForDisable);
string text = GetString(isForDisable ? Resource.String.autofill_disable : Resource.String.autofill_enable_for, new Java.Lang.Object[] { GetDisplayNameForQuery(query, this) });
RemoteViews presentation = AutofillHelper.NewRemoteViews(base.PackageName,
text, Resource.Drawable.baseline_close_24);
var datasetBuilder = new Dataset.Builder(presentation);
datasetBuilder.SetAuthentication(pendingIntent?.IntentSender);
AutofillHelper.AddInlinePresentation(this, inlinePresentationSpec, text, datasetBuilder, Resource.Drawable.baseline_close_24, null);
foreach (var autofillId in autofillIds)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));
}
responseBuilder.AddDataset(datasetBuilder.Build());
}
private bool CanAutofill(StructureParser.AutofillTargetId query, bool isManual)
{
if (BlacklistedUris.Contains(query.PackageNameWithPseudoSchema))
return false;
if (!isManual)
{
var isQueryDisabled = IsQueryDisabled(query.DomainOrPackage);
if (isQueryDisabled)
return false;
}
return true;
}
private bool IsQueryDisabled(string query)
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(this);
var disabledValues = prefs.GetStringSet("AutoFillDisabledQueries", new List<string>());
bool isQueryDisabled = disabledValues.Contains(query);
return isQueryDisabled;
}
public override void OnSaveRequest(SaveRequest request, SaveCallback callback)
{
var structure = request.FillContexts?.LastOrDefault()?.Structure;
if (structure == null)
{
return;
}
var parser = new StructureParser(this, structure);
var query = parser.ParseForSave();
try
{
HandleSaveRequest(parser, query);
callback.OnSuccess();
}
catch (Exception e)
{
callback.OnFailure(e.Message);
}
}
protected abstract void HandleSaveRequest(StructureParser parser, StructureParser.AutofillTargetId query);
public override void OnConnected()
{
CommonUtil.logd( "onConnected");
}
public override void OnDisconnected()
{
_lockTime = DateTime.MinValue;
CommonUtil.logd( "onDisconnected");
}
public abstract IAutofillIntentBuilder IntentBuilder{get;}
}
}

View File

@@ -0,0 +1,264 @@
using System;
using Android.App;
using Android.App.Assist;
using Android.Content;
using Android.OS;
using Android.Service.Autofill;
using Android.Util;
using Android.Views.Autofill;
using Android.Widget;
using Java.Util;
using keepass2android.services.AutofillBase.model;
using System.Linq;
using Android.Content.PM;
using Google.Android.Material.Dialog;
using keepass2android;
using Kp2aAutofillParser;
using AlertDialog = Android.App.AlertDialog;
namespace keepass2android.services.AutofillBase
{
public abstract class ChooseForAutofillActivityBase : AndroidX.AppCompat.App.AppCompatActivity
{
protected Intent ReplyIntent;
public static string ExtraUuidString => "EXTRA_UUID_STRING";
public static string ExtraQueryString => "EXTRA_QUERY_STRING";
public static string ExtraQueryPackageString => "EXTRA_QUERY_PACKAGE_STRING";
public static string ExtraQueryDomainString => "EXTRA_QUERY_DOMAIN_STRING";
public static string ExtraUseLastOpenedEntry => "EXTRA_USE_LAST_OPENED_ENTRY"; //if set to true, no query UI is displayed. Can be used to just show a warning
public static string ExtraIsManualRequest => "EXTRA_IS_MANUAL_REQUEST";
public static string ExtraAutoReturnFromQuery => "EXTRA_AUTO_RETURN_FROM_QUERY";
public static string ExtraDisplayWarning => "EXTRA_DISPLAY_WARNING";
public int RequestCodeQuery => 6245;
protected override void OnCreate(Bundle savedInstanceState)
{
Kp2aLog.Log("ChooseForAutofillActivityBase.OnCreate");
base.OnCreate(savedInstanceState);
//if launched from history, don't re-use the task. Proceed to FileSelect instead.
if (Intent.Flags.HasFlag(ActivityFlags.LaunchedFromHistory))
{
Kp2aLog.Log("ChooseForAutofillActivityBase: started from history");
Kp2aLog.Log("Forwarding to FileSelect. QueryCredentialsActivity started from history.");
RestartApp();
return;
}
string requestedUrl = Intent.GetStringExtra(ExtraQueryString);
string requestedUuid = Intent.GetStringExtra(ExtraUuidString);
if (requestedUrl == null && requestedUuid == null)
{
Kp2aLog.Log("ChooseForAutofillActivityBase: no requestedUrl and no requestedUuid");
Toast.MakeText(this, "Cannot execute query for null.", ToastLength.Long).Show();
RestartApp();
return;
}
if (Intent.HasExtra(ExtraDisplayWarning))
{
AutofillServiceBase.DisplayWarning warning =
(AutofillServiceBase.DisplayWarning)Intent.GetIntExtra(ExtraDisplayWarning, (int)AutofillServiceBase.DisplayWarning.None);
Kp2aLog.Log("ChooseForAutofillActivityBase: ExtraDisplayWarning = " + warning);
if (warning != AutofillServiceBase.DisplayWarning.None)
{
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.SetTitle(this.GetString(Resource.String.AutofillWarning_title));
string appName = Intent.GetStringExtra(ExtraQueryPackageString);
string appNameWithPackage = appName;
try
{
var appInfo = PackageManager.GetApplicationInfo(appName, 0);
if (appInfo != null)
{
appName = PackageManager.GetApplicationLabel(appInfo);
appNameWithPackage = appName + " (" + Intent.GetStringExtra(ExtraQueryPackageString) + ")";
}
}
catch (Exception)
{
// ignored
}
builder.SetMessage(
GetString(Resource.String.AutofillWarning_Intro, new Java.Lang.Object[]
{
Intent.GetStringExtra(ExtraQueryDomainString), appNameWithPackage
})
+ " " +
this.GetString(Resource.String.AutofillWarning_FillDomainInUntrustedApp, new Java.Lang.Object[]
{
Intent.GetStringExtra(ExtraQueryDomainString), appName
}));
builder.SetPositiveButton(this.GetString(Resource.String.Continue),
(dlgSender, dlgEvt) =>
{
new Kp2aDigitalAssetLinksDataSource(this).RememberTrustedLink(Intent.GetStringExtra(ExtraQueryDomainString),
Intent.GetStringExtra(ExtraQueryPackageString));
Proceed();
});
builder.SetNeutralButton(this.GetString(Resource.String.AutofillWarning_trustAsBrowser, new Java.Lang.Object[]
{appName}),
(sender, args) =>
{
new Kp2aDigitalAssetLinksDataSource(this).RememberAsTrustedApp(Intent.GetStringExtra(ExtraQueryPackageString));
Proceed();
});
builder.SetNegativeButton(this.GetString(Resource.String.cancel), (dlgSender, dlgEvt) =>
{
Finish();
});
Dialog dialog = builder.Create();
dialog.Show();
return;
}
}
else Kp2aLog.Log("ChooseForAutofillActivityBase: No ExtraDisplayWarning");
Proceed();
}
private void Proceed()
{
string requestedUrl = Intent.GetStringExtra(ExtraQueryString);
string requestedUuid = Intent.GetStringExtra(ExtraUuidString);
if (requestedUuid != null)
{
var i = GetOpenEntryIntent(requestedUuid);
StartActivityForResult(i, RequestCodeQuery);
}
else
{
var i = GetQueryIntent(requestedUrl, Intent.GetBooleanExtra(ExtraAutoReturnFromQuery, true), Intent.GetBooleanExtra(ExtraUseLastOpenedEntry, false));
if (i == null)
{
//GetQueryIntent returns null if no query is required
ReturnSuccess();
}
else
StartActivityForResult(i, RequestCodeQuery);
}
}
protected abstract Intent GetQueryIntent(string requestedUrl, bool autoReturnFromQuery, bool useLastOpenedEntry);
protected abstract Intent GetOpenEntryIntent(string entryUuid);
protected void RestartApp()
{
Intent intent = IntentBuilder.GetRestartAppIntent(this);
StartActivity(intent);
Finish();
}
public override void Finish()
{
if (ReplyIntent != null)
{
SetResult(Result.Ok, ReplyIntent);
}
else
{
SetResult(Result.Canceled);
}
base.Finish();
}
void OnFailure()
{
Log.Warn(CommonUtil.Tag, "Failed auth.");
ReplyIntent = null;
}
protected void OnSuccess(FilledAutofillFieldCollection<ViewNodeInputField> clientFormDataMap, bool isManual)
{
var intent = Intent;
AssistStructure structure = (AssistStructure)intent.GetParcelableExtra(AutofillManager.ExtraAssistStructure);
if (structure == null)
{
SetResult(Result.Canceled);
Finish();
return;
}
StructureParser parser = new StructureParser(this, structure);
parser.ParseForFill(isManual);
AutofillFieldMetadataCollection autofillFields = parser.AutofillFields;
var partitionData = AutofillHintsHelper.FilterForPartition(clientFormDataMap, parser.AutofillFields.FocusedAutofillCanonicalHints);
ReplyIntent = new Intent();
SetDatasetIntent(AutofillHelper.NewDataset(this, autofillFields, partitionData, IntentBuilder, null /*TODO can we get the inlinePresentationSpec here?*/));
SetResult(Result.Ok, ReplyIntent);
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (requestCode == RequestCodeQuery)
{
if (resultCode == ExpectedActivityResult)
ReturnSuccess();
else
{
OnFailure();
Finish();
}
}
}
private void ReturnSuccess()
{
OnSuccess(GetDataset(), Intent.GetBooleanExtra(ExtraIsManualRequest, false));
Finish();
}
protected virtual Result ExpectedActivityResult
{
get { return Result.Ok; }
}
/// <summary>
/// Creates the FilledAutofillFieldCollection from the intent returned from the query activity
/// </summary>
protected abstract FilledAutofillFieldCollection<ViewNodeInputField> GetDataset();
public abstract IAutofillIntentBuilder IntentBuilder { get; }
protected void SetResponseIntent(FillResponse fillResponse)
{
ReplyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, fillResponse);
}
protected void SetDatasetIntent(Dataset dataset)
{
if (dataset == null)
{
Toast.MakeText(this, "Failed to build an autofill dataset.", ToastLength.Long).Show();
return;
}
ReplyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Text;
using Android.OS;
using Android.Util;
using Java.Util;
namespace keepass2android.services.AutofillBase
{
public class CommonUtil
{
public const string Tag = "Kp2aAutofill";
public const bool Debug = true;
static void BundleToString(StringBuilder builder, Bundle data)
{
var keySet = data.KeySet();
builder.Append("[Bundle with ").Append(keySet.Count).Append(" keys:");
foreach (var key in keySet)
{
builder.Append(' ').Append(key).Append('=');
Object value = data.Get(key);
if (value is Bundle)
{
BundleToString(builder, (Bundle)value);
}
else
{
builder.Append((value is Object[])
? Arrays.ToString((bool[])value) : value);
}
}
builder.Append(']');
}
public static string BundleToString(Bundle data)
{
if (data == null)
{
return "N/A";
}
StringBuilder builder = new StringBuilder();
BundleToString(builder, data);
return builder.ToString();
}
public static void logd(string s)
{
#if DEBUG
Log.Debug(Tag, s);
#endif
}
public static void loge(string s)
{
Kp2aLog.Log(s);
}
}
}

View File

@@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.Content.Res;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
namespace DomainNameParser
{
using System.Collections.Generic;
using System.Linq;
using System;
public class DomainName
{
public DomainName(string rawDomainName, string publicSuffix, string registerableDomainName)
{
this.RawDomainName = rawDomainName;
this.PublicSuffix = publicSuffix;
this.RegisterableDomainName = registerableDomainName;
}
public string RawDomainName { get; private set; }
public string PublicSuffix { get; private set; }
public string RegisterableDomainName { get; private set; }
public static bool TryParse(string rawDomainName, PublicSuffixRuleCache ruleCache, out DomainName domainName)
{
if (string.IsNullOrEmpty(rawDomainName) || !rawDomainName.Contains('.') || rawDomainName.StartsWith("."))
{
domainName = new DomainName(rawDomainName, null, null);
return true;
}
try
{
rawDomainName = rawDomainName.ToLower();
// Split our domain into parts (based on the '.')
// We'll be checking rules from the right-most part of the domain
var domainLabels = rawDomainName.Trim().Split('.').ToList();
domainLabels.Reverse();
// If no rules match, the prevailing rule is "*"
var prevailingRule = FindMatchingRule(domainLabels, ruleCache) ?? new PublicSuffixRule("*");
// If the prevailing rule is an exception rule, modify it by removing the leftmost label.
if (prevailingRule.Type == PublicSuffixRule.RuleType.Exception)
{
var labels = prevailingRule.Labels;
labels.Reverse();
labels.RemoveAt(0);
prevailingRule = new PublicSuffixRule(string.Join(".", labels));
}
// The public suffix is the set of labels from the domain which directly match the labels of the prevailing rule (joined by dots).
var publicSuffix = Enumerable.Range(0, prevailingRule.Labels.Count).Aggregate(string.Empty, (current, i) => string.Format("{0}.{1}", domainLabels[i], current).Trim('.'));
// The registered or registrable domain is the public suffix plus one additional label.
var registrableDomain = string.Format("{0}.{1}", domainLabels[prevailingRule.Labels.Count], publicSuffix);
domainName = new DomainName(rawDomainName, publicSuffix, registrableDomain);
return true;
}
catch
{
domainName = null;
return false;
}
}
private static PublicSuffixRule FindMatchingRule(List<string> domainLabels, PublicSuffixRuleCache ruleCache)
{
var ruleMatches = ruleCache.PublicSuffixRules.Where(r => r.AppliesTo(domainLabels)).ToList();
// If there is only one match, return it.
if (ruleMatches.Count() == 1)
{
return ruleMatches[0];
}
// If more than one rule matches, the prevailing rule is the one which is an exception rule.
var exceptionRules = ruleMatches.Where(r => r.Type == PublicSuffixRule.RuleType.Exception).ToList();
if (exceptionRules.Count() == 1)
{
return exceptionRules[0];
}
if (exceptionRules.Count() > 1)
{
throw new ApplicationException("Unexpectedly found multiple matching exception rules.");
}
// If there is no matching exception rule, the prevailing rule is the one with the most labels.
var prevailingRule = ruleMatches.OrderByDescending(r => r.Labels.Count).Take(1).SingleOrDefault();
return prevailingRule;
}
}
public class PublicSuffixRule
{
/// <summary>
/// Construct a rule based on a single line from the www.publicsuffix.org list
/// </summary>
/// <param name="ruleLine">The rule line.</param>
public PublicSuffixRule(string ruleLine)
{
if (string.IsNullOrEmpty(ruleLine) || string.IsNullOrWhiteSpace(ruleLine))
{
throw new ArgumentNullException("ruleLine");
}
// Parse the rule and set properties accordingly:
string ruleName;
if (ruleLine.StartsWith("*", StringComparison.InvariantCultureIgnoreCase))
{
this.Type = RuleType.Wildcard;
ruleName = ruleLine;
}
else if (ruleLine.StartsWith("!", StringComparison.InvariantCultureIgnoreCase))
{
this.Type = RuleType.Exception;
ruleName = ruleLine.Substring(1);
}
else
{
this.Type = RuleType.Normal;
ruleName = ruleLine;
}
this.Name = ruleName.Split(' ')[0];
var labels = this.Name.Split('.').ToList();
labels.Reverse();
this.Labels = labels;
}
public string Name { get; private set; }
public RuleType Type { get; private set; }
public List<string> Labels { get; private set; }
public bool AppliesTo(List<string> domainLabels)
{
if (this.Labels.Count > domainLabels.Count)
{
return false;
}
foreach (var position in Enumerable.Range(0, this.Labels.Count))
{
if (this.Labels[position] == "*")
{
return true;
}
if (this.Labels[position] != domainLabels[position])
{
return false;
}
}
return true;
}
/// <summary>
/// Rule type
/// </summary>
public enum RuleType
{
/// <summary>
/// A normal rule
/// </summary>
Normal,
/// <summary>
/// A wildcard rule, as defined by www.publicsuffix.org
/// </summary>
Wildcard,
/// <summary>
/// An exception rule, as defined by www.publicsuffix.org
/// </summary>
Exception
}
}
public class PublicSuffixRuleCache
{
static IEnumerable<string> ReadLines(StreamReader reader,
Encoding encoding)
{
string line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
public PublicSuffixRuleCache(Context context)
{
this.PublicSuffixRules = GetRules(context);
}
public PublicSuffixRuleCache(IEnumerable<string> publicSuffixRules)
{
this.PublicSuffixRules = GetRules(publicSuffixRules);
}
public List<PublicSuffixRule> PublicSuffixRules { get; private set; }
/// <summary>
/// Gets the list of TLD rules from the cache
/// </summary>
/// <returns></returns>
private static List<PublicSuffixRule> GetRules(Context context)
{
AssetManager assets = context.Assets;
using (StreamReader sr = new StreamReader(assets.Open("publicsuffix.txt")))
{
var ruleStrings = ReadLines(sr, Encoding.UTF8).ToList();
return GetRules(ruleStrings);
}
}
private static List<PublicSuffixRule> GetRules(IEnumerable<string> publicSuffixRules)
{
// Strip out any lines that are a comment or blank.
return
publicSuffixRules.Where(
ruleString =>
ruleString.Trim().Length != 0
&& !ruleString.StartsWith("//", StringComparison.InvariantCultureIgnoreCase)).Select(ruleString => new PublicSuffixRule(ruleString)).ToList();
}
}
}

View File

@@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Linq;
using Android.Content;
using Android.Preferences;
using keepass2android;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase
{
internal class Kp2aDigitalAssetLinksDataSource : IKp2aDigitalAssetLinksDataSource
{
private const string Autofilltrustedapps = "AutoFillTrustedApps";
private readonly Context _ctx;
public Kp2aDigitalAssetLinksDataSource(Context ctx)
{
_ctx = ctx;
}
public bool IsTrustedApp(string packageName)
{
if (_trustedBrowsers.Contains(packageName))
return true;
var prefs = PreferenceManager.GetDefaultSharedPreferences(_ctx);
var trustedApps = prefs.GetStringSet(Autofilltrustedapps, new List<string>()).ToHashSet();
return trustedApps.Contains(packageName);
}
public bool IsTrustedLink(string domain, string targetPackage)
{
//we can fill everything into trusted apps (aka browsers)
if (IsTrustedApp(targetPackage))
return true;
//see if the user explicitly allows to fill credentials for domain into targetPackage:
var prefs = PreferenceManager.GetDefaultSharedPreferences(_ctx);
var trustedLinks = prefs.GetStringSet("AutoFillTrustedLinks", new List<string>()).ToHashSet();
return trustedLinks.Contains(BuildLink(domain, targetPackage));
}
public bool IsEnabled()
{
return !PreferenceManager.GetDefaultSharedPreferences(_ctx).GetBoolean(_ctx.GetString(Resource.String.NoDalVerification_key), false);
}
public void RememberAsTrustedApp(string packageName)
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(_ctx);
var trustedApps = prefs.GetStringSet(Autofilltrustedapps, new List<string>()).ToHashSet();
trustedApps.Add(packageName);
prefs.Edit().PutStringSet(Autofilltrustedapps, trustedApps).Commit();
}
public void RememberTrustedLink(string domain, string package)
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(_ctx);
var trustedLinks = prefs.GetStringSet("AutoFillTrustedLinks", new List<string>()).ToHashSet();
trustedLinks.Add(BuildLink(domain, package));
prefs.Edit().PutStringSet("AutoFillTrustedLinks", trustedLinks).Commit();
}
private static string BuildLink(string domain, string package)
{
return domain + " + " + package;
}
static readonly HashSet<string> _trustedBrowsers = new HashSet<string>
{
"org.mozilla.firefox","org.mozilla.firefox_beta","org.mozilla.klar","org.mozilla.focus",
"org.mozilla.fenix","org.mozilla.fenix.nightly","org.mozilla.reference.browser",
"com.android.browser","com.android.chrome","com.chrome.beta","com.chrome.dev","com.chrome.canary",
"com.google.android.apps.chrome","com.google.android.apps.chrome_dev",
"com.opera.browser","com.opera.browser.beta","com.opera.mini.native","com.opera.mini.native.beta","com.opera.touch",
"com.brave.browser","com.yandex.browser","com.microsoft.emmx","com.amazon.cloud9",
"com.sec.android.app.sbrowser","com.sec.android.app.sbrowser.beta","org.codeaurora.swe.browser",
"mark.via.gp","org.bromite.bromite", "org.mozilla.fennec_fdroid", "com.vivaldi.browser","com.kiwibrowser.browser",
"acr.browser.lightning", "acr.browser.barebones", "jp.hazuki.yuzubrowser"
};
}
}

View File

@@ -0,0 +1,207 @@
using System;
using System.Linq;
using Android.App.Assist;
using Android.Content;
using Android.Preferences;
using Android.Views.Autofill;
using DomainNameParser;
using keepass2android;
using Kp2aAutofillParser;
using Newtonsoft.Json;
namespace keepass2android.services.AutofillBase
{
public class ViewNodeInputField : Kp2aAutofillParser.InputField
{
public ViewNodeInputField(AssistStructure.ViewNode viewNode)
{
ViewNode = viewNode;
IdEntry = viewNode.IdEntry;
Hint = viewNode.Hint;
ClassName = viewNode.ClassName;
AutofillHints = viewNode.GetAutofillHints();
IsFocused = viewNode.IsFocused;
InputType = (Kp2aAutofillParser.InputTypes) ((int)viewNode.InputType);
HtmlInfoTag = viewNode.HtmlInfo?.Tag;
HtmlInfoTypeAttribute = viewNode.HtmlInfo?.Attributes?.FirstOrDefault(p => p.First?.ToString() == "type")?.Second?.ToString();
}
[JsonIgnore]
public AssistStructure.ViewNode ViewNode { get; set; }
public override void FillFilledAutofillValue(FilledAutofillField filledField)
{
AutofillValue autofillValue = ViewNode.AutofillValue;
if (autofillValue != null)
{
if (autofillValue.IsList)
{
string[] autofillOptions = ViewNode.GetAutofillOptions();
int index = autofillValue.ListValue;
if (autofillOptions != null && autofillOptions.Length > 0)
{
filledField.TextValue = autofillOptions[index];
}
}
else if (autofillValue.IsDate)
{
filledField.DateValue = autofillValue.DateValue;
}
else if (autofillValue.IsText)
{
filledField.TextValue = autofillValue.TextValue;
}
}
}
}
/// <summary>
/// Converts an AssistStructure into a list of InputFields
/// </summary>
class AutofillViewFromAssistStructureFinder
{
private readonly Context _context;
private readonly AssistStructure _structure;
private PublicSuffixRuleCache domainSuffixParserCache;
public AutofillViewFromAssistStructureFinder(Context context, AssistStructure structure)
{
_context = context;
_structure = structure;
domainSuffixParserCache = new PublicSuffixRuleCache(context);
}
public AutofillView<ViewNodeInputField> GetAutofillView(bool isManualRequest)
{
AutofillView<ViewNodeInputField> autofillView = new AutofillView<ViewNodeInputField>();
int nodeCount = _structure.WindowNodeCount;
for (int i = 0; i < nodeCount; i++)
{
var node = _structure.GetWindowNodeAt(i);
var view = node.RootViewNode;
ParseRecursive(autofillView, view, isManualRequest);
}
return autofillView;
}
void ParseRecursive(AutofillView<ViewNodeInputField> autofillView, AssistStructure.ViewNode viewNode, bool isManualRequest)
{
String webDomain = viewNode.WebDomain;
if ((autofillView.PackageId == null) && (!string.IsNullOrWhiteSpace(viewNode.IdPackage)) &&
(viewNode.IdPackage != "android"))
{
autofillView.PackageId = viewNode.IdPackage;
}
DomainName outDomain;
if (DomainName.TryParse(webDomain, domainSuffixParserCache, out outDomain))
{
webDomain = outDomain.RawDomainName;
}
if (webDomain != null)
{
if (!string.IsNullOrEmpty(autofillView.WebDomain))
{
if (webDomain != autofillView.WebDomain)
{
throw new Java.Lang.SecurityException($"Found multiple web domains: valid= {autofillView.WebDomain}, child={webDomain}");
}
}
else
{
autofillView.WebDomain = webDomain;
}
}
autofillView.InputFields.Add(new ViewNodeInputField(viewNode));
var childrenSize = viewNode.ChildCount;
if (childrenSize > 0)
{
for (int i = 0; i < childrenSize; i++)
{
ParseRecursive(autofillView, viewNode.GetChildAt(i), isManualRequest);
}
}
}
}
/// <summary>
/// Parser for an AssistStructure object. This is invoked when the Autofill Service receives an
/// AssistStructure from the client Activity, representing its View hierarchy. In this sample, it
/// parses the hierarchy and collects autofill metadata from {@link ViewNode}s along the way.
/// </summary>
public sealed class StructureParser: StructureParserBase<ViewNodeInputField>
{
private readonly AssistStructure _structure;
public Context _context { get; }
public AutofillFieldMetadataCollection AutofillFields { get; set; }
public FilledAutofillFieldCollection<ViewNodeInputField> ClientFormData { get; set; }
public string PackageId { get; set; }
public StructureParser(Context context, AssistStructure structure)
: base(new Kp2aLogger(), new Kp2aDigitalAssetLinksDataSource(context))
{
_context = context;
_structure = structure;
AutofillFields = new AutofillFieldMetadataCollection();
LogAutofillView = PreferenceManager.GetDefaultSharedPreferences(context).GetBoolean(context.GetString(Resource.String.LogAutofillView_key), false);
}
protected override AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView<ViewNodeInputField> autofillView)
{
if (autofillView == null)
Kp2aLog.Log("Received null autofill view!");
var result = base.Parse(forFill, isManualRequest, autofillView);
Kp2aLog.Log("Parsing done");
if (forFill)
{
foreach (var p in FieldsMappedToHints)
AutofillFields.Add(new AutofillFieldMetadata(p.Key.ViewNode, p.Value));
}
else
{
ClientFormData = new FilledAutofillFieldCollection<ViewNodeInputField>();
foreach (var p in FieldsMappedToHints)
ClientFormData.Add(new FilledAutofillField(p.Key, p.Value));
}
return result;
}
public AutofillTargetId ParseForSave()
{
var autofillView = new AutofillViewFromAssistStructureFinder(_context, _structure).GetAutofillView(true);
return Parse(false, true, autofillView);
}
public StructureParserBase<ViewNodeInputField>.AutofillTargetId ParseForFill(bool isManual)
{
var autofillView = new AutofillViewFromAssistStructureFinder(_context, _structure).GetAutofillView(isManual);
return Parse(true, isManual, autofillView);
}
}
public class Kp2aLogger : ILogger
{
public void Log(string x)
{
Kp2aLog.Log(x);
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Android.App.Assist;
using Android.Views.Autofill;
using KeePassLib.Utility;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase.model
{
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using Android.Service.Autofill;
using Android.Util;
using Android.Views;
using Android.Views.Autofill;
namespace keepass2android.services.AutofillBase.model
{
}

View File

@@ -0,0 +1,6 @@
using Android.Util;
namespace keepass2android.services.AutofillBase.model
{
}

View File

@@ -0,0 +1,950 @@
/*
This file is part of Keepass2Android, Copyright 2013 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 <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using Android.AccessibilityServices;
using Java.Util;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using Android.Widget;
using Android.Preferences;
using Android.Views.Accessibility;
using KeePassLib;
using KeePassLib.Utility;
using Android.Views.InputMethods;
using AndroidX.Core.App;
using KeePass.Util.Spr;
using keepass2android;
using KeePassLib.Serialization;
using PluginTOTP;
namespace keepass2android
{
/// <summary>
/// Service to show the notifications to make the current entry accessible through clipboard or the KP2A keyboard.
/// </summary>
/// The name reflects only the possibility through clipboard because keyboard was introduced later.
/// The notifications require to be displayed by a service in order to be kept when the activity is closed
/// after searching for a URL.
[Service]
public class CopyToClipboardService : Service
{
protected override void AttachBaseContext(Context baseContext)
{
base.AttachBaseContext(LocaleManager.setLocale(baseContext));
}
class PasswordAccessNotificationBuilder
{
private readonly Context _ctx;
private readonly NotificationManager _notificationManager;
public PasswordAccessNotificationBuilder(Context ctx, NotificationManager notificationManager)
{
_ctx = ctx;
_notificationManager = notificationManager;
}
private bool _hasPassword;
private bool _hasUsername;
private bool _hasTotp;
private bool _hasKeyboard;
public void AddPasswordAccess()
{
_hasPassword = true;
}
public void AddUsernameAccess()
{
_hasUsername = true;
}
public void AddTotpAccess()
{
_hasTotp = true;
}
public void AddKeyboardAccess()
{
_hasKeyboard = true;
}
public int CreateNotifications(string entryName, Bitmap entryIcon)
{
if (((int)Build.VERSION.SdkInt < 16) ||
(PreferenceManager.GetDefaultSharedPreferences(_ctx)
.GetBoolean(_ctx.GetString(Resource.String.ShowSeparateNotifications_key),
_ctx.Resources.GetBoolean(Resource.Boolean.ShowSeparateNotifications_default))))
{
return CreateSeparateNotifications(entryName, entryIcon);
}
else
{
return CreateCombinedNotification(entryName, entryIcon);
}
}
private int CreateCombinedNotification(string entryName, Bitmap entryIcon)
{
Kp2aLog.Log("Create Combined Notifications: " + _hasKeyboard + " " + _hasPassword + " " + _hasUsername +
" " + _hasTotp);
if ((!_hasUsername) && (!_hasPassword) && (!_hasKeyboard) && (!_hasTotp))
return 0;
NotificationCompat.Builder notificationBuilder;
if (_hasKeyboard)
{
notificationBuilder = GetNotificationBuilder(Intents.CheckKeyboard, Resource.String.available_through_keyboard,
Resource.Drawable.ic_notify_keyboard, entryName, entryIcon);
}
else
{
notificationBuilder = GetNotificationBuilder(null, Resource.String.entry_is_available, Resource.Drawable.ic_launcher_gray,
entryName, entryIcon);
}
//add action buttons to base notification:
if (_hasUsername)
notificationBuilder.AddAction(new NotificationCompat.Action(Resource.Drawable.baseline_account_circle_24,
_ctx.GetString(Resource.String.menu_copy_user),
GetPendingIntent(Intents.CopyUsername, Resource.String.menu_copy_user)));
if (_hasPassword)
notificationBuilder.AddAction(new NotificationCompat.Action(Resource.Drawable.baseline_vpn_key_24,
_ctx.GetString(Resource.String.menu_copy_pass),
GetPendingIntent(Intents.CopyPassword, Resource.String.menu_copy_pass)));
if (_hasTotp)
notificationBuilder.AddAction(new NotificationCompat.Action(Resource.Drawable.baseline_vpn_key_24,
_ctx.GetString(Resource.String.menu_copy_totp),
GetPendingIntent(Intents.CopyTotp, Resource.String.menu_copy_totp)));
// Don't show on wearable devices if possible
if ((int)Build.VERSION.SdkInt >= 20)
notificationBuilder.SetLocalOnly(true);
notificationBuilder.SetPriority((int)Android.App.NotificationPriority.Max);
var notification = notificationBuilder.Build();
notification.DeleteIntent = CreateDeleteIntent(NotifyCombined);
_notificationManager.Notify(NotifyCombined, notification);
return 1;
}
private int CreateSeparateNotifications(string entryName, Bitmap entryIcon)
{
Kp2aLog.Log("Create Separate Notifications: " + _hasKeyboard + " " + _hasPassword + " " + _hasUsername +
" " + _hasTotp);
int numNotifications = 0;
if (_hasPassword)
{
// only show notification if password is available
Notification password = GetNotification(Intents.CopyPassword, Resource.String.copy_password,
Resource.Drawable.baseline_vpn_key_24, entryName, entryIcon);
numNotifications++;
password.DeleteIntent = CreateDeleteIntent(NotifyPassword);
_notificationManager.Notify(NotifyPassword, password);
}
if (_hasUsername)
{
// only show notification if username is available
Notification username = GetNotification(Intents.CopyUsername, Resource.String.copy_username,
Resource.Drawable.baseline_account_circle_24, entryName, entryIcon);
username.DeleteIntent = CreateDeleteIntent(NotifyUsername);
_notificationManager.Notify(NotifyUsername, username);
numNotifications++;
}
if (_hasTotp)
{
// only show notification if totp is available
Notification totp = GetNotification(Intents.CopyTotp, Resource.String.copy_totp,
Resource.Drawable.baseline_vpn_key_24, entryName, entryIcon);
totp.DeleteIntent = CreateDeleteIntent(NotifyTotp);
_notificationManager.Notify(NotifyTotp, totp);
numNotifications++;
}
if (_hasKeyboard)
{
// only show notification if username is available
Notification keyboard = GetNotification(Intents.CheckKeyboard, Resource.String.available_through_keyboard,
Resource.Drawable.ic_notify_keyboard, entryName, entryIcon);
keyboard.DeleteIntent = CreateDeleteIntent(NotifyKeyboard);
_notificationManager.Notify(NotifyKeyboard, keyboard);
numNotifications++;
}
return numNotifications;
}
//creates a delete intent (started when notification is cancelled by user or something else)
//requires different request codes for every item (otherwise the intents are identical)
PendingIntent CreateDeleteIntent(int requestCode)
{
Intent intent = new Intent(ActionNotificationCancelled);
Bundle extra = new Bundle();
extra.PutInt("requestCode", requestCode);
intent.PutExtras(extra);
return PendingIntent.GetBroadcast(_ctx, requestCode, intent, Util.AddMutabilityFlag(PendingIntentFlags.CancelCurrent, PendingIntentFlags.Immutable));
}
private Notification GetNotification(string intentText, int descResId, int drawableResId, string entryName, Bitmap entryIcon)
{
var builder = GetNotificationBuilder(intentText, descResId, drawableResId, entryName, entryIcon);
return builder.Build();
}
private NotificationCompat.Builder GetNotificationBuilder(string intentText, int descResId, int drawableResId, string entryName, Bitmap entryIcon)
{
String desc = _ctx.GetString(descResId);
String title = _ctx.GetString(Resource.String.app_name);
if (!String.IsNullOrEmpty(entryName))
title += " (" + entryName + ")";
PendingIntent pending;
if (intentText == null)
{
pending = PendingIntent.GetActivity(_ctx.ApplicationContext, 0, new Intent(), Util.AddMutabilityFlag(0, PendingIntentFlags.Immutable));
}
else
{
pending = GetPendingIntent(intentText, descResId);
}
var builder = new NotificationCompat.Builder(_ctx, App.NotificationChannelIdEntry);
builder.SetSmallIcon(drawableResId)
.SetContentText(desc)
.SetContentTitle(entryName)
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
.SetTicker(entryName + ": " + desc)
.SetVisibility((int)Android.App.NotificationVisibility.Secret)
.SetAutoCancel(true)
.SetContentIntent(pending);
if (entryIcon != null)
builder.SetLargeIcon(entryIcon);
return builder;
}
private PendingIntent GetPendingIntent(string intentText, int descResId)
{
PendingIntent pending;
Intent intent = new Intent(_ctx, typeof(CopyToClipboardBroadcastReceiver));
intent.SetAction(intentText);
pending = PendingIntent.GetBroadcast(_ctx, descResId, intent, Util.AddMutabilityFlag(PendingIntentFlags.CancelCurrent, PendingIntentFlags.Immutable));
return pending;
}
}
public const int NotifyUsername = 1;
public const int NotifyPassword = 2;
public const int NotifyKeyboard = 3;
public const int ClearClipboard = 4;
public const int NotifyCombined = 5;
public const int NotifyTotp = 6;
static public void CopyValueToClipboardWithTimeout(Context ctx, string text, bool isProtected)
{
Intent i = new Intent(ctx, typeof(CopyToClipboardService));
i.SetAction(Intents.CopyStringToClipboard);
i.PutExtra(_stringtocopy, text);
i.PutExtra(_stringisprotected, isProtected);
ctx.StartService(i);
}
static public void ActivateKeyboard(Context ctx)
{
Intent i = new Intent(ctx, typeof(CopyToClipboardService));
i.SetAction(Intents.ActivateKeyboard);
ctx.StartService(i);
}
public static void CancelNotifications(Context ctx)
{
Intent i = new Intent(ctx, typeof(CopyToClipboardService));
i.SetAction(Intents.ClearNotificationsAndData);
ctx.StartService(i);
}
public CopyToClipboardService(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
NotificationDeletedBroadcastReceiver _notificationDeletedBroadcastReceiver;
StopOnLockBroadcastReceiver _stopOnLockBroadcastReceiver;
public CopyToClipboardService()
{
}
public override IBinder OnBind(Intent intent)
{
return null;
}
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
{
Kp2aLog.Log("Received intent to provide access to entry");
if (_stopOnLockBroadcastReceiver == null)
{
_stopOnLockBroadcastReceiver = new StopOnLockBroadcastReceiver(this);
IntentFilter filter = new IntentFilter();
filter.AddAction(Intents.DatabaseLocked);
RegisterReceiver(_stopOnLockBroadcastReceiver, filter);
}
if ((intent.Action == Intents.ShowNotification) || (intent.Action == Intents.UpdateKeyboard))
{
String entryId = intent.GetStringExtra(EntryActivity.KeyEntry);
String searchUrl = intent.GetStringExtra(SearchUrlTask.UrlToSearchKey);
if (entryId == null)
{
Kp2aLog.Log("received intent " + intent.Action + " without KeyEntry!");
#if DEBUG
throw new Exception("invalid intent received!");
#endif
return StartCommandResult.NotSticky;
}
PwEntryOutput entry;
try
{
ElementAndDatabaseId fullId = new ElementAndDatabaseId(entryId);
if (((App.Kp2a.LastOpenedEntry != null)
&& (fullId.ElementId.Equals(App.Kp2a.LastOpenedEntry.Uuid))))
{
entry = App.Kp2a.LastOpenedEntry;
}
else
{
Database entryDb = App.Kp2a.GetDatabase(fullId.DatabaseId);
entry = new PwEntryOutput(entryDb.EntriesById[fullId.ElementId], entryDb);
}
}
catch (Exception e)
{
Kp2aLog.LogUnexpectedError(e);
//seems like restarting the service happened after closing the DB
StopSelf();
return StartCommandResult.NotSticky;
}
if (intent.Action == Intents.ShowNotification)
{
//first time opening the entry -> bring up the notifications
bool activateKeyboard = intent.GetBooleanExtra(EntryActivity.KeyActivateKeyboard, false);
DisplayAccessNotifications(entry, activateKeyboard, searchUrl);
}
else //UpdateKeyboard
{
#if !EXCLUDE_KEYBOARD
//this action is received when the data in the entry has changed (e.g. by plugins)
//update the keyboard data.
//Check if keyboard is (still) available
if (Keepass2android.Kbbridge.KeyboardData.EntryId == entry.Uuid.ToHexString())
MakeAccessibleForKeyboard(entry, searchUrl);
#endif
}
}
if (intent.Action == Intents.CopyStringToClipboard)
{
TimeoutCopyToClipboard(intent.GetStringExtra(_stringtocopy), intent.GetBooleanExtra(_stringisprotected, false));
}
if (intent.Action == Intents.ActivateKeyboard)
{
ActivateKp2aKeyboard();
}
if (intent.Action == Intents.ClearNotificationsAndData)
{
ClearNotifications();
}
return StartCommandResult.RedeliverIntent;
}
private void OnLockDatabase()
{
Kp2aLog.Log("Stopping clipboard service due to database lock");
StopSelf();
}
private NotificationManager _notificationManager;
private int _numElementsToWaitFor;
public override void OnDestroy()
{
Kp2aLog.Log("CopyToClipboardService.OnDestroy");
// These members might never get initialized if the app timed out
if (_stopOnLockBroadcastReceiver != null)
{
UnregisterReceiver(_stopOnLockBroadcastReceiver);
_stopOnLockBroadcastReceiver = null;
}
if (_notificationDeletedBroadcastReceiver != null)
{
UnregisterReceiver(_notificationDeletedBroadcastReceiver);
_notificationDeletedBroadcastReceiver = null;
}
if (_notificationManager != null)
{
_notificationManager.Cancel(NotifyPassword);
_notificationManager.Cancel(NotifyUsername);
_notificationManager.Cancel(NotifyKeyboard);
_notificationManager.Cancel(NotifyCombined);
_numElementsToWaitFor = 0;
ClearKeyboard(true);
}
if (_clearClipboardTask != null)
{
Kp2aLog.Log("Clearing clipboard due to stop CopyToClipboardService");
_clearClipboardTask.Run();
}
Kp2aLog.Log("Destroyed Show-Notification-Receiver.");
base.OnDestroy();
}
private const string ActionNotificationCancelled = "notification_cancelled";
public void DisplayAccessNotifications(PwEntryOutput entry, bool activateKeyboard, string searchUrl)
{
var hadKeyboardData = ClearNotifications();
String entryName = entry.OutputStrings.ReadSafe(PwDefs.TitleField);
Database db = App.Kp2a.FindDatabaseForElement(entry.Entry);
var bmp = Util.DrawableToBitmap(db.DrawableFactory.GetIconDrawable(this,
db.KpDatabase, entry.Entry.IconId, entry.Entry.CustomIconUuid, false));
if (!(((entry.Entry.CustomIconUuid != null) && (!entry.Entry.CustomIconUuid.Equals(PwUuid.Zero))))
&& PreferenceManager.GetDefaultSharedPreferences(this).GetString("IconSetKey", PackageName) == PackageName)
{
Color drawingColor = new Color(189, 189, 189);
bmp = Util.ChangeImageColor(bmp, drawingColor);
}
Bitmap entryIcon = Util.MakeLargeIcon(bmp, this);
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
var notBuilder = new PasswordAccessNotificationBuilder(this, _notificationManager);
if (prefs.GetBoolean(GetString(Resource.String.CopyToClipboardNotification_key), Resources.GetBoolean(Resource.Boolean.CopyToClipboardNotification_default)))
{
if (entry.OutputStrings.ReadSafe(PwDefs.PasswordField).Length > 0)
{
notBuilder.AddPasswordAccess();
}
if (entry.OutputStrings.ReadSafe(PwDefs.UserNameField).Length > 0)
{
notBuilder.AddUsernameAccess();
}
if (entry.OutputStrings.ReadSafe(UpdateTotpTimerTask.TotpKey).Length > 0)
{
notBuilder.AddTotpAccess();
}
}
bool hasKeyboardDataNow = false;
if (prefs.GetBoolean(GetString(Resource.String.UseKp2aKeyboard_key), Resources.GetBoolean(Resource.Boolean.UseKp2aKeyboard_default)))
{
//keyboard
hasKeyboardDataNow = MakeAccessibleForKeyboard(entry, searchUrl);
if (hasKeyboardDataNow)
{
notBuilder.AddKeyboardAccess();
if (activateKeyboard)
ActivateKp2aKeyboard();
}
}
if ((!hasKeyboardDataNow) && (hadKeyboardData))
{
ClearKeyboard(true); //this clears again and then (this is the point) broadcasts that we no longer have keyboard data
}
_numElementsToWaitFor = notBuilder.CreateNotifications(entryName, entryIcon);
if (_numElementsToWaitFor == 0)
{
Kp2aLog.Log("Stopping CopyToClipboardService, created empty notification");
StopSelf();
return;
}
//register receiver to get notified when notifications are discarded in which case we can shutdown the service
if (_notificationDeletedBroadcastReceiver == null)
{
_notificationDeletedBroadcastReceiver = new NotificationDeletedBroadcastReceiver(this);
IntentFilter deletefilter = new IntentFilter();
deletefilter.AddAction(ActionNotificationCancelled);
RegisterReceiver(_notificationDeletedBroadcastReceiver, deletefilter);
}
}
private bool ClearNotifications()
{
// Notification Manager
_notificationManager = (NotificationManager)GetSystemService(NotificationService);
_notificationManager.Cancel(NotifyPassword);
_notificationManager.Cancel(NotifyUsername);
_notificationManager.Cancel(NotifyKeyboard);
_notificationManager.Cancel(NotifyCombined);
_numElementsToWaitFor = 0;
bool hadKeyboardData = ClearKeyboard(false); //do not broadcast if the keyboard was changed
return hadKeyboardData;
}
bool MakeAccessibleForKeyboard(PwEntryOutput entry, string searchUrl)
{
#if EXCLUDE_KEYBOARD
return false;
#else
bool hasData = false;
Keepass2android.Kbbridge.KeyboardDataBuilder kbdataBuilder = new Keepass2android.Kbbridge.KeyboardDataBuilder();
String[] standardKeys = {PwDefs.UserNameField,
PwDefs.PasswordField,
Kp2aTotp.TotpKey,
PwDefs.UrlField,
PwDefs.NotesField,
PwDefs.TitleField
};
int[] resIds = {Resource.String.entry_user_name,
Resource.String.entry_password,
0,
Resource.String.entry_url,
Resource.String.entry_comment,
Resource.String.entry_title };
//add standard fields:
int i = 0;
foreach (string key in standardKeys)
{
String value = entry.OutputStrings.ReadSafe(key);
if (value.Length > 0)
{
kbdataBuilder.AddString(key, resIds[i] > 0 ? GetString(resIds[i]) : key, value);
hasData = true;
}
i++;
}
//add additional fields:
var totpData = new Kp2aTotp().TryGetTotpData(entry);
foreach (var pair in entry.OutputStrings)
{
var key = pair.Key;
var value = pair.Value.ReadString();
if (!standardKeys.Contains(key) && totpData?.InternalFields.Contains(key) != true)
{
kbdataBuilder.AddString(pair.Key, pair.Key, value);
hasData = true;
}
}
kbdataBuilder.Commit();
Keepass2android.Kbbridge.KeyboardData.EntryName = entry.OutputStrings.ReadSafe(PwDefs.TitleField);
Keepass2android.Kbbridge.KeyboardData.EntryId = entry.Uuid.ToHexString();
if (hasData)
Keepass2android.Autofill.AutoFillService.NotifyNewData(searchUrl);
return hasData;
#endif
}
public void OnWaitElementDeleted(int itemId)
{
Kp2aLog.Log("Wait element deleted: " + itemId);
_numElementsToWaitFor--;
if (_numElementsToWaitFor <= 0)
{
Kp2aLog.Log("Stopping CopyToClipboardService, no more elements");
StopSelf();
}
if ((itemId == NotifyKeyboard) || (itemId == NotifyCombined))
{
//keyboard notification was deleted -> clear entries in keyboard
ClearKeyboard(true);
}
}
bool ClearKeyboard(bool broadcastClear)
{
#if !EXCLUDE_KEYBOARD
Keepass2android.Kbbridge.KeyboardData.AvailableFields.Clear();
Keepass2android.Kbbridge.KeyboardData.EntryName = null;
bool hadData = Keepass2android.Kbbridge.KeyboardData.EntryId != null;
Keepass2android.Kbbridge.KeyboardData.EntryId = null;
if ((hadData) && broadcastClear)
SendBroadcast(new Intent(Intents.KeyboardCleared));
return hadData;
#else
return false;
#endif
}
private readonly Java.Util.Timer _timer = new Java.Util.Timer();
internal void TimeoutCopyToClipboard(String text, bool isProtected)
{
Util.CopyToClipboard(this, text, isProtected);
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
String sClipClear = prefs.GetString(GetString(Resource.String.clipboard_timeout_key), GetString(Resource.String.clipboard_timeout_default));
long clipClearTime = long.Parse(sClipClear);
_clearClipboardTask = new ClearClipboardTask(this, text, _uiThreadCallback);
if (clipClearTime > 0)
{
_numElementsToWaitFor++;
_timer.Schedule(_clearClipboardTask, clipClearTime);
}
}
// Task which clears the clipboard, and sends a toast to the foreground.
private class ClearClipboardTask : TimerTask
{
private readonly String _clearText;
private readonly CopyToClipboardService _service;
private readonly Handler _handler;
public ClearClipboardTask(CopyToClipboardService service, String clearText, Handler handler)
{
_clearText = clearText;
_service = service;
_handler = handler;
}
public override void Run()
{
String currentClip = Util.GetClipboard(_service);
DoPostClear();
if (currentClip.Equals(_clearText))
{
Util.CopyToClipboard(_service, "", false);
DoPostWarn();
}
}
private void DoPostWarn()
{
_handler.Post(ShowClipboardWarning);
}
private void DoPostClear()
{
_handler.Post(DoClearClipboard);
}
private void DoClearClipboard()
{
_service.OnWaitElementDeleted(CopyToClipboardService.ClearClipboard);
}
private void ShowClipboardWarning()
{
string message = _service.GetString(Resource.String.ClearClipboard) + " "
+ _service.GetString(Resource.String.ClearClipboardWarning);
Android.Util.Log.Debug("KP2A", message);
Toast.MakeText(_service,
message,
ToastLength.Long).Show();
}
}
// Setup to allow the toast to happen in the foreground
readonly Handler _uiThreadCallback = new Handler();
private ClearClipboardTask _clearClipboardTask;
private const string _stringtocopy = "StringToCopy";
private const string _stringisprotected = "StringIsProtected";
private class StopOnLockBroadcastReceiver : BroadcastReceiver
{
readonly CopyToClipboardService _service;
public StopOnLockBroadcastReceiver(CopyToClipboardService service)
{
_service = service;
}
public override void OnReceive(Context context, Intent intent)
{
switch (intent.Action)
{
case Intents.DatabaseLocked:
_service.OnLockDatabase();
break;
}
}
}
class NotificationDeletedBroadcastReceiver : BroadcastReceiver
{
readonly CopyToClipboardService _service;
public NotificationDeletedBroadcastReceiver(CopyToClipboardService service)
{
_service = service;
}
#region implemented abstract members of BroadcastReceiver
public override void OnReceive(Context context, Intent intent)
{
if (intent.Action == ActionNotificationCancelled)
{
_service.OnWaitElementDeleted(intent.Extras.GetInt("requestCode"));
}
}
#endregion
}
internal void ActivateKp2aKeyboard()
{
string currentIme = Android.Provider.Settings.Secure.GetString(
ContentResolver,
Android.Provider.Settings.Secure.DefaultInputMethod);
string kp2aIme = Kp2aInputMethodName;
if (currentIme == kp2aIme)
{
//keyboard already activated. bring it up.
InputMethodManager imeManager = (InputMethodManager)ApplicationContext.GetSystemService(InputMethodService);
if (imeManager == null)
{
Toast.MakeText(this, Resource.String.not_possible_im_picker, ToastLength.Long).Show();
return;
}
try
{
imeManager.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.None);
}
catch (Exception e)
{
Kp2aLog.LogUnexpectedError(e);
try
{
imeManager.ToggleSoftInput(ShowFlags.Implicit, HideSoftInputFlags.ImplicitOnly);
return;
}
catch (Exception)
{
Toast.MakeText(this, Resource.String.not_possible_im_picker, ToastLength.Long).Show();
}
return;
}
}
else
{
if (!IsKp2aInputMethodEnabled)
{
//must be enabled in settings first
Toast.MakeText(this, Resource.String.please_activate_keyboard, ToastLength.Long).Show();
Intent settingsIntent = new Intent(Android.Provider.Settings.ActionInputMethodSettings);
try
{
settingsIntent.SetFlags(ActivityFlags.NewTask | ActivityFlags.ExcludeFromRecents);
StartActivity(settingsIntent);
}
catch (Exception e)
{
//seems like on Huawei devices this call can fail.
Kp2aLog.LogUnexpectedError(e);
Toast.MakeText(this, "Failed to switch keyboard.", ToastLength.Long).Show();
}
}
else
{
//let's bring up the keyboard switching dialog.
//Unfortunately this no longer works starting with Android 9 if our app is not in foreground.
//first it seemed to be required for Samsung mostly, but there are use cases where it is required for other devices as well.
//Let's be sure and use the helper activity.
bool mustUseHelperActivity = (int)Build.VERSION.SdkInt >= 28;
if (mustUseHelperActivity)
{
try
{
Intent switchImeIntent = new Intent(this, typeof(SwitchImeActivity));
switchImeIntent.SetFlags(ActivityFlags.NewTask | ActivityFlags.ExcludeFromRecents);
StartActivity(switchImeIntent);
}
catch (Exception e)
{
//seems like on Huawei devices this call can fail.
Kp2aLog.LogUnexpectedError(e);
Toast.MakeText(this, "Failed to switch keyboard.", ToastLength.Long).Show();
}
}
else
{
#if !EXCLUDE_KEYBOARD
Keepass2android.Kbbridge.ImeSwitcher.SwitchToKeyboard(this, kp2aIme, false);
#endif
}
}
}
}
public bool IsKp2aInputMethodEnabled
{
get
{
InputMethodManager imeManager = (InputMethodManager)ApplicationContext.GetSystemService(InputMethodService);
if (imeManager == null)
return false;
IList<InputMethodInfo> inputMethodProperties = imeManager.EnabledInputMethodList;
return inputMethodProperties.Any(imi => imi.Id.Equals(Kp2aInputMethodName));
}
}
private string Kp2aInputMethodName
{
get { return PackageName + "/keepass2android.softkeyboard.KP2AKeyboard"; }
}
}
[BroadcastReceiver(Permission = "keepass2android." + AppNames.PackagePart + ".permission.CopyToClipboard")]
class CopyToClipboardBroadcastReceiver : BroadcastReceiver
{
public CopyToClipboardBroadcastReceiver(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
public CopyToClipboardBroadcastReceiver()
{
}
public override void OnReceive(Context context, Intent intent)
{
String action = intent.Action;
//check if we have a last opened entry
//this should always be non-null, but if the OS has killed the app, it might occur.
if (App.Kp2a.LastOpenedEntry == null)
{
Intent i = new Intent(context, typeof(AppKilledInfo));
i.SetFlags(ActivityFlags.ClearTask | ActivityFlags.NewTask | ActivityFlags.ExcludeFromRecents);
context.StartActivity(i);
return;
}
if (action.Equals(Intents.CopyUsername))
{
String username = App.Kp2a.LastOpenedEntry.OutputStrings.ReadSafe(PwDefs.UserNameField);
if (username.Length > 0)
{
CopyToClipboardService.CopyValueToClipboardWithTimeout(context, username, false);
}
CloseNotificationDrawer(context);
}
else if (action.Equals(Intents.CopyPassword))
{
String password = App.Kp2a.LastOpenedEntry.OutputStrings.ReadSafe(PwDefs.PasswordField);
if (password.Length > 0)
{
CopyToClipboardService.CopyValueToClipboardWithTimeout(context, password, true);
}
CloseNotificationDrawer(context);
}
else if (action.Equals(Intents.CopyTotp))
{
String totp = App.Kp2a.LastOpenedEntry.OutputStrings.ReadSafe(UpdateTotpTimerTask.TotpKey);
if (totp.Length > 0)
{
CopyToClipboardService.CopyValueToClipboardWithTimeout(context, totp, true);
}
CloseNotificationDrawer(context);
}
else if (action.Equals(Intents.CheckKeyboard))
{
CopyToClipboardService.ActivateKeyboard(context);
}
}
private static void CloseNotificationDrawer(Context context)
{
if ((int)Build.VERSION.SdkInt < 31) //sending this intent is no longer allowed since Android 31
context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer
}
};
}

View File

@@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using AndroidX.Preference;
using KeePass.Util.Spr;
using keepass2android.services.AutofillBase;
using keepass2android.services.AutofillBase.model;
using Keepass2android.Pluginsdk;
using keepass2android;
using KeePassLib;
using KeePassLib.Utility;
using Kp2aAutofillParser;
namespace keepass2android.services.Kp2aAutofill
{
[Activity(Label = "@string/app_name",
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden,
Theme = "@style/Kp2aTheme_ActionBar",
WindowSoftInputMode = SoftInput.AdjustResize,
Permission = "keepass2android." + AppNames.PackagePart + ".permission.Kp2aChooseAutofill")]
public class ChooseForAutofillActivity : ChooseForAutofillActivityBase
{
public bool ActivateKeyboardWhenTotpPreference
{
get
{
return PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean("AutoFillTotp_prefs_ActivateKeyboard_key", false);
}
}
public bool CopyTotpToClipboardPreference
{
get
{
return PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean("AutoFillTotp_prefs_CopyTotpToClipboard_key", true);
}
}
public bool ShowNotificationPreference
{
get
{
return PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean("AutoFillTotp_prefs_ShowNotification_key", true);
}
}
protected override Intent GetQueryIntent(string requestedUrl, bool autoReturnFromQuery, bool useLastOpenedEntry)
{
if (useLastOpenedEntry && (App.Kp2a.LastOpenedEntry?.SearchUrl == requestedUrl))
{
return null;
}
//launch SelectCurrentDbActivity (which is root of the stack (exception: we're even below!)) with the appropriate task.
//will return the results later
Intent i = new Intent(this, typeof(SelectCurrentDbActivity));
//don't show user notifications when an entry is opened.
var task = new SearchUrlTask()
{
UrlToSearchFor = requestedUrl,
AutoReturnFromQuery = autoReturnFromQuery
};
SetTotpDependantActionsOnTask(task);
task.ToIntent(i);
return i;
}
private void SetTotpDependantActionsOnTask(SelectEntryTask task)
{
task.ShowUserNotifications =
ShowNotificationPreference ? ActivationCondition.WhenTotp : ActivationCondition.Never;
task.CopyTotpToClipboard = CopyTotpToClipboardPreference;
task.ActivateKeyboard = ActivateKeyboardWhenTotpPreference
? ActivationCondition.WhenTotp
: ActivationCondition.Never;
}
protected override Intent GetOpenEntryIntent(string entryUuid)
{
Intent i = new Intent(this, typeof(SelectCurrentDbActivity));
//don't show user notifications when an entry is opened.
var task = new OpenSpecificEntryTask() { EntryUuid = entryUuid };
SetTotpDependantActionsOnTask(task);
task.ToIntent(i);
return i;
}
protected override Result ExpectedActivityResult => KeePass.ExitCloseAfterTaskComplete;
protected override FilledAutofillFieldCollection<ViewNodeInputField> GetDataset()
{
if (App.Kp2a.CurrentDb==null || (App.Kp2a.QuickLocked))
return null;
var entryOutput = App.Kp2a.LastOpenedEntry;
return GetFilledAutofillFieldCollectionFromEntry(entryOutput, this);
}
public static FilledAutofillFieldCollection<ViewNodeInputField> GetFilledAutofillFieldCollectionFromEntry(PwEntryOutput pwEntryOutput, Context context)
{
if (pwEntryOutput == null)
return null;
FilledAutofillFieldCollection<ViewNodeInputField> fieldCollection = new FilledAutofillFieldCollection<ViewNodeInputField>();
var pwEntry = pwEntryOutput.Entry;
foreach (string key in pwEntryOutput.OutputStrings.GetKeys())
{
FilledAutofillField field =
new FilledAutofillField
{
AutofillHints = GetCanonicalHintsFromKp2aField(key).ToArray(),
TextValue = pwEntryOutput.OutputStrings.ReadSafe(key)
};
fieldCollection.Add(field);
}
if (IsCreditCard(pwEntry, context) && pwEntry.Expires)
{
DateTime expTime = pwEntry.ExpiryTime;
FilledAutofillField field =
new FilledAutofillField
{
AutofillHints = new[] {View.AutofillHintCreditCardExpirationDate},
DateValue = (long) (1000 * TimeUtil.SerializeUnix(expTime))
};
fieldCollection.Add(field);
field =
new FilledAutofillField
{
AutofillHints = new[] {View.AutofillHintCreditCardExpirationDay},
TextValue = expTime.Day.ToString()
};
fieldCollection.Add(field);
field =
new FilledAutofillField
{
AutofillHints = new[] {View.AutofillHintCreditCardExpirationMonth},
TextValue = expTime.Month.ToString()
};
fieldCollection.Add(field);
field =
new FilledAutofillField
{
AutofillHints = new[] {View.AutofillHintCreditCardExpirationYear},
TextValue = expTime.Year.ToString()
};
fieldCollection.Add(field);
}
fieldCollection.DatasetName = pwEntry.Strings.ReadSafe(PwDefs.TitleField);
fieldCollection.DatasetName = SprEngine.Compile(fieldCollection.DatasetName, new SprContext(pwEntry, App.Kp2a.CurrentDb.KpDatabase, SprCompileFlags.All));
return fieldCollection;
}
private static bool IsCreditCard(PwEntry pwEntry, Context context)
{
return pwEntry.Strings.Exists("cc-number")
|| pwEntry.Strings.Exists("cc-csc")
|| pwEntry.Strings.Exists(context.GetString(Resource.String.TemplateField_CreditCard_CVV));
}
private static readonly Dictionary<string, List<string>> keyToHint = BuildKeyToHints();
public static string GetKp2aKeyFromHint(string canonicalHint)
{
var key = keyToHint.FirstOrDefault(p => p.Value.Contains(canonicalHint)).Key;
if (string.IsNullOrWhiteSpace(key))
return canonicalHint;
return key;
}
private static Dictionary<string, List<string>> BuildKeyToHints()
{
var result = new Dictionary<string, List<string>>
{
{PwDefs.UserNameField, new List<string>{View.AutofillHintUsername, View.AutofillHintEmailAddress}},
{PwDefs.PasswordField, new List<string>{View.AutofillHintPassword}},
{PwDefs.UrlField, new List<string>{W3cHints.URL}},
{
LocaleManager.LocalizedAppContext.GetString(Resource.String.TemplateField_CreditCard_CVV),
new List<string>{View.AutofillHintCreditCardSecurityCode}
},
{
LocaleManager.LocalizedAppContext.GetString(Resource.String.TemplateField_CreditCard_Owner),
new List<string>{W3cHints.CC_NAME}
},
{LocaleManager.LocalizedAppContext.GetString(Resource.String.TemplateField_Number), new List<string>{View.AutofillHintCreditCardNumber}},
{LocaleManager.LocalizedAppContext.GetString(Resource.String.TemplateField_IdCard_Name), new List<string>{View.AutofillHintName}},
};
return result;
}
private static List<string> GetCanonicalHintsFromKp2aField(string key)
{
List<string> result = new List<string>() {key};
List<string> hints;
if (keyToHint.TryGetValue(key, out hints))
result = hints;
for (int i = 0; i < result.Count; i++)
{
result[i] = result[i].ToLower();
}
return result;
}
public override IAutofillIntentBuilder IntentBuilder => new Kp2aAutofillIntentBuilder();
}
}

View File

@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Android;
using Android.App;
using Android.Content;
using Android.Preferences;
using Android.Runtime;
using keepass2android.services.AutofillBase;
using keepass2android.services.AutofillBase.model;
using keepass2android.services.Kp2aAutofill;
using Keepass2android.Pluginsdk;
using KeePassLib;
using KeePassLib.Collections;
using KeePassLib.Utility;
using Kp2aAutofillParser;
using Org.Json;
using AutofillServiceBase = keepass2android.services.AutofillBase.AutofillServiceBase;
namespace keepass2android.services
{
[Service(Label = AppNames.AppName, Permission=Manifest.Permission.BindAutofillService, Exported = true)]
[IntentFilter(new [] {"android.service.autofill.AutofillService"})]
[MetaData("android.autofill", Resource = "@xml/autofillservice")]
[Register("keepass2android.services.Kp2aAutofillService")]
public class Kp2aAutofillService: AutofillServiceBase
{
public Kp2aAutofillService()
{
}
public Kp2aAutofillService(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
protected override Dictionary<PwEntryOutput, FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query)
{
if (!App.Kp2a.DatabaseIsUnlocked)
return new Dictionary<PwEntryOutput, FilledAutofillFieldCollection<ViewNodeInputField>>();
var foundEntries = (ShareUrlResults.GetSearchResultsForUrl(query)?.Entries ?? new PwObjectList<PwEntry>())
.ToList();
if (App.Kp2a.LastOpenedEntry?.SearchUrl == query)
{
foundEntries.Clear();
foundEntries.Add(App.Kp2a.LastOpenedEntry?.Entry);
}
int numDisableDatasets = 0;
if (!PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean(GetString(keepass2android.Resource.String.NoAutofillDisabling_key), false))
numDisableDatasets = 1;
//it seems like at least with Firefox we can have at most 3 datasets. Reserve space for the disable dataset and the "fill with KP2A" which allows to select another item
return foundEntries.Take(2-numDisableDatasets)
.Select(e => new PwEntryOutput(e, App.Kp2a.FindDatabaseForElement(e)))
.ToDictionary(e => e,
e => ChooseForAutofillActivity.GetFilledAutofillFieldCollectionFromEntry(e, this));
}
protected override void HandleSaveRequest(StructureParser parser, StructureParser.AutofillTargetId query)
{
var intent = new Intent(this, typeof(SelectCurrentDbActivity));
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.ClearTop | ActivityFlags.SingleTop);
Dictionary<string, string> outputFields = new Dictionary<string, string>();
foreach (var p in parser.ClientFormData.HintMap)
{
CommonUtil.logd(p.Key + " = " + p.Value.ValueToString());
outputFields.TryAdd(ChooseForAutofillActivity.GetKp2aKeyFromHint(p.Key), p.Value.ValueToString());
}
if (query != null)
outputFields.TryAdd(PwDefs.UrlField, query.WebDomain);
JSONObject jsonOutput = new JSONObject(outputFields);
var jsonOutputStr = jsonOutput.ToString();
intent.PutExtra(Strings.ExtraEntryOutputData, jsonOutputStr);
JSONArray jsonProtectedFields = new JSONArray(
(System.Collections.ICollection)new string[]{});
intent.PutExtra(Strings.ExtraProtectedFieldsList, jsonProtectedFields.ToString());
intent.PutExtra(AppTask.AppTaskKey, "CreateEntryThenCloseTask");
intent.PutExtra(CreateEntryThenCloseTask.ShowUserNotificationsKey, "false");
StartActivity(intent);
}
public override IAutofillIntentBuilder IntentBuilder => new Kp2aAutofillIntentBuilder();
}
}

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using keepass2android.services.AutofillBase;
using keepass2android.services.Kp2aAutofill;
using KeePassLib;
namespace keepass2android.services
{
class Kp2aAutofillIntentBuilder: IAutofillIntentBuilder
{
private static int _pendingIntentRequestCode = 0;
public PendingIntent GetAuthPendingIntentForResponse(Context context, string query, string queryDomain, string queryPackage,
bool isManualRequest, bool autoReturnFromQuery, AutofillServiceBase.DisplayWarning warning)
{
Intent intent = new Intent(context, typeof(ChooseForAutofillActivity));
intent.PutExtra(ChooseForAutofillActivityBase.ExtraQueryString, query);
intent.PutExtra(ChooseForAutofillActivityBase.ExtraQueryDomainString, queryDomain);
intent.PutExtra(ChooseForAutofillActivityBase.ExtraQueryPackageString, queryPackage);
intent.PutExtra(ChooseForAutofillActivityBase.ExtraIsManualRequest, isManualRequest);
intent.PutExtra(ChooseForAutofillActivityBase.ExtraAutoReturnFromQuery, autoReturnFromQuery);
intent.PutExtra(ChooseForAutofillActivityBase.ExtraDisplayWarning, (int)warning);
return PendingIntent.GetActivity(context, _pendingIntentRequestCode++, intent, Util.AddMutabilityFlag(PendingIntentFlags.CancelCurrent, PendingIntentFlags.Mutable));
}
public PendingIntent GetAuthPendingIntentForWarning(Context context,PwUuid entryUuid,
AutofillServiceBase.DisplayWarning warning)
{
Intent intent = new Intent(context, typeof(ChooseForAutofillActivity));
intent.PutExtra(ChooseForAutofillActivityBase.ExtraUuidString, entryUuid.ToHexString());
intent.PutExtra(ChooseForAutofillActivityBase.ExtraDisplayWarning, (int)warning);
intent.PutExtra(ChooseForAutofillActivityBase.ExtraUseLastOpenedEntry, true);
return PendingIntent.GetActivity(context, _pendingIntentRequestCode++, intent, Util.AddMutabilityFlag(PendingIntentFlags.CancelCurrent, PendingIntentFlags.Mutable));
}
public PendingIntent GetDisablePendingIntentForResponse(Context context, string query,
bool isManualRequest, bool isDisable)
{
Intent intent = new Intent(context, typeof(DisableAutofillForQueryActivity));
intent.PutExtra(ChooseForAutofillActivityBase.ExtraQueryString, query);
intent.PutExtra(ChooseForAutofillActivityBase.ExtraIsManualRequest, isManualRequest);
intent.PutExtra(DisableAutofillForQueryActivity.ExtraIsDisable, isDisable);
return PendingIntent.GetActivity(context, _pendingIntentRequestCode++, intent, Util.AddMutabilityFlag(PendingIntentFlags.CancelCurrent, PendingIntentFlags.Immutable));
}
public Intent GetRestartAppIntent(Context context)
{
var intent = new Intent(context, typeof(SelectCurrentDbActivity));
intent.AddFlags(ActivityFlags.ForwardResult);
return intent;
}
public int AppIconResource
{
get { return AppNames.LauncherIcon; }
}
}
}

View File

@@ -0,0 +1,267 @@
/*
This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file is based on Keepassdroid, Copyright Brian Pellin.
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 <http://www.gnu.org/licenses/>.
*/
using System;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Preferences;
using AndroidX.Core.App;
using keepass2android;
using KeePassLib.Utility;
namespace keepass2android
{
/// <summary>
/// Service for showing ongoing notifications
///
/// Shows database unlocked warning persistent notification
/// Shows Quick-Unlock notification
/// </summary>
/// This service is running as foreground service to keep the app alive even when it's not currently
/// used by the user. This ensures the database is kept in memory (until Android kills it due to low memory).
/// It is important to also have a foreground service also for the "unlocked" state because it's really
/// irritating if the db is closed while switching between apps.
[Service]
public class OngoingNotificationsService : Service
{
protected override void AttachBaseContext(Context baseContext)
{
base.AttachBaseContext(LocaleManager.setLocale(baseContext));
}
private ScreenOffReceiver _screenOffReceiver;
#region Service
private const int QuickUnlockId = 100;
private const int UnlockedWarningId = 200;
public override void OnCreate()
{
base.OnCreate();
_screenOffReceiver = new ScreenOffReceiver();
IntentFilter filter = new IntentFilter();
filter.AddAction(Intent.ActionScreenOff);
RegisterReceiver(_screenOffReceiver, filter);
}
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
{
Kp2aLog.Log("Starting/Updating OngoingNotificationsService. Database " + (App.Kp2a.DatabaseIsUnlocked ? "Unlocked" : (App.Kp2a.QuickLocked ? "QuickLocked" : "Locked")));
var notificationManager = (NotificationManager)GetSystemService(NotificationService);
// Set the icon to reflect the current state
if (App.Kp2a.DatabaseIsUnlocked)
{
// Clear QuickUnlock icon
notificationManager.Cancel(QuickUnlockId);
//use foreground again to let the app not be killed too easily.
StartForeground(UnlockedWarningId, GetUnlockedNotification());
}
else
{
notificationManager.Cancel(UnlockedWarningId);
if (App.Kp2a.QuickLocked)
{
// Show the Quick Unlock notification
StartForeground(QuickUnlockId, GetQuickUnlockNotification());
}
else
{
// Not showing any notification, database is locked, no point in keeping running
StopSelf();
}
}
return StartCommandResult.NotSticky;
}
public override void OnTaskRemoved(Intent rootIntent)
{
base.OnTaskRemoved(rootIntent);
Kp2aLog.Log("OngoingNotificationsService.OnTaskRemoved: " + rootIntent.Action);
// If the user has closed the task (probably by swiping it out of the recent apps list) then lock the database
App.Kp2a.Lock();
}
public override void OnDestroy()
{
base.OnDestroy();
CancelNotifications(this);
Kp2aLog.Log("OngoingNotificationsService.OnDestroy");
// If the service is killed, then lock the database immediately
if (App.Kp2a.DatabaseIsUnlocked)
{
App.Kp2a.Lock(false);
}
UnregisterReceiver(_screenOffReceiver);
}
public static void CancelNotifications(Context ctx)
{
var notificationManager = (NotificationManager) ctx.GetSystemService(NotificationService);
notificationManager.Cancel(UnlockedWarningId);
// Quick Unlock notification should be removed automatically by the service (if present), as it was the foreground notification.
//also remove any notifications of the app
notificationManager.CancelAll();
}
public override IBinder OnBind(Intent intent)
{
return null;
}
#endregion
#region QuickUnlock
private Notification GetQuickUnlockNotification()
{
int grayIconResouceId = Resource.Drawable.ic_launcher_gray;
if ((int)Android.OS.Build.VERSION.SdkInt < 16)
if (PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(GetString(Resource.String.QuickUnlockIconHidden_key), false))
{
grayIconResouceId = Resource.Drawable.transparent;
}
NotificationCompat.Builder builder =
new NotificationCompat.Builder(this, App.NotificationChannelIdQuicklocked)
.SetSmallIcon(grayIconResouceId)
.SetLargeIcon(MakeLargeIcon(BitmapFactory.DecodeResource(Resources, AppNames.NotificationLockedIcon)))
.SetVisibility((int)Android.App.NotificationVisibility.Secret)
.SetContentTitle(GetString(Resource.String.app_name))
.SetContentText(GetString(Resource.String.database_loaded_quickunlock_enabled, GetDatabaseName()));
if ((int)Build.VERSION.SdkInt >= 16)
{
if (PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean(GetString(Resource.String.QuickUnlockIconHidden16_key), true))
{
builder.SetPriority((int) NotificationPriority.Min);
}
else
{
builder.SetPriority((int)NotificationPriority.Default);
}
}
// Default action is to show Kp2A
builder.SetContentIntent(GetSwitchToAppPendingIntent());
// Additional action to allow locking the database
builder.AddAction(Resource.Drawable.baseline_lock_24, GetString(Resource.String.QuickUnlock_lockButton),
PendingIntent.GetBroadcast(this, 0, new Intent(this, typeof(ApplicationBroadcastReceiver)).SetAction(Intents.CloseDatabase), Util.AddMutabilityFlag(PendingIntentFlags.UpdateCurrent, PendingIntentFlags.Immutable)));
return builder.Build();
}
private Bitmap MakeLargeIcon(Bitmap unscaled)
{
return Util.MakeLargeIcon(unscaled, this);
}
#endregion
#region Unlocked Warning
private Notification GetUnlockedNotification()
{
NotificationCompat.Builder builder =
new NotificationCompat.Builder(this, App.NotificationChannelIdUnlocked)
.SetOngoing(true)
.SetSmallIcon(Resource.Drawable.ic_notify)
.SetLargeIcon(MakeLargeIcon(BitmapFactory.DecodeResource(Resources, AppNames.NotificationUnlockedIcon)))
.SetVisibility((int)Android.App.NotificationVisibility.Public)
.SetContentTitle(GetString(Resource.String.app_name))
.SetContentText(GetString(Resource.String.database_loaded_unlocked, GetDatabaseName()));
if ((int)Build.VERSION.SdkInt >= 16)
{
if (PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean(GetString(Resource.String.ShowUnlockedNotification_key),
Resources.GetBoolean(Resource.Boolean.ShowUnlockedNotification_default)))
{
builder.SetPriority((int)NotificationPriority.Default);
}
else
{
builder.SetPriority((int) NotificationPriority.Min);
}
}
// Default action is to show Kp2A
builder.SetContentIntent(GetSwitchToAppPendingIntent());
// Additional action to allow locking the database
builder.AddAction(Resource.Drawable.baseline_lock_24, GetString(Resource.String.menu_lock), PendingIntent.GetBroadcast(this, 0, new Intent(this, typeof(ApplicationBroadcastReceiver)).SetAction(Intents.LockDatabase), Util.AddMutabilityFlag(PendingIntentFlags.UpdateCurrent, PendingIntentFlags.Immutable)));
return builder.Build();
}
private PendingIntent GetSwitchToAppPendingIntent()
{
var startKp2aIntent = new Intent(this, typeof(KeePass));
startKp2aIntent.SetAction(Intent.ActionMain);
startKp2aIntent.AddCategory(Intent.CategoryLauncher);
return PendingIntent.GetActivity(this, 0, startKp2aIntent, Util.AddMutabilityFlag(PendingIntentFlags.UpdateCurrent, PendingIntentFlags.Immutable));
}
private static string GetDatabaseName()
{
string displayString = "";
foreach (Database db in App.Kp2a.OpenDatabases)
{
var kpDatabase = db.KpDatabase;
var dbname = kpDatabase.Name;
if (String.IsNullOrEmpty(dbname))
{
//if paranoid ("don't remember recent files")return "***"
if (!App.Kp2a.GetBooleanPreference(PreferenceKey.remember_keyfile))
return "***";
dbname = UrlUtil.StripExtension(
UrlUtil.GetFileName(App.Kp2a.GetFileStorage(kpDatabase.IOConnectionInfo).GetDisplayName(kpDatabase.IOConnectionInfo)));
}
if (displayString != "")
displayString = displayString + ", ";
displayString += dbname;
}
return displayString;
}
#endregion
class ScreenOffReceiver: BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
App.Kp2a.OnScreenOff();
}
}
}
}