rename back to current names
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.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, ReceiverFlags.Exported);
|
||||
}
|
||||
|
||||
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, ReceiverFlags.Exported);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
270
src/keepass2android-app/services/OngoingNotificationsService.cs
Normal file
270
src/keepass2android-app/services/OngoingNotificationsService.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
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.Content.PM;
|
||||
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(ForegroundServiceType = ForegroundService.TypeSpecialUse )]
|
||||
[MetaData("android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE", Value = " 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.")]
|
||||
|
||||
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, ReceiverFlags.Exported);
|
||||
}
|
||||
|
||||
|
||||
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