rename folder keepass2android => keepass2android-app
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
242
src/keepass2android-app/services/AutofillBase/AutofillHelper.cs
Normal file
242
src/keepass2android-app/services/AutofillBase/AutofillHelper.cs
Normal 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)];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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;}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/keepass2android-app/services/AutofillBase/CommonUtil.cs
Normal file
58
src/keepass2android-app/services/AutofillBase/CommonUtil.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
252
src/keepass2android-app/services/AutofillBase/DomainParser.cs
Normal file
252
src/keepass2android-app/services/AutofillBase/DomainParser.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
207
src/keepass2android-app/services/AutofillBase/StructureParser.cs
Normal file
207
src/keepass2android-app/services/AutofillBase/StructureParser.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using Android.Util;
|
||||
|
||||
namespace keepass2android.services.AutofillBase.model
|
||||
{
|
||||
|
||||
}
|
||||
950
src/keepass2android-app/services/CopyToClipboardService.cs
Normal file
950
src/keepass2android-app/services/CopyToClipboardService.cs
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
267
src/keepass2android-app/services/OngoingNotificationsService.cs
Normal file
267
src/keepass2android-app/services/OngoingNotificationsService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user