From f0a86db9e44a84d1209ea55b4ae46fea1a45121c Mon Sep 17 00:00:00 2001 From: StefanSchoof Date: Thu, 11 Jan 2018 10:56:22 +0100 Subject: [PATCH 01/75] Add Bugzilla link to Firefox Support --- docs/OreoAutoFill.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/OreoAutoFill.md b/docs/OreoAutoFill.md index 75b4818af..49d230a0e 100644 --- a/docs/OreoAutoFill.md +++ b/docs/OreoAutoFill.md @@ -12,7 +12,7 @@ As of January 2018, the following browsers are known to have Android Autofill su These browsers do not (yet) have autofill support: * Google Chrome -* Firefox for Android +* Firefox for Android ([bugzilla entry](https://bugzilla.mozilla.org/show_bug.cgi?id=1352011)) * Brave-Browser * Opera From 304c1ef5d2ef1df248fe69b5b152abee5ada692c Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Mon, 22 Jan 2018 11:45:28 +0100 Subject: [PATCH 02/75] don't use implicit intents for notification actions. Should fix #149. --- .../app/ApplicationBroadcastReceiver.cs | 1 - .../services/CopyToClipboardService.cs | 12 +++--------- .../services/OngoingNotificationsService.cs | 4 ++-- src/keepass2android/timeout/TimeoutHelper.cs | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/keepass2android/app/ApplicationBroadcastReceiver.cs b/src/keepass2android/app/ApplicationBroadcastReceiver.cs index e1096c39f..51ba4303d 100644 --- a/src/keepass2android/app/ApplicationBroadcastReceiver.cs +++ b/src/keepass2android/app/ApplicationBroadcastReceiver.cs @@ -8,7 +8,6 @@ namespace keepass2android { [BroadcastReceiver] - [IntentFilter(new[] { Intents.LockDatabase, Intents.CloseDatabase })] public class ApplicationBroadcastReceiver : BroadcastReceiver { public override void OnReceive(Context context, Intent intent) diff --git a/src/keepass2android/services/CopyToClipboardService.cs b/src/keepass2android/services/CopyToClipboardService.cs index 9761c3c36..52b67c870 100644 --- a/src/keepass2android/services/CopyToClipboardService.cs +++ b/src/keepass2android/services/CopyToClipboardService.cs @@ -212,8 +212,8 @@ private NotificationCompat.Builder GetNotificationBuilder(string intentText, int private PendingIntent GetPendingIntent(string intentText, int descResId) { PendingIntent pending; - Intent intent = new Intent(intentText); - intent.SetPackage(_ctx.PackageName); + Intent intent = new Intent(_ctx, typeof(CopyToClipboardBroadcastReceiver)); + intent.SetAction(intentText); pending = PendingIntent.GetBroadcast(_ctx, descResId, intent, PendingIntentFlags.CancelCurrent); return pending; } @@ -454,12 +454,7 @@ public void DisplayAccessNotifications(PwEntryOutput entry, bool closeAfterCreat StopSelf(); return; } - - IntentFilter filter = new IntentFilter(); - filter.AddAction(Intents.CopyUsername); - filter.AddAction(Intents.CopyPassword); - filter.AddAction(Intents.CheckKeyboard); - + //register receiver to get notified when notifications are discarded in which case we can shutdown the service _notificationDeletedBroadcastReceiver = new NotificationDeletedBroadcastReceiver(this); IntentFilter deletefilter = new IntentFilter(); @@ -757,7 +752,6 @@ private string Kp2aInputMethodName } [BroadcastReceiver(Permission = "keepass2android." + AppNames.PackagePart + ".permission.CopyToClipboard")] - [IntentFilter(new[] { Intents.CopyUsername, Intents.CopyPassword, Intents.CheckKeyboard })] class CopyToClipboardBroadcastReceiver : BroadcastReceiver { public CopyToClipboardBroadcastReceiver(IntPtr javaReference, JniHandleOwnership transfer) diff --git a/src/keepass2android/services/OngoingNotificationsService.cs b/src/keepass2android/services/OngoingNotificationsService.cs index 17925a671..976a13739 100644 --- a/src/keepass2android/services/OngoingNotificationsService.cs +++ b/src/keepass2android/services/OngoingNotificationsService.cs @@ -163,7 +163,7 @@ private Notification GetQuickUnlockNotification() builder.SetContentIntent(GetSwitchToAppPendingIntent()); // Additional action to allow locking the database builder.AddAction(Android.Resource.Drawable.IcLockLock, GetString(Resource.String.QuickUnlock_lockButton), - PendingIntent.GetBroadcast(this, 0, new Intent(Intents.CloseDatabase), PendingIntentFlags.UpdateCurrent)); + PendingIntent.GetBroadcast(this, 0, new Intent(this, typeof(ApplicationBroadcastReceiver)).SetAction(Intents.CloseDatabase), PendingIntentFlags.UpdateCurrent)); return builder.Build(); @@ -208,7 +208,7 @@ private Notification GetUnlockedNotification() // Default action is to show Kp2A builder.SetContentIntent(GetSwitchToAppPendingIntent()); // Additional action to allow locking the database - builder.AddAction(Resource.Drawable.ic_action_lock, GetString(Resource.String.menu_lock), PendingIntent.GetBroadcast(this, 0, new Intent(Intents.LockDatabase), PendingIntentFlags.UpdateCurrent)); + builder.AddAction(Resource.Drawable.ic_action_lock, GetString(Resource.String.menu_lock), PendingIntent.GetBroadcast(this, 0, new Intent(this, typeof(ApplicationBroadcastReceiver)).SetAction(Intents.LockDatabase), PendingIntentFlags.UpdateCurrent)); return builder.Build(); } diff --git a/src/keepass2android/timeout/TimeoutHelper.cs b/src/keepass2android/timeout/TimeoutHelper.cs index 04ca4425e..c49e6383f 100644 --- a/src/keepass2android/timeout/TimeoutHelper.cs +++ b/src/keepass2android/timeout/TimeoutHelper.cs @@ -35,7 +35,7 @@ private static class Timeout private static PendingIntent BuildIntent(Context ctx) { - return PendingIntent.GetBroadcast(ctx, 0, new Intent(Intents.LockDatabase), PendingIntentFlags.UpdateCurrent); + return PendingIntent.GetBroadcast(ctx, 0, new Intent(ctx, typeof(ApplicationBroadcastReceiver)).SetAction(Intents.LockDatabase), PendingIntentFlags.UpdateCurrent); } public static void Start(Context ctx) From 84875553159a200902745b124627f7107ce62526 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Mon, 22 Jan 2018 12:48:40 +0100 Subject: [PATCH 03/75] fix issue with displaying long passwords by using two different TextViews for the visible and "protected" password view, toggling visibility instead of InputType. Fixes #96. --- src/keepass2android/EntryActivity.cs | 113 +++++++++++------- .../StandardStringView.cs | 33 ++--- .../layout/entry_extrastring_value.xml | 7 ++ .../Resources/layout/entry_view_contents.xml | 6 + 4 files changed, 102 insertions(+), 57 deletions(-) diff --git a/src/keepass2android/EntryActivity.cs b/src/keepass2android/EntryActivity.cs index a3c1a621b..f07d4b0a1 100644 --- a/src/keepass2android/EntryActivity.cs +++ b/src/keepass2android/EntryActivity.cs @@ -91,7 +91,14 @@ public EntryActivity() private int _pos; AppTask _appTask; - private List _protectedTextViews; + + struct ProtectedTextviewGroup + { + public TextView ProtectedField; + public TextView VisibleProtectedField; + } + + private List _protectedTextViews; private IMenu _menu; private readonly Dictionary> _popupMenuItems = @@ -476,14 +483,24 @@ private ExtraStringView CreateExtraSection(string key, string value, bool isProt RelativeLayout valueViewContainer = (RelativeLayout) LayoutInflater.Inflate(Resource.Layout.entry_extrastring_value, null); var valueView = valueViewContainer.FindViewById(Resource.Id.entry_extra); - if (value != null) - valueView.Text = value; - SetPasswordTypeface(valueView); - if (isProtected) - { - RegisterProtectedTextView(valueView); - valueView.TransformationMethod = PasswordTransformationMethod.Instance; - } + var valueViewVisible = valueViewContainer.FindViewById(Resource.Id.entry_extra_visible); + if (value != null) + { + valueView.Text = value; + valueViewVisible.Text = value; + + } + SetPasswordTypeface(valueViewVisible); + if (isProtected) + { + RegisterProtectedTextView(valueView, valueViewVisible); + //valueView.TransformationMethod = PasswordTransformationMethod.Instance; + + } + else + { + valueView.Visibility = ViewStates.Gone; + } layout.AddView(valueViewContainer); var stringView = new ExtraStringView(layout, valueView, keyView); @@ -599,9 +616,9 @@ internal void OpenBinaryFile(Android.Net.Uri uri) - private void RegisterProtectedTextView(TextView protectedTextView) + private void RegisterProtectedTextView(TextView protectedTextView, TextView visibleTextView) { - _protectedTextViews.Add(protectedTextView); + _protectedTextViews.Add(new ProtectedTextviewGroup { ProtectedField = protectedTextView, VisibleProtectedField = visibleTextView}); } @@ -687,7 +704,7 @@ public override void OnBackPressed() protected void FillData() { - _protectedTextViews = new List(); + _protectedTextViews = new List(); ImageView iv = (ImageView) FindViewById(Resource.Id.icon); if (iv != null) { @@ -704,9 +721,9 @@ protected void FillData() PopulateStandardText(Resource.Id.entry_user_name, Resource.Id.entryfield_container_username, PwDefs.UserNameField); PopulateStandardText(Resource.Id.entry_url, Resource.Id.entryfield_container_url, PwDefs.UrlField); - PopulateStandardText(Resource.Id.entry_password, Resource.Id.entryfield_container_password, PwDefs.PasswordField); - RegisterProtectedTextView(FindViewById(Resource.Id.entry_password)); - SetPasswordTypeface(FindViewById(Resource.Id.entry_password)); + PopulateStandardText(new List { Resource.Id.entry_password, Resource.Id.entry_password_visible}, Resource.Id.entryfield_container_password, PwDefs.PasswordField); + + RegisterProtectedTextView(FindViewById(Resource.Id.entry_password), FindViewById(Resource.Id.entry_password_visible)); RegisterTextPopup(FindViewById (Resource.Id.groupname_container), FindViewById (Resource.Id.entry_group_name), KeyGroupFullPath); @@ -820,28 +837,43 @@ private void SetPasswordTypeface(TextView textView) textView.Typeface = _passwordFont; } - private void PopulateText(int viewId, int containerViewId, String text) + private void PopulateText(int viewId, int containerViewId, String text) + { + PopulateText(new List {viewId}, containerViewId, text); + } + + + private void PopulateText(List viewIds, int containerViewId, String text) { View container = FindViewById(containerViewId); - TextView tv = (TextView) FindViewById(viewId); - if (String.IsNullOrEmpty(text)) - { - container.Visibility = tv.Visibility = ViewStates.Gone; - } - else - { - container.Visibility = tv.Visibility = ViewStates.Visible; - tv.Text = text; - - } + foreach (int viewId in viewIds) + { + TextView tv = (TextView) FindViewById(viewId); + if (String.IsNullOrEmpty(text)) + { + container.Visibility = tv.Visibility = ViewStates.Gone; + } + else + { + container.Visibility = tv.Visibility = ViewStates.Visible; + tv.Text = text; + + } + } } - private void PopulateStandardText(int viewId, int containerViewId, String key) + private void PopulateStandardText(int viewId, int containerViewId, String key) + { + PopulateStandardText(new List {viewId}, containerViewId, key); + } + + + private void PopulateStandardText(List viewIds, int containerViewId, String key) { String value = Entry.Strings.ReadSafe(key); value = SprEngine.Compile(value, new SprContext(Entry, App.Kp2a.GetDb().KpDatabase, SprCompileFlags.All)); - PopulateText(viewId, containerViewId, value); - _stringViews.Add(key, new StandardStringView(viewId, containerViewId, this)); + PopulateText(viewIds, containerViewId, value); + _stringViews.Add(key, new StandardStringView(viewIds, containerViewId, this)); } private void PopulateGroupText(int viewId, int containerViewId, String key) @@ -853,7 +885,7 @@ private void PopulateGroupText(int viewId, int containerViewId, String key) groupName = Entry.ParentGroup.GetFullPath(); } PopulateText(viewId, containerViewId, groupName); - _stringViews.Add (key, new StandardStringView (viewId, containerViewId, this)); + _stringViews.Add (key, new StandardStringView (new List{viewId}, containerViewId, this)); } private void RequiresRefresh() @@ -932,20 +964,15 @@ private void UpdateTogglePasswordMenu() private void SetPasswordStyle() { - foreach (TextView password in _protectedTextViews) + foreach (ProtectedTextviewGroup group in _protectedTextViews) { - if (_showPassword) - { - //password.TransformationMethod = null; - password.InputType = password.InputType = InputTypes.ClassText | InputTypes.TextVariationVisiblePassword; - SetPasswordTypeface(password); - } - else - { - //password.TransformationMethod = PasswordTransformationMethod.Instance; - password.InputType = InputTypes.ClassText | InputTypes.TextVariationPassword; - } + group.VisibleProtectedField.Visibility = _showPassword ? ViewStates.Visible : ViewStates.Gone; + group.ProtectedField.Visibility = !_showPassword ? ViewStates.Visible : ViewStates.Gone; + + SetPasswordTypeface(group.VisibleProtectedField); + + group.ProtectedField.InputType = InputTypes.ClassText | InputTypes.TextVariationPassword; } } diff --git a/src/keepass2android/EntryActivityClasses/StandardStringView.cs b/src/keepass2android/EntryActivityClasses/StandardStringView.cs index bcbcb0198..c307a3c28 100644 --- a/src/keepass2android/EntryActivityClasses/StandardStringView.cs +++ b/src/keepass2android/EntryActivityClasses/StandardStringView.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Android.App; using Android.Views; using Android.Widget; @@ -7,13 +9,13 @@ namespace keepass2android { internal class StandardStringView : IStringView { - private readonly int _viewId; + private readonly List _viewIds; private readonly int _containerViewId; private readonly Activity _activity; - public StandardStringView(int viewId, int containerViewId, Activity activity) + public StandardStringView(List viewIds, int containerViewId, Activity activity) { - _viewId = viewId; + _viewIds = viewIds; _containerViewId = containerViewId; _activity = activity; } @@ -23,20 +25,23 @@ public string Text set { View container = _activity.FindViewById(_containerViewId); - TextView tv = (TextView) _activity.FindViewById(_viewId); - if (String.IsNullOrEmpty(value)) - { - container.Visibility = tv.Visibility = ViewStates.Gone; - } - else - { - container.Visibility = tv.Visibility = ViewStates.Visible; - tv.Text = value; - } + foreach (int viewId in _viewIds) + { + TextView tv = (TextView) _activity.FindViewById(viewId); + if (String.IsNullOrEmpty(value)) + { + container.Visibility = tv.Visibility = ViewStates.Gone; + } + else + { + container.Visibility = tv.Visibility = ViewStates.Visible; + tv.Text = value; + } + } } get { - TextView tv = (TextView) _activity.FindViewById(_viewId); + TextView tv = (TextView) _activity.FindViewById(_viewIds.First()); return tv.Text; } } diff --git a/src/keepass2android/Resources/layout/entry_extrastring_value.xml b/src/keepass2android/Resources/layout/entry_extrastring_value.xml index b5d14fe1f..0bc1272eb 100644 --- a/src/keepass2android/Resources/layout/entry_extrastring_value.xml +++ b/src/keepass2android/Resources/layout/entry_extrastring_value.xml @@ -22,4 +22,11 @@ android:typeface="monospace" android:layout_toLeftOf="@id/extra_vdots" style="@style/EntryItem" /> + \ No newline at end of file diff --git a/src/keepass2android/Resources/layout/entry_view_contents.xml b/src/keepass2android/Resources/layout/entry_view_contents.xml index 8c05ad386..4d08fa4f0 100644 --- a/src/keepass2android/Resources/layout/entry_view_contents.xml +++ b/src/keepass2android/Resources/layout/entry_view_contents.xml @@ -174,6 +174,12 @@ android:typeface="monospace" android:layout_toLeftOf="@id/password_vdots" style="@style/EntryItem" /> + From c4e67db75f929928cead407c532452dd8257f745 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Mon, 22 Jan 2018 13:07:04 +0100 Subject: [PATCH 04/75] fix QuickUnlock with Unicode characters like emojis (length correction was incorrect or misleading), fixes #161 --- src/keepass2android/QuickUnlock.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/keepass2android/QuickUnlock.cs b/src/keepass2android/QuickUnlock.cs index 450004385..6a49877d0 100644 --- a/src/keepass2android/QuickUnlock.cs +++ b/src/keepass2android/QuickUnlock.cs @@ -340,8 +340,13 @@ private string ExpectedPasswordPart { KcpPassword kcpPassword = (KcpPassword) App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey(typeof (KcpPassword)); String password = kcpPassword.Password.ReadString(); - String expectedPasswordPart = password.Substring(Math.Max(0, password.Length - _quickUnlockLength), - Math.Min(password.Length, _quickUnlockLength)); + + var passwordStringInfo = new System.Globalization.StringInfo(password); + + int passwordLength = passwordStringInfo.LengthInTextElements; + + String expectedPasswordPart = passwordStringInfo.SubstringByTextElements(Math.Max(0, passwordLength - _quickUnlockLength), + Math.Min(passwordLength, _quickUnlockLength)); return expectedPasswordPart; } } From 9fe1a904c8968881063fc29c984d20beb63fd1c5 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 23 Jan 2018 19:11:01 +0100 Subject: [PATCH 05/75] allow to hide the length of the QuickUnlock code. Closes #52 --- src/keepass2android/QuickUnlock.cs | 11 ++++++++++- src/keepass2android/Resources/values/config.xml | 1 + src/keepass2android/Resources/values/strings.xml | 4 ++++ src/keepass2android/Resources/xml/preferences.xml | 8 ++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/keepass2android/QuickUnlock.cs b/src/keepass2android/QuickUnlock.cs index 6a49877d0..a04543d68 100644 --- a/src/keepass2android/QuickUnlock.cs +++ b/src/keepass2android/QuickUnlock.cs @@ -107,7 +107,16 @@ protected override void OnCreate(Bundle bundle) _quickUnlockLength = App.Kp2a.QuickUnlockKeyLength; - txtLabel.Text = GetString(Resource.String.QuickUnlock_label, new Java.Lang.Object[] {_quickUnlockLength}); + if (PreferenceManager.GetDefaultSharedPreferences(this) + .GetBoolean(GetString(Resource.String.QuickUnlockHideLength_key), false)) + { + txtLabel.Text = GetString(Resource.String.QuickUnlock_label_secure); + } + else + { + txtLabel.Text = GetString(Resource.String.QuickUnlock_label, new Java.Lang.Object[] { _quickUnlockLength }); + } + EditText pwd = (EditText) FindViewById(Resource.Id.QuickUnlock_password); pwd.SetEms(_quickUnlockLength); diff --git a/src/keepass2android/Resources/values/config.xml b/src/keepass2android/Resources/values/config.xml index 86b60f8c1..b3ba7a6fd 100644 --- a/src/keepass2android/Resources/values/config.xml +++ b/src/keepass2android/Resources/values/config.xml @@ -97,6 +97,7 @@ FileHandling_prefs_key keyboardswitch_prefs_key AutoFill_prefs_key + QuickUnlockHideLength_key OfflineMode_key diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index 17d974b09..d585b0ec4 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -290,6 +290,7 @@ File to import will be selected in the next step. Enable QuickUnlock Enter last %1$d characters of your password: + Enter QuickUnlock code: QuickUnlock! Close database Enable QuickUnlock by default @@ -302,6 +303,9 @@ QuickUnlock requires a notification to work properly. Select this option to display a notification without an icon. Length of QuickUnlock key Maximum number of characters used as QuickUnlock password. + Hide QuickUnlock length + If enabled, the length of the QuickUnlock code is not displayed on the QuickUnlock screen. + QuickUnlock failed: incorrect password! File attachments directory Directory where file attachments are saved to. diff --git a/src/keepass2android/Resources/xml/preferences.xml b/src/keepass2android/Resources/xml/preferences.xml index 5ab3772e5..90a209322 100644 --- a/src/keepass2android/Resources/xml/preferences.xml +++ b/src/keepass2android/Resources/xml/preferences.xml @@ -423,6 +423,14 @@ android:defaultValue="true" android:title="@string/QuickUnlockIconHidden16_title" android:key="@string/QuickUnlockIconHidden16_key" /> + + Date: Tue, 23 Jan 2018 19:52:33 +0100 Subject: [PATCH 06/75] fix display issue with dynamic fields and visible passwords (related to #96) --- src/keepass2android/EntryActivity.cs | 27 +++++++++++-------- .../EntryActivityClasses/ExtraStringView.cs | 15 ++++++----- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/keepass2android/EntryActivity.cs b/src/keepass2android/EntryActivity.cs index f07d4b0a1..fcefa04fc 100644 --- a/src/keepass2android/EntryActivity.cs +++ b/src/keepass2android/EntryActivity.cs @@ -494,7 +494,6 @@ private ExtraStringView CreateExtraSection(string key, string value, bool isProt if (isProtected) { RegisterProtectedTextView(valueView, valueViewVisible); - //valueView.TransformationMethod = PasswordTransformationMethod.Instance; } else @@ -503,7 +502,7 @@ private ExtraStringView CreateExtraSection(string key, string value, bool isProt } layout.AddView(valueViewContainer); - var stringView = new ExtraStringView(layout, valueView, keyView); + var stringView = new ExtraStringView(layout, valueView, valueViewVisible, keyView); _stringViews.Add(key, stringView); RegisterTextPopup(valueViewContainer, valueViewContainer.FindViewById(Resource.Id.extra_vdots), key, isProtected); @@ -618,7 +617,9 @@ internal void OpenBinaryFile(Android.Net.Uri uri) private void RegisterProtectedTextView(TextView protectedTextView, TextView visibleTextView) { - _protectedTextViews.Add(new ProtectedTextviewGroup { ProtectedField = protectedTextView, VisibleProtectedField = visibleTextView}); + var protectedTextviewGroup = new ProtectedTextviewGroup { ProtectedField = protectedTextView, VisibleProtectedField = visibleTextView}; + _protectedTextViews.Add(protectedTextviewGroup); + SetPasswordStyle(protectedTextviewGroup); } @@ -965,18 +966,22 @@ private void UpdateTogglePasswordMenu() private void SetPasswordStyle() { foreach (ProtectedTextviewGroup group in _protectedTextViews) - { + { + SetPasswordStyle(group); + } + } - group.VisibleProtectedField.Visibility = _showPassword ? ViewStates.Visible : ViewStates.Gone; - group.ProtectedField.Visibility = !_showPassword ? ViewStates.Visible : ViewStates.Gone; + private void SetPasswordStyle(ProtectedTextviewGroup group) + { + group.VisibleProtectedField.Visibility = _showPassword ? ViewStates.Visible : ViewStates.Gone; + group.ProtectedField.Visibility = !_showPassword ? ViewStates.Visible : ViewStates.Gone; - SetPasswordTypeface(group.VisibleProtectedField); + SetPasswordTypeface(group.VisibleProtectedField); - group.ProtectedField.InputType = InputTypes.ClassText | InputTypes.TextVariationPassword; - } - } + group.ProtectedField.InputType = InputTypes.ClassText | InputTypes.TextVariationPassword; + } - protected override void OnResume() + protected override void OnResume() { ClearCache(); base.OnResume(); diff --git a/src/keepass2android/EntryActivityClasses/ExtraStringView.cs b/src/keepass2android/EntryActivityClasses/ExtraStringView.cs index e65585fea..08f9561b2 100644 --- a/src/keepass2android/EntryActivityClasses/ExtraStringView.cs +++ b/src/keepass2android/EntryActivityClasses/ExtraStringView.cs @@ -8,13 +8,15 @@ internal class ExtraStringView : IStringView { private readonly View _container; private readonly TextView _valueView; - private readonly TextView _keyView; + private readonly TextView _visibleValueView; + private readonly TextView _keyView; - public ExtraStringView(LinearLayout container, TextView valueView, TextView keyView) + public ExtraStringView(LinearLayout container, TextView valueView, TextView visibleValueView, TextView keyView) { _container = container; _valueView = valueView; - _keyView = keyView; + _visibleValueView = visibleValueView; + _keyView = keyView; } public View View @@ -29,16 +31,15 @@ public string Text { if (String.IsNullOrEmpty(value)) { - _valueView.Visibility = ViewStates.Gone; - _keyView.Visibility = ViewStates.Gone; _container.Visibility = ViewStates.Gone; } else { - _valueView.Visibility = ViewStates.Visible; - _keyView.Visibility = ViewStates.Visible; _container.Visibility = ViewStates.Visible; _valueView.Text = value; + if (_visibleValueView != null) + _visibleValueView.Text = value; + } } } From 002c67e48c2864674131830ec61088d537c85552 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 23 Jan 2018 19:53:06 +0100 Subject: [PATCH 07/75] support otpauth:// URIs in otp field as used by KeeWeb (closes #118) --- .../Totp/KeeWebOtpPluginAdapter.cs | 43 +++++++++++++++++++ src/keepass2android/Totp/Kp2aTotp.cs | 3 +- src/keepass2android/keepass2android.csproj | 2 + 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/keepass2android/Totp/KeeWebOtpPluginAdapter.cs diff --git a/src/keepass2android/Totp/KeeWebOtpPluginAdapter.cs b/src/keepass2android/Totp/KeeWebOtpPluginAdapter.cs new file mode 100644 index 000000000..ab53bcb35 --- /dev/null +++ b/src/keepass2android/Totp/KeeWebOtpPluginAdapter.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Web; +using Android.Content; +using PluginTOTP; + +namespace keepass2android +{ + internal class KeeWebOtpPluginAdapter : ITotpPluginAdapter + { + public TotpData GetTotpData(IDictionary entryFields, Context ctx, bool muteWarnings) + { + TotpData res = new TotpData(); + string data; + if (!entryFields.TryGetValue("otp", out data)) + { + return res; + } + + string otpUriStart = "otpauth://totp/"; + + if (!data.StartsWith(otpUriStart)) + return res; + + + try + { + Uri myUri = new Uri(data); + var parsedQuery = HttpUtility.ParseQueryString(myUri.Query); + res.TotpSeed = parsedQuery.Get("secret"); + res.Length = parsedQuery.Get("digits"); + res.Duration = parsedQuery.Get("period"); + } + catch (Exception) + { + return res; + } + + res.IsTotpEnry = true; + return res; + } + } +} \ No newline at end of file diff --git a/src/keepass2android/Totp/Kp2aTotp.cs b/src/keepass2android/Totp/Kp2aTotp.cs index 04f4af99c..9a67b71c5 100644 --- a/src/keepass2android/Totp/Kp2aTotp.cs +++ b/src/keepass2android/Totp/Kp2aTotp.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Text; using Android.App; @@ -11,7 +10,7 @@ namespace keepass2android class Kp2aTotp { - readonly ITotpPluginAdapter[] _pluginAdapters = new ITotpPluginAdapter[] { new TrayTotpPluginAdapter(), new KeeOtpPluginAdapter() }; + readonly ITotpPluginAdapter[] _pluginAdapters = new ITotpPluginAdapter[] { new TrayTotpPluginAdapter(), new KeeOtpPluginAdapter(), new KeeWebOtpPluginAdapter() }; public void OnOpenEntry() { diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index d2307ba40..d77654659 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -102,6 +102,7 @@ + @@ -255,6 +256,7 @@ + From b98676ea77e3078bea078d5f1a3f58b90e92f4ff Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 23 Jan 2018 20:03:05 +0100 Subject: [PATCH 08/75] avoid crash when IconSet was uninstalled (fixes #139) --- src/keepass2android/icons/DrawableFactory.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/keepass2android/icons/DrawableFactory.cs b/src/keepass2android/icons/DrawableFactory.cs index 78085c509..ea82ccf23 100644 --- a/src/keepass2android/icons/DrawableFactory.cs +++ b/src/keepass2android/icons/DrawableFactory.cs @@ -101,7 +101,21 @@ public Drawable GetIconDrawable (Context context, PwIcon icon, bool forGroup) { string packageName = PreferenceManager.GetDefaultSharedPreferences(Application.Context).GetString("IconSetKey", context.PackageName); - Resources res = context.PackageManager.GetResourcesForApplication(packageName); + Resources res; + try + { + res = context.PackageManager.GetResourcesForApplication(packageName); + } + catch (Exception) + { + //can happen after uninstalling icons + packageName = context.PackageName; + res = context.PackageManager.GetResourcesForApplication(packageName); + PreferenceManager.GetDefaultSharedPreferences(Application.Context) + .Edit() + .PutString("IconSetKey", packageName) + .Commit(); + } try { From c34e38e50f0160349ee2f9ce7366411d275f50f7 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 23 Jan 2018 20:23:23 +0100 Subject: [PATCH 09/75] allow to import content-URI database files to internal folder (closes #158) --- src/keepass2android/settings/DatabaseSettingsActivity.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/keepass2android/settings/DatabaseSettingsActivity.cs b/src/keepass2android/settings/DatabaseSettingsActivity.cs index f0aa308a7..e54ad6deb 100644 --- a/src/keepass2android/settings/DatabaseSettingsActivity.cs +++ b/src/keepass2android/settings/DatabaseSettingsActivity.cs @@ -863,7 +863,9 @@ private void UpdateImportDbPref() { //Import db/key file preferences: Preference importDb = FindPreference("import_db_prefs"); - if (!App.Kp2a.GetDb().Ioc.IsLocalFile()) + bool isLocalOrContent = + App.Kp2a.GetDb().Ioc.IsLocalFile() || App.Kp2a.GetDb().Ioc.Path.StartsWith("content://"); + if (!isLocalOrContent) { importDb.Summary = GetString(Resource.String.OnlyAvailableForLocalFiles); importDb.Enabled = false; From b993be46588168f083ec643677109cb6d2c8ba2d Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 23 Jan 2018 23:09:17 +0100 Subject: [PATCH 10/75] add some info texts, especialy for novice users to avoid some common misunderstandings. closes #46, closes #47 --- .../Io/AndroidContentStorage.cs | 7 +- .../Io/BuiltInFileStorage.cs | 7 +- .../Io/CachingFileStorage.cs | 7 +- .../Io/DropboxFileStorage.cs | 14 +- src/Kp2aBusinessLogic/Io/GDriveFileStorage.cs | 4 + src/Kp2aBusinessLogic/Io/IFileStorage.cs | 9 +- src/Kp2aBusinessLogic/Io/JavaFileStorage.cs | 3 +- src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs | 7 +- .../Io/OfflineSwitchableFileStorage.cs | 7 +- .../Io/OneDriveFileStorage.cs | 5 + src/Kp2aBusinessLogic/Io/SftpFileStorage.cs | 6 +- src/Kp2aBusinessLogic/Io/WebDavFileStorage.cs | 7 +- src/keepass2android/GroupBaseActivity.cs | 2070 +++++++++-------- .../Resources/layout/group.xml | 57 + .../Resources/values/strings.xml | 15 + 15 files changed, 1243 insertions(+), 982 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/AndroidContentStorage.cs b/src/Kp2aBusinessLogic/Io/AndroidContentStorage.cs index 56cdfc1a5..47a51182b 100644 --- a/src/Kp2aBusinessLogic/Io/AndroidContentStorage.cs +++ b/src/Kp2aBusinessLogic/Io/AndroidContentStorage.cs @@ -28,7 +28,12 @@ public IEnumerable SupportedProtocols get { yield return "content"; } } - public void Delete(IOConnectionInfo ioc) + public bool UserShouldBackup + { + get { return true; } + } + + public void Delete(IOConnectionInfo ioc) { throw new NotImplementedException(); } diff --git a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs index 65525e5ec..1743d415e 100644 --- a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs @@ -59,7 +59,12 @@ public BuiltInFileStorage(IKp2aApp app) public abstract IEnumerable SupportedProtocols { get; } - public void Delete(IOConnectionInfo ioc) + public bool UserShouldBackup + { + get { return true; } + } + + public void Delete(IOConnectionInfo ioc) { //todo check if directory IOConnection.DeleteFile(ioc); diff --git a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs index 31d1d0bc3..4ecc25846 100644 --- a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs @@ -84,7 +84,12 @@ public void ClearCache() public IEnumerable SupportedProtocols { get { return _cachedStorage.SupportedProtocols; } } - public void DeleteFile(IOConnectionInfo ioc) + public bool UserShouldBackup + { + get { return _cachedStorage.UserShouldBackup; } + } + + public void DeleteFile(IOConnectionInfo ioc) { if (IsCached(ioc)) { diff --git a/src/Kp2aBusinessLogic/Io/DropboxFileStorage.cs b/src/Kp2aBusinessLogic/Io/DropboxFileStorage.cs index e4b3e774f..4ad644722 100644 --- a/src/Kp2aBusinessLogic/Io/DropboxFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/DropboxFileStorage.cs @@ -10,7 +10,11 @@ public DropboxFileStorage(Context ctx, IKp2aApp app) : { } - + + public override bool UserShouldBackup + { + get { return false; } + } } public partial class DropboxAppFolderFileStorage: JavaFileStorage @@ -20,8 +24,12 @@ public DropboxAppFolderFileStorage(Context ctx, IKp2aApp app) : { } - - } + public override bool UserShouldBackup + { + get { return false; } + } + + } } #endif \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/Io/GDriveFileStorage.cs b/src/Kp2aBusinessLogic/Io/GDriveFileStorage.cs index dbd5c9756..1636e2320 100644 --- a/src/Kp2aBusinessLogic/Io/GDriveFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/GDriveFileStorage.cs @@ -22,6 +22,10 @@ public GoogleDriveFileStorage(Context ctx, IKp2aApp app) : } + public override bool UserShouldBackup + { + get { return false; } + } } } #endif \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/Io/IFileStorage.cs b/src/Kp2aBusinessLogic/Io/IFileStorage.cs index 79cab4a20..33793ce5c 100644 --- a/src/Kp2aBusinessLogic/Io/IFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/IFileStorage.cs @@ -46,9 +46,14 @@ public interface IFileStorage /// /// returns the protocol ids supported by this FileStorage. Can return pseudo-protocols like "dropbox" or real protocols like "ftp" /// - IEnumerable SupportedProtocols { get; } + IEnumerable SupportedProtocols { get; } - /// + /// + /// returns true if users should backup files on this file storage (if the file is important). Can be false for cloud providers with built-in versioning or backups. + /// + bool UserShouldBackup { get; } + + /// /// Deletes the given file or directory. /// void Delete(IOConnectionInfo ioc); diff --git a/src/Kp2aBusinessLogic/Io/JavaFileStorage.cs b/src/Kp2aBusinessLogic/Io/JavaFileStorage.cs index a7173d260..0e6870e07 100644 --- a/src/Kp2aBusinessLogic/Io/JavaFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/JavaFileStorage.cs @@ -22,9 +22,10 @@ public abstract class JavaFileStorage: IFileStorage, IPermissionRequestingFileSt protected string Protocol { get { return _jfs.ProtocolId; } } public virtual IEnumerable SupportedProtocols { get { yield return Protocol; } } + public abstract bool UserShouldBackup { get; } - private readonly IJavaFileStorage _jfs; + private readonly IJavaFileStorage _jfs; private readonly IKp2aApp _app; public JavaFileStorage(IJavaFileStorage jfs, IKp2aApp app) diff --git a/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs b/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs index 5e19d66a4..8db18f0a8 100644 --- a/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs @@ -161,7 +161,12 @@ public IEnumerable SupportedProtocols } } - public void Delete(IOConnectionInfo ioc) + public bool UserShouldBackup + { + get { return true; } + } + + public void Delete(IOConnectionInfo ioc) { try { diff --git a/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs b/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs index bf93653ec..725b7f31e 100644 --- a/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs @@ -32,7 +32,12 @@ public IEnumerable SupportedProtocols get { return _baseStorage.SupportedProtocols; } } - public void Delete(IOConnectionInfo ioc) + public bool UserShouldBackup + { + get { return _baseStorage.UserShouldBackup; } + } + + public void Delete(IOConnectionInfo ioc) { _baseStorage.Delete(ioc); } diff --git a/src/Kp2aBusinessLogic/Io/OneDriveFileStorage.cs b/src/Kp2aBusinessLogic/Io/OneDriveFileStorage.cs index c5d522a8c..25ff4d9b2 100644 --- a/src/Kp2aBusinessLogic/Io/OneDriveFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/OneDriveFileStorage.cs @@ -33,6 +33,11 @@ public override IEnumerable SupportedProtocols yield return "onedrive"; } } + + public override bool UserShouldBackup + { + get { return false; } + } } } #endif \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/Io/SftpFileStorage.cs b/src/Kp2aBusinessLogic/Io/SftpFileStorage.cs index ce2f3a9c3..7f54a577a 100644 --- a/src/Kp2aBusinessLogic/Io/SftpFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/SftpFileStorage.cs @@ -10,7 +10,11 @@ public SftpFileStorage(IKp2aApp app) : { } - + + public override bool UserShouldBackup + { + get { return true; } + } } diff --git a/src/Kp2aBusinessLogic/Io/WebDavFileStorage.cs b/src/Kp2aBusinessLogic/Io/WebDavFileStorage.cs index 4459d4f83..cb4890b92 100644 --- a/src/Kp2aBusinessLogic/Io/WebDavFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/WebDavFileStorage.cs @@ -33,7 +33,12 @@ public override IEnumerable SupportedProtocols } } - public static string Owncloud2Webdav(string owncloudUrl) + public override bool UserShouldBackup + { + get { return true; } + } + + public static string Owncloud2Webdav(string owncloudUrl) { string owncloudPrefix = "owncloud://"; if (owncloudUrl.StartsWith(owncloudPrefix)) diff --git a/src/keepass2android/GroupBaseActivity.cs b/src/keepass2android/GroupBaseActivity.cs index f0c58970a..7249a80df 100644 --- a/src/keepass2android/GroupBaseActivity.cs +++ b/src/keepass2android/GroupBaseActivity.cs @@ -43,1035 +43,1167 @@ You should have received a copy of the GNU General Public License namespace keepass2android { - public abstract class GroupBaseActivity : LockCloseActivity - { - public const String KeyEntry = "entry"; - public const String KeyMode = "mode"; + public abstract class GroupBaseActivity : LockCloseActivity + { + public const String KeyEntry = "entry"; + public const String KeyMode = "mode"; static readonly Dictionary bottomBarElementsPriority = new Dictionary() { { Resource.Id.cancel_insert_element, 20 }, { Resource.Id.insert_element, 20 }, { Resource.Id.autofill_infotext, 10 }, + { Resource.Id.infotext, 11 }, { Resource.Id.select_other_entry, 20}, { Resource.Id.add_url_entry, 20}, }; - private readonly HashSet showableBottomBarElements = new HashSet(); + private readonly HashSet showableBottomBarElements = new HashSet(); - private ActivityDesign _design; + private ActivityDesign _design; - public virtual void LaunchActivityForEntry(PwEntry pwEntry, int pos) - { - EntryActivity.Launch(this, pwEntry, pos, AppTask); - } + public virtual void LaunchActivityForEntry(PwEntry pwEntry, int pos) + { + EntryActivity.Launch(this, pwEntry, pos, AppTask); + } - protected GroupBaseActivity() - { - _design = new ActivityDesign(this); - } + protected GroupBaseActivity() + { + _design = new ActivityDesign(this); + } - protected GroupBaseActivity(IntPtr javaReference, JniHandleOwnership transfer) - : base(javaReference, transfer) - { + protected GroupBaseActivity(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) + { - } + } - protected override void OnSaveInstanceState(Bundle outState) - { - base.OnSaveInstanceState(outState); - AppTask.ToBundle(outState); - } + protected override void OnSaveInstanceState(Bundle outState) + { + base.OnSaveInstanceState(outState); + AppTask.ToBundle(outState); + } - public virtual void SetupNormalButtons() - { - SetNormalButtonVisibility(AddGroupEnabled, AddEntryEnabled); - } + public virtual void SetupNormalButtons() + { + SetNormalButtonVisibility(AddGroupEnabled, AddEntryEnabled); + } - protected virtual bool AddGroupEnabled - { - get { return App.Kp2a.GetDb().CanWrite; } - } - protected virtual bool AddEntryEnabled - { - get { return App.Kp2a.GetDb().CanWrite; } - } + protected virtual bool AddGroupEnabled + { + get { return App.Kp2a.GetDb().CanWrite; } + } + protected virtual bool AddEntryEnabled + { + get { return App.Kp2a.GetDb().CanWrite; } + } - public void SetNormalButtonVisibility(bool showAddGroup, bool showAddEntry) - { - if (FindViewById(Resource.Id.fabCancelAddNew) != null) - { - FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNewGroup).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNewEntry).Visibility = ViewStates.Gone; + public void SetNormalButtonVisibility(bool showAddGroup, bool showAddEntry) + { + if (FindViewById(Resource.Id.fabCancelAddNew) != null) + { + FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNewGroup).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNewEntry).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNew).Visibility = (showAddGroup || showAddEntry) ? ViewStates.Visible : ViewStates.Gone; - } + FindViewById(Resource.Id.fabAddNew).Visibility = (showAddGroup || showAddEntry) ? ViewStates.Visible : ViewStates.Gone; + } - UpdateBottomBarElementVisibility(Resource.Id.insert_element, false); - UpdateBottomBarElementVisibility(Resource.Id.cancel_insert_element, false); + UpdateBottomBarElementVisibility(Resource.Id.insert_element, false); + UpdateBottomBarElementVisibility(Resource.Id.cancel_insert_element, false); } void UpdateBottomBarVisibility() - { - var bottomBar = FindViewById(Resource.Id.bottom_bar); - //check for null because the "empty" layouts may not have all views - int highestPrio = -1; - HashSet highestPrioElements = new HashSet(); + { + var bottomBar = FindViewById(Resource.Id.bottom_bar); + //check for null because the "empty" layouts may not have all views + int highestPrio = -1; + HashSet highestPrioElements = new HashSet(); if (bottomBar != null) - { - for (int i = 0; i < bottomBar.ChildCount; i++) - { - int id = bottomBar.GetChildAt(i).Id; - if (!showableBottomBarElements.Contains(id)) - continue; - int myPrio = bottomBarElementsPriority[id]; + { + for (int i = 0; i < bottomBar.ChildCount; i++) + { + int id = bottomBar.GetChildAt(i).Id; + if (!showableBottomBarElements.Contains(id)) + continue; + int myPrio = bottomBarElementsPriority[id]; if (!highestPrioElements.Any() || highestPrio < myPrio) - { - highestPrioElements.Clear(); - highestPrio = myPrio; - } - if (highestPrio == myPrio) - { - highestPrioElements.Add(id); + { + highestPrioElements.Clear(); + highestPrio = myPrio; + } + if (highestPrio == myPrio) + { + highestPrioElements.Add(id); } } - bottomBar.Visibility = highestPrioElements.Any() ? ViewStates.Visible : ViewStates.Gone; - - for (int i = 0; i < bottomBar.ChildCount; i++) - { - int id = bottomBar.GetChildAt(i).Id; - bottomBar.GetChildAt(i).Visibility = - highestPrioElements.Contains(id) ? ViewStates.Visible : ViewStates.Gone; - } - - if (FindViewById(Resource.Id.divider2) != null) - FindViewById(Resource.Id.divider2).Visibility = highestPrioElements.Any() ? ViewStates.Visible : ViewStates.Gone; - } - } - - - protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) - { - base.OnActivityResult(requestCode, resultCode, data); - - if (AppTask.TryGetFromActivityResult(data, ref AppTask)) - { - //make sure the app task is passed to the calling activity - AppTask.SetActivityResult(this, KeePass.ExitNormal); - - } - - if (resultCode == Result.Ok) - { - String groupName = data.Extras.GetString(GroupEditActivity.KeyName); - int groupIconId = data.Extras.GetInt(GroupEditActivity.KeyIconId); - PwUuid groupCustomIconId = - new PwUuid(MemUtil.HexStringToByteArray(data.Extras.GetString(GroupEditActivity.KeyCustomIconId))); - String strGroupUuid = data.Extras.GetString(GroupEditActivity.KeyGroupUuid); - GroupBaseActivity act = this; - Handler handler = new Handler(); - RunnableOnFinish task; - if (strGroupUuid == null) - { - task = AddGroup.GetInstance(this, App.Kp2a, groupName, groupIconId, groupCustomIconId, Group, new RefreshTask(handler, this), false); - } - else - { - PwUuid groupUuid = new PwUuid(MemUtil.HexStringToByteArray(strGroupUuid)); - task = new EditGroup(this, App.Kp2a, groupName, (PwIcon)groupIconId, groupCustomIconId, App.Kp2a.GetDb().Groups[groupUuid], - new RefreshTask(handler, this)); - } - ProgressTask pt = new ProgressTask(App.Kp2a, act, task); - pt.Run(); - } - - if (resultCode == KeePass.ExitCloseAfterTaskComplete) - { - AppTask.SetActivityResult(this, KeePass.ExitCloseAfterTaskComplete); - Finish(); - } - - if (resultCode == KeePass.ExitReloadDb) - { - AppTask.SetActivityResult(this, KeePass.ExitReloadDb); - Finish(); - } - - } - - private ISharedPreferences _prefs; - - protected PwGroup Group; - - internal AppTask AppTask; - - private String strCachedGroupUuid = null; - private IMenuItem _offlineItem; - private IMenuItem _onlineItem; - private IMenuItem _syncItem; - - - public String UuidGroup - { - get - { - if (strCachedGroupUuid == null) - { - strCachedGroupUuid = MemUtil.ByteArrayToHexString(Group.Uuid.UuidBytes); - } - return strCachedGroupUuid; - } - } - - - protected override void OnResume() - { - base.OnResume(); - _design.ReapplyTheme(); - AppTask.StartInGroupActivity(this); - AppTask.SetupGroupBaseActivityButtons(this); - - UpdateAutofillInfo(); + bottomBar.Visibility = highestPrioElements.Any() ? ViewStates.Visible : ViewStates.Gone; + + for (int i = 0; i < bottomBar.ChildCount; i++) + { + int id = bottomBar.GetChildAt(i).Id; + bottomBar.GetChildAt(i).Visibility = + highestPrioElements.Contains(id) ? ViewStates.Visible : ViewStates.Gone; + } + + if (FindViewById(Resource.Id.divider2) != null) + FindViewById(Resource.Id.divider2).Visibility = highestPrioElements.Any() ? ViewStates.Visible : ViewStates.Gone; + } + } + + + protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) + { + base.OnActivityResult(requestCode, resultCode, data); + + if (AppTask.TryGetFromActivityResult(data, ref AppTask)) + { + //make sure the app task is passed to the calling activity + AppTask.SetActivityResult(this, KeePass.ExitNormal); + + } + + if (resultCode == Result.Ok) + { + String groupName = data.Extras.GetString(GroupEditActivity.KeyName); + int groupIconId = data.Extras.GetInt(GroupEditActivity.KeyIconId); + PwUuid groupCustomIconId = + new PwUuid(MemUtil.HexStringToByteArray(data.Extras.GetString(GroupEditActivity.KeyCustomIconId))); + String strGroupUuid = data.Extras.GetString(GroupEditActivity.KeyGroupUuid); + GroupBaseActivity act = this; + Handler handler = new Handler(); + RunnableOnFinish task; + if (strGroupUuid == null) + { + task = AddGroup.GetInstance(this, App.Kp2a, groupName, groupIconId, groupCustomIconId, Group, new RefreshTask(handler, this), false); + } + else + { + PwUuid groupUuid = new PwUuid(MemUtil.HexStringToByteArray(strGroupUuid)); + task = new EditGroup(this, App.Kp2a, groupName, (PwIcon)groupIconId, groupCustomIconId, App.Kp2a.GetDb().Groups[groupUuid], + new RefreshTask(handler, this)); + } + ProgressTask pt = new ProgressTask(App.Kp2a, act, task); + pt.Run(); + } + + if (resultCode == KeePass.ExitCloseAfterTaskComplete) + { + AppTask.SetActivityResult(this, KeePass.ExitCloseAfterTaskComplete); + Finish(); + } + + if (resultCode == KeePass.ExitReloadDb) + { + AppTask.SetActivityResult(this, KeePass.ExitReloadDb); + Finish(); + } + + } + + private ISharedPreferences _prefs; + + protected PwGroup Group; + + internal AppTask AppTask; + + private String strCachedGroupUuid = null; + private IMenuItem _offlineItem; + private IMenuItem _onlineItem; + private IMenuItem _syncItem; + + + public String UuidGroup + { + get + { + if (strCachedGroupUuid == null) + { + strCachedGroupUuid = MemUtil.ByteArrayToHexString(Group.Uuid.UuidBytes); + } + return strCachedGroupUuid; + } + } + + + protected override void OnResume() + { + base.OnResume(); + _design.ReapplyTheme(); + AppTask.StartInGroupActivity(this); + AppTask.SetupGroupBaseActivityButtons(this); + + UpdateAutofillInfo(); RefreshIfDirty(); - } - - public override bool OnSearchRequested() - { - Intent i = new Intent(this, typeof(SearchActivity)); - AppTask.ToIntent(i); - StartActivityForResult(i, 0); - return true; - } - - public void RefreshIfDirty() - { - Database db = App.Kp2a.GetDb(); - if (db.Dirty.Contains(Group)) - { - db.Dirty.Remove(Group); - ListAdapter.NotifyDataSetChanged(); - - } - } - - public BaseAdapter ListAdapter - { - get { return (BaseAdapter)FragmentManager.FindFragmentById(Resource.Id.list_fragment).ListAdapter; } - } - - public virtual bool IsSearchResult - { - get { return false; } - } - - protected override void OnCreate(Bundle savedInstanceState) - { - _design.ApplyTheme(); - base.OnCreate(savedInstanceState); - - Android.Util.Log.Debug("KP2A", "Creating GBA"); - - AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent); - - // Likely the app has been killed exit the activity - if (!App.Kp2a.GetDb().Loaded) - { - Finish(); - return; - } - - _prefs = PreferenceManager.GetDefaultSharedPreferences(this); - - + } + + public override bool OnSearchRequested() + { + Intent i = new Intent(this, typeof(SearchActivity)); + AppTask.ToIntent(i); + StartActivityForResult(i, 0); + return true; + } + + public void RefreshIfDirty() + { + Database db = App.Kp2a.GetDb(); + if (db.Dirty.Contains(Group)) + { + db.Dirty.Remove(Group); + ListAdapter.NotifyDataSetChanged(); + + } + } + + public BaseAdapter ListAdapter + { + get { return (BaseAdapter)FragmentManager.FindFragmentById(Resource.Id.list_fragment).ListAdapter; } + } + + public virtual bool IsSearchResult + { + get { return false; } + } + + protected override void OnCreate(Bundle savedInstanceState) + { + _design.ApplyTheme(); + base.OnCreate(savedInstanceState); + + Android.Util.Log.Debug("KP2A", "Creating GBA"); + + AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent); + + // Likely the app has been killed exit the activity + if (!App.Kp2a.GetDb().Loaded) + { + Finish(); + return; + } + + _prefs = PreferenceManager.GetDefaultSharedPreferences(this); + + SetContentView(ContentResourceId); - if (FindViewById(Resource.Id.enable_autofill) != null) - { - FindViewById(Resource.Id.enable_autofill).Click += (sender, args) => - { - var intent = new Intent(Settings.ActionRequestSetAutofillService); - intent.SetData(Android.Net.Uri.Parse("package:" + PackageName)); - StartActivity(intent); - }; - } + if (FindViewById(Resource.Id.enable_autofill) != null) + { + FindViewById(Resource.Id.enable_autofill).Click += (sender, args) => + { + var intent = new Intent(Settings.ActionRequestSetAutofillService); + intent.SetData(Android.Net.Uri.Parse("package:" + PackageName)); + StartActivity(intent); + }; + } if (FindViewById(Resource.Id.fabCancelAddNew) != null) - { - FindViewById(Resource.Id.fabAddNew).Click += (sender, args) => - { - FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Visible; - FindViewById(Resource.Id.fabAddNewGroup).Visibility = AddGroupEnabled ? ViewStates.Visible : ViewStates.Gone; - FindViewById(Resource.Id.fabAddNewEntry).Visibility = AddEntryEnabled ? ViewStates.Visible : ViewStates.Gone; - FindViewById(Resource.Id.fabAddNew).Visibility = ViewStates.Gone; - }; - - FindViewById(Resource.Id.fabCancelAddNew).Click += (sender, args) => - { - FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNewGroup).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNewEntry).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNew).Visibility = ViewStates.Visible; - }; - - - } - - - if (FindViewById(Resource.Id.cancel_insert_element) != null) - { - FindViewById(Resource.Id.cancel_insert_element).Click += (sender, args) => StopMovingElements(); - FindViewById(Resource.Id.insert_element).Click += (sender, args) => InsertElements(); - Util.MoveBottomBarButtons(Resource.Id.cancel_insert_element, Resource.Id.insert_element, Resource.Id.bottom_bar, this); - } - - if (FindViewById(Resource.Id.show_autofill_info) != null) - { - FindViewById(Resource.Id.show_autofill_info).Click += (sender, args) => Util.GotoUrl(this, "https://philippc.github.io/keepass2android/OreoAutoFill.html"); + { + FindViewById(Resource.Id.fabAddNew).Click += (sender, args) => + { + FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.fabAddNewGroup).Visibility = AddGroupEnabled ? ViewStates.Visible : ViewStates.Gone; + FindViewById(Resource.Id.fabAddNewEntry).Visibility = AddEntryEnabled ? ViewStates.Visible : ViewStates.Gone; + FindViewById(Resource.Id.fabAddNew).Visibility = ViewStates.Gone; + }; + + FindViewById(Resource.Id.fabCancelAddNew).Click += (sender, args) => + { + FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNewGroup).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNewEntry).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNew).Visibility = ViewStates.Visible; + }; + + + } + + + if (FindViewById(Resource.Id.cancel_insert_element) != null) + { + FindViewById(Resource.Id.cancel_insert_element).Click += (sender, args) => StopMovingElements(); + FindViewById(Resource.Id.insert_element).Click += (sender, args) => InsertElements(); + Util.MoveBottomBarButtons(Resource.Id.cancel_insert_element, Resource.Id.insert_element, Resource.Id.bottom_bar, this); + } + + if (FindViewById(Resource.Id.show_autofill_info) != null) + { + FindViewById(Resource.Id.show_autofill_info).Click += (sender, args) => Util.GotoUrl(this, "https://philippc.github.io/keepass2android/OreoAutoFill.html"); Util.MoveBottomBarButtons(Resource.Id.show_autofill_info, Resource.Id.enable_autofill, Resource.Id.autofill_buttons, this); - } + } + + + string lastInfoText; + if (IsTimeForInfotext(out lastInfoText)) + { + + FingerprintUnlockMode um; + Enum.TryParse(_prefs.GetString(Database.GetFingerprintModePrefKey(App.Kp2a.GetDb().Ioc), ""), out um); + bool isFingerprintEnabled = (um == FingerprintUnlockMode.FullUnlock); + + string masterKeyKey = "MasterKey" + isFingerprintEnabled; + string emergencyKey = "Emergency"; + string backupKey = "Backup"; + + List applicableInfoTextKeys = new List {masterKeyKey}; + + if (App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc).UserShouldBackup) + { + applicableInfoTextKeys.Add(backupKey); + } + if (App.Kp2a.GetDb().Entries.Count > 15) + { + applicableInfoTextKeys.Add(emergencyKey); + } + + List enabledInfoTextKeys = new List(); + foreach (string key in applicableInfoTextKeys) + { + if (!InfoTextWasDisabled(key)) + enabledInfoTextKeys.Add(key); + } + + if (enabledInfoTextKeys.Any()) + { + string infoTextKey = "", infoHead = "", infoMain = "", infoNote = ""; + + if (enabledInfoTextKeys.Count > 1) + { + foreach (string key in enabledInfoTextKeys) + if (key == lastInfoText) + { + enabledInfoTextKeys.Remove(key); + break; + } + infoTextKey = enabledInfoTextKeys[new Random().Next(enabledInfoTextKeys.Count)]; + } + + if (infoTextKey == masterKeyKey) + { + infoHead = GetString(Resource.String.masterkey_infotext_head); + infoMain = GetString(Resource.String.masterkey_infotext_main); + if (isFingerprintEnabled) + infoNote = GetString(Resource.String.masterkey_infotext_fingerprint_note); + } + else if (infoTextKey == emergencyKey) + { + infoHead = GetString(Resource.String.emergency_infotext_head); + infoMain = GetString(Resource.String.emergency_infotext_main); + } + else if (infoTextKey == backupKey) + { + infoHead = GetString(Resource.String.backup_infotext_head); + infoMain = GetString(Resource.String.backup_infotext_main); + infoNote = GetString(Resource.String.backup_infotext_note, GetString(Resource.String.menu_app_settings), GetString(Resource.String.menu_db_settings), GetString(Resource.String.export_prefs)); + } + FindViewById(Resource.Id.info_head).Text = infoHead; + FindViewById(Resource.Id.info_main).Text = infoMain; + var additionalInfoText = FindViewById(Resource.Id.info_additional); + additionalInfoText.Text = infoNote; + additionalInfoText.Visibility = string.IsNullOrEmpty(infoNote) ? ViewStates.Gone : ViewStates.Visible; - SetResult(KeePass.ExitNormal); + if (infoTextKey != "") + { + RegisterInfoTextDisplay(infoTextKey); + FindViewById(Resource.Id.info_ok).Click += (sender, args) => + { + UpdateBottomBarElementVisibility(Resource.Id.infotext, false); + }; + FindViewById(Resource.Id.info_dont_show_again).Click += (sender, args) => + { + UpdateBottomBarElementVisibility(Resource.Id.infotext, false); + DisableInfoTextDisplay(infoTextKey); + }; + UpdateBottomBarElementVisibility(Resource.Id.infotext, true); + } + + } - } - private void UpdateAutofillInfo() - { - bool canShowAutofillInfo = false; - - if (!((Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.O) || - !((AutofillManager) GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager)))) - .IsAutofillSupported)) - { - const string autofillservicewasenabled = "AutofillServiceWasEnabled"; - if (!((AutofillManager) GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager)))) - .HasEnabledAutofillServices) - { + + } + + + + + SetResult(KeePass.ExitNormal); + + + + } + + private bool IsTimeForInfotext(out string lastInfoText) + { + DateTime lastDisplayTime = new DateTime(_prefs.GetLong("LastInfoTextTime", 0)); + lastInfoText = _prefs.GetString("LastInfoTextKey", ""); +#if DEBUG + return DateTime.UtcNow - lastDisplayTime > TimeSpan.FromSeconds(10); +#else + return DateTime.UtcNow - lastDisplayTime > TimeSpan.FromDays(3); +#endif + } + + private void DisableInfoTextDisplay(string infoTextKey) + { + _prefs + .Edit() + .PutBoolean("InfoTextDisabled_" + infoTextKey, true) + .Commit(); + + } + + private void RegisterInfoTextDisplay(string infoTextKey) + { + _prefs + .Edit() + .PutLong("LastInfoTextTime", DateTime.UtcNow.Ticks) + .PutString("LastInfoTextKey", infoTextKey) + .Commit(); + + } + + private bool InfoTextWasDisabled(string infoTextKey) + { + return _prefs.GetBoolean("InfoTextDisabled_" + infoTextKey, false); + } + + private void UpdateAutofillInfo() + { + bool canShowAutofillInfo = false; + + if (!((Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.O) || + !((AutofillManager)GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager)))) + .IsAutofillSupported)) + { + const string autofillservicewasenabled = "AutofillServiceWasEnabled"; + if (!((AutofillManager)GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager)))) + .HasEnabledAutofillServices) + { if (!_prefs.GetBoolean(autofillservicewasenabled, false)) - canShowAutofillInfo = true; - } - else - { - _prefs.Edit().PutBoolean(autofillservicewasenabled, true).Commit(); - - } - } - UpdateBottomBarElementVisibility(Resource.Id.autofill_infotext, canShowAutofillInfo); + canShowAutofillInfo = true; + } + else + { + _prefs.Edit().PutBoolean(autofillservicewasenabled, true).Commit(); + + } + } + UpdateBottomBarElementVisibility(Resource.Id.autofill_infotext, canShowAutofillInfo); } - protected void UpdateBottomBarElementVisibility(int resourceId, bool canShow) - { + protected void UpdateBottomBarElementVisibility(int resourceId, bool canShow) + { if (canShow) showableBottomBarElements.Add(resourceId); else showableBottomBarElements.Remove(resourceId); UpdateBottomBarVisibility(); - } - - protected virtual int ContentResourceId - { - get { return Resource.Layout.group; } - } - - private void InsertElements() - { - MoveElementsTask moveElementsTask = (MoveElementsTask)AppTask; - IEnumerable elementsToMove = - moveElementsTask.Uuids.Select(uuid => App.Kp2a.GetDb().KpDatabase.RootGroup.FindObject(uuid, true, null)); - - - - var moveElement = new MoveElements(elementsToMove.ToList(), Group, this, App.Kp2a, new ActionOnFinish((success, message) => { StopMovingElements(); if (!String.IsNullOrEmpty(message)) Toast.MakeText(this, message, ToastLength.Long).Show(); })); - var progressTask = new ProgressTask(App.Kp2a, this, moveElement); - progressTask.Run(); - - } - - - - protected void SetGroupTitle() - { - String name = Group.Name; - String titleText; - bool clickable = (Group != null) && (Group.IsVirtual == false) && (Group.ParentGroup != null); - if (!String.IsNullOrEmpty(name)) - { - titleText = name; - } - else - { - titleText = GetText(Resource.String.root); - } - - SupportActionBar.Title = titleText; - if (clickable) - { - SupportActionBar.SetHomeButtonEnabled(true); - SupportActionBar.SetDisplayHomeAsUpEnabled(true); - SupportActionBar.SetDisplayShowHomeEnabled(true); - } - - } - - - protected void SetGroupIcon() - { - if (Group != null) - { - Drawable drawable = App.Kp2a.GetDb().DrawableFactory.GetIconDrawable(this, App.Kp2a.GetDb().KpDatabase, Group.IconId, Group.CustomIconUuid, true); - SupportActionBar.SetDisplayShowHomeEnabled(true); - //SupportActionBar.SetIcon(drawable); - } - } - - class SuggestionListener : Java.Lang.Object, SearchView.IOnSuggestionListener, Android.Support.V7.Widget.SearchView.IOnSuggestionListener - { - private readonly CursorAdapter _suggestionsAdapter; - private readonly GroupBaseActivity _activity; - private readonly IMenuItem _searchItem; - - - public SuggestionListener(Android.Support.V4.Widget.CursorAdapter suggestionsAdapter, GroupBaseActivity activity, IMenuItem searchItem) - { - _suggestionsAdapter = suggestionsAdapter; - _activity = activity; - _searchItem = searchItem; - } - - public bool OnSuggestionClick(int position) - { - var cursor = _suggestionsAdapter.Cursor; - cursor.MoveToPosition(position); - string entryIdAsHexString = cursor.GetString(cursor.GetColumnIndexOrThrow(SearchManager.SuggestColumnIntentDataId)); - EntryActivity.Launch(_activity, App.Kp2a.GetDb().Entries[new PwUuid(MemUtil.HexStringToByteArray(entryIdAsHexString))], -1, _activity.AppTask); - return true; - } - - public bool OnSuggestionSelect(int position) - { - return false; - } - } - - class OnQueryTextListener : Java.Lang.Object, Android.Support.V7.Widget.SearchView.IOnQueryTextListener - { - private readonly GroupBaseActivity _activity; - - public OnQueryTextListener(GroupBaseActivity activity) - { - _activity = activity; - } - - public bool OnQueryTextChange(string newText) - { - return false; - } - - public bool OnQueryTextSubmit(string query) - { - if (String.IsNullOrEmpty(query)) - return false; //let the default happen - - Intent searchIntent = new Intent(_activity, typeof(search.SearchResults)); - searchIntent.SetAction(Intent.ActionSearch); //currently not necessary to set because SearchResults doesn't care, but let's be as close to the default as possible - searchIntent.PutExtra(SearchManager.Query, query); - //forward appTask: - _activity.AppTask.ToIntent(searchIntent); - - _activity.StartActivityForResult(searchIntent, 0); - - return true; - } - } - - public override bool OnCreateOptionsMenu(IMenu menu) - { - - MenuInflater inflater = MenuInflater; - inflater.Inflate(Resource.Menu.group, menu); - var searchManager = (SearchManager)GetSystemService(Context.SearchService); - IMenuItem searchItem = menu.FindItem(Resource.Id.menu_search); - var view = MenuItemCompat.GetActionView(searchItem); - var searchView = view.JavaCast(); - - searchView.SetSearchableInfo(searchManager.GetSearchableInfo(ComponentName)); - searchView.SetOnSuggestionListener(new SuggestionListener(searchView.SuggestionsAdapter, this, searchItem)); - searchView.SetOnQueryTextListener(new OnQueryTextListener(this)); - - ActionBar.LayoutParams lparams = new ActionBar.LayoutParams(ActionBar.LayoutParams.MatchParent, - ActionBar.LayoutParams.MatchParent); - searchView.LayoutParameters = lparams; - - _syncItem = menu.FindItem(Resource.Id.menu_sync); - - - _offlineItem = menu.FindItem(Resource.Id.menu_work_offline); - _onlineItem = menu.FindItem(Resource.Id.menu_work_online); - - UpdateOfflineModeMenu(); - - - return base.OnCreateOptionsMenu(menu); - - } - - private void UpdateOfflineModeMenu() - { - try - { - if (_syncItem != null) - { - if (App.Kp2a.GetDb().Ioc.IsLocalFile()) - _syncItem.SetVisible(false); - else - _syncItem.SetVisible(!App.Kp2a.OfflineMode); - } - - if (App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc) is IOfflineSwitchable) - { - if (_offlineItem != null) - _offlineItem.SetVisible(App.Kp2a.OfflineMode == false); - if (_onlineItem != null) - _onlineItem.SetVisible(App.Kp2a.OfflineMode); - } - else - { - if (_offlineItem != null) - _offlineItem.SetVisible(false); - if (_onlineItem != null) - _onlineItem.SetVisible(false); - - } - } - catch (Exception e) - { - Kp2aLog.LogUnexpectedError(new Exception("Cannot UpdateOfflineModeMenu " + (App.Kp2a == null) + " " + ((App.Kp2a == null) || (App.Kp2a.GetDb() == null)) + " " + (((App.Kp2a == null) || (App.Kp2a.GetDb() == null) || (App.Kp2a.GetDb().Ioc == null)) + " " + (_syncItem != null) + " " + (_offlineItem != null) + " " + (_onlineItem != null)))); - } - - } - - - public override bool OnPrepareOptionsMenu(IMenu menu) - { - if (!base.OnPrepareOptionsMenu(menu)) - { - return false; - } - - Util.PrepareDonateOptionMenu(menu, this); - - - return true; - } - - public override bool OnOptionsItemSelected(IMenuItem item) - { - switch (item.ItemId) - { - case Resource.Id.menu_donate: - return Util.GotoDonateUrl(this); - case Resource.Id.menu_lock: - App.Kp2a.LockDatabase(); - return true; - - case Resource.Id.menu_search: - case Resource.Id.menu_search_advanced: - OnSearchRequested(); - return true; - - case Resource.Id.menu_app_settings: - DatabaseSettingsActivity.Launch(this); - return true; - - case Resource.Id.menu_sync: - Synchronize(); - return true; - - case Resource.Id.menu_work_offline: - App.Kp2a.OfflineMode = App.Kp2a.OfflineModePreference = true; - UpdateOfflineModeMenu(); - return true; - - case Resource.Id.menu_work_online: - App.Kp2a.OfflineMode = App.Kp2a.OfflineModePreference = false; - UpdateOfflineModeMenu(); - Synchronize(); - return true; - - - case Resource.Id.menu_sort: - ChangeSort(); - return true; - case Android.Resource.Id.Home: - //Currently the action bar only displays the home button when we come from a previous activity. - //So we can simply Finish. See this page for information on how to do this in more general (future?) cases: - //http://developer.android.com/training/implementing-navigation/ancestral.html - AppTask.SetActivityResult(this, KeePass.ExitNormal); - Finish(); - //OverridePendingTransition(Resource.Animation.anim_enter_back, Resource.Animation.anim_leave_back); - - return true; - } - - return base.OnOptionsItemSelected(item); - } - - public class SyncOtpAuxFile : RunnableOnFinish - { - private readonly IOConnectionInfo _ioc; - - public SyncOtpAuxFile(IOConnectionInfo ioc) - : base(null) - { - _ioc = ioc; - } - - public override void Run() - { - StatusLogger.UpdateMessage(UiStringKey.SynchronizingOtpAuxFile); - try - { - //simply open the file. The file storage does a complete sync. - using (App.Kp2a.GetOtpAuxFileStorage(_ioc).OpenFileForRead(_ioc)) - { - } + } + + protected virtual int ContentResourceId + { + get { return Resource.Layout.group; } + } + + private void InsertElements() + { + MoveElementsTask moveElementsTask = (MoveElementsTask)AppTask; + IEnumerable elementsToMove = + moveElementsTask.Uuids.Select(uuid => App.Kp2a.GetDb().KpDatabase.RootGroup.FindObject(uuid, true, null)); + + + + var moveElement = new MoveElements(elementsToMove.ToList(), Group, this, App.Kp2a, new ActionOnFinish((success, message) => { StopMovingElements(); if (!String.IsNullOrEmpty(message)) Toast.MakeText(this, message, ToastLength.Long).Show(); })); + var progressTask = new ProgressTask(App.Kp2a, this, moveElement); + progressTask.Run(); + + } + + + + protected void SetGroupTitle() + { + String name = Group.Name; + String titleText; + bool clickable = (Group != null) && (Group.IsVirtual == false) && (Group.ParentGroup != null); + if (!String.IsNullOrEmpty(name)) + { + titleText = name; + } + else + { + titleText = GetText(Resource.String.root); + } + + SupportActionBar.Title = titleText; + if (clickable) + { + SupportActionBar.SetHomeButtonEnabled(true); + SupportActionBar.SetDisplayHomeAsUpEnabled(true); + SupportActionBar.SetDisplayShowHomeEnabled(true); + } + + } + + + protected void SetGroupIcon() + { + if (Group != null) + { + Drawable drawable = App.Kp2a.GetDb().DrawableFactory.GetIconDrawable(this, App.Kp2a.GetDb().KpDatabase, Group.IconId, Group.CustomIconUuid, true); + SupportActionBar.SetDisplayShowHomeEnabled(true); + //SupportActionBar.SetIcon(drawable); + } + } + + class SuggestionListener : Java.Lang.Object, SearchView.IOnSuggestionListener, Android.Support.V7.Widget.SearchView.IOnSuggestionListener + { + private readonly CursorAdapter _suggestionsAdapter; + private readonly GroupBaseActivity _activity; + private readonly IMenuItem _searchItem; + + + public SuggestionListener(Android.Support.V4.Widget.CursorAdapter suggestionsAdapter, GroupBaseActivity activity, IMenuItem searchItem) + { + _suggestionsAdapter = suggestionsAdapter; + _activity = activity; + _searchItem = searchItem; + } + + public bool OnSuggestionClick(int position) + { + var cursor = _suggestionsAdapter.Cursor; + cursor.MoveToPosition(position); + string entryIdAsHexString = cursor.GetString(cursor.GetColumnIndexOrThrow(SearchManager.SuggestColumnIntentDataId)); + EntryActivity.Launch(_activity, App.Kp2a.GetDb().Entries[new PwUuid(MemUtil.HexStringToByteArray(entryIdAsHexString))], -1, _activity.AppTask); + return true; + } + + public bool OnSuggestionSelect(int position) + { + return false; + } + } + + class OnQueryTextListener : Java.Lang.Object, Android.Support.V7.Widget.SearchView.IOnQueryTextListener + { + private readonly GroupBaseActivity _activity; + + public OnQueryTextListener(GroupBaseActivity activity) + { + _activity = activity; + } + + public bool OnQueryTextChange(string newText) + { + return false; + } + + public bool OnQueryTextSubmit(string query) + { + if (String.IsNullOrEmpty(query)) + return false; //let the default happen + + Intent searchIntent = new Intent(_activity, typeof(search.SearchResults)); + searchIntent.SetAction(Intent.ActionSearch); //currently not necessary to set because SearchResults doesn't care, but let's be as close to the default as possible + searchIntent.PutExtra(SearchManager.Query, query); + //forward appTask: + _activity.AppTask.ToIntent(searchIntent); + + _activity.StartActivityForResult(searchIntent, 0); + + return true; + } + } - Finish(true); - } - catch (Exception e) - { + public override bool OnCreateOptionsMenu(IMenu menu) + { + + MenuInflater inflater = MenuInflater; + inflater.Inflate(Resource.Menu.group, menu); + var searchManager = (SearchManager)GetSystemService(Context.SearchService); + IMenuItem searchItem = menu.FindItem(Resource.Id.menu_search); + var view = MenuItemCompat.GetActionView(searchItem); + var searchView = view.JavaCast(); + + searchView.SetSearchableInfo(searchManager.GetSearchableInfo(ComponentName)); + searchView.SetOnSuggestionListener(new SuggestionListener(searchView.SuggestionsAdapter, this, searchItem)); + searchView.SetOnQueryTextListener(new OnQueryTextListener(this)); + + ActionBar.LayoutParams lparams = new ActionBar.LayoutParams(ActionBar.LayoutParams.MatchParent, + ActionBar.LayoutParams.MatchParent); + searchView.LayoutParameters = lparams; + + _syncItem = menu.FindItem(Resource.Id.menu_sync); + + + _offlineItem = menu.FindItem(Resource.Id.menu_work_offline); + _onlineItem = menu.FindItem(Resource.Id.menu_work_online); + + UpdateOfflineModeMenu(); + + + return base.OnCreateOptionsMenu(menu); + + } + + private void UpdateOfflineModeMenu() + { + try + { + if (_syncItem != null) + { + if (App.Kp2a.GetDb().Ioc.IsLocalFile()) + _syncItem.SetVisible(false); + else + _syncItem.SetVisible(!App.Kp2a.OfflineMode); + } + + if (App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc) is IOfflineSwitchable) + { + if (_offlineItem != null) + _offlineItem.SetVisible(App.Kp2a.OfflineMode == false); + if (_onlineItem != null) + _onlineItem.SetVisible(App.Kp2a.OfflineMode); + } + else + { + if (_offlineItem != null) + _offlineItem.SetVisible(false); + if (_onlineItem != null) + _onlineItem.SetVisible(false); + + } + } + catch (Exception e) + { + Kp2aLog.LogUnexpectedError(new Exception("Cannot UpdateOfflineModeMenu " + (App.Kp2a == null) + " " + ((App.Kp2a == null) || (App.Kp2a.GetDb() == null)) + " " + (((App.Kp2a == null) || (App.Kp2a.GetDb() == null) || (App.Kp2a.GetDb().Ioc == null)) + " " + (_syncItem != null) + " " + (_offlineItem != null) + " " + (_onlineItem != null)))); + } + + } + + + public override bool OnPrepareOptionsMenu(IMenu menu) + { + if (!base.OnPrepareOptionsMenu(menu)) + { + return false; + } + + Util.PrepareDonateOptionMenu(menu, this); + + + return true; + } + + public override bool OnOptionsItemSelected(IMenuItem item) + { + switch (item.ItemId) + { + case Resource.Id.menu_donate: + return Util.GotoDonateUrl(this); + case Resource.Id.menu_lock: + App.Kp2a.LockDatabase(); + return true; + + case Resource.Id.menu_search: + case Resource.Id.menu_search_advanced: + OnSearchRequested(); + return true; + + case Resource.Id.menu_app_settings: + DatabaseSettingsActivity.Launch(this); + return true; + + case Resource.Id.menu_sync: + Synchronize(); + return true; + + case Resource.Id.menu_work_offline: + App.Kp2a.OfflineMode = App.Kp2a.OfflineModePreference = true; + UpdateOfflineModeMenu(); + return true; + + case Resource.Id.menu_work_online: + App.Kp2a.OfflineMode = App.Kp2a.OfflineModePreference = false; + UpdateOfflineModeMenu(); + Synchronize(); + return true; + + + case Resource.Id.menu_sort: + ChangeSort(); + return true; + case Android.Resource.Id.Home: + //Currently the action bar only displays the home button when we come from a previous activity. + //So we can simply Finish. See this page for information on how to do this in more general (future?) cases: + //http://developer.android.com/training/implementing-navigation/ancestral.html + AppTask.SetActivityResult(this, KeePass.ExitNormal); + Finish(); + //OverridePendingTransition(Resource.Animation.anim_enter_back, Resource.Animation.anim_leave_back); + + return true; + } + + return base.OnOptionsItemSelected(item); + } + + public class SyncOtpAuxFile : RunnableOnFinish + { + private readonly IOConnectionInfo _ioc; + + public SyncOtpAuxFile(IOConnectionInfo ioc) + : base(null) + { + _ioc = ioc; + } + + public override void Run() + { + StatusLogger.UpdateMessage(UiStringKey.SynchronizingOtpAuxFile); + try + { + //simply open the file. The file storage does a complete sync. + using (App.Kp2a.GetOtpAuxFileStorage(_ioc).OpenFileForRead(_ioc)) + { + } + + Finish(true); + } + catch (Exception e) + { + + Finish(false, e.Message); + } + + + } + + } + + private void Synchronize() + { + var filestorage = App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc); + RunnableOnFinish task; + OnFinish onFinish = new ActionOnFinish((success, message) => + { + if (!String.IsNullOrEmpty(message)) + Toast.MakeText(this, message, ToastLength.Long).Show(); + + // Tell the adapter to refresh it's list + BaseAdapter adapter = (BaseAdapter)ListAdapter; + adapter.NotifyDataSetChanged(); + + if (App.Kp2a.GetDb().OtpAuxFileIoc != null) + { + var task2 = new SyncOtpAuxFile(App.Kp2a.GetDb().OtpAuxFileIoc); + new ProgressTask(App.Kp2a, this, task2).Run(); + } + }); + + if (filestorage is CachingFileStorage) + { + + task = new SynchronizeCachedDatabase(this, App.Kp2a, onFinish); + } + else + { + + task = new CheckDatabaseForChanges(this, App.Kp2a, onFinish); + } + + + + + var progressTask = new ProgressTask(App.Kp2a, this, task); + progressTask.Run(); + + } + + public override void OnBackPressed() + { + AppTask.SetActivityResult(this, KeePass.ExitNormal); + base.OnBackPressed(); + } + + private void ChangeSort() + { + var sortOrderManager = new GroupViewSortOrderManager(this); + IEnumerable sortOptions = sortOrderManager.SortOrders.Select( + o => GetString(o.ResourceId) + ); + + int selectedBefore = sortOrderManager.GetCurrentSortOrderIndex(); + + new AlertDialog.Builder(this) + .SetSingleChoiceItems(sortOptions.ToArray(), selectedBefore, (sender, args) => + { + int selectedAfter = args.Which; + + sortOrderManager.SetNewSortOrder(selectedAfter); + // Refresh menu titles + ActivityCompat.InvalidateOptionsMenu(this); + + // Mark all groups as dirty now to refresh them on load + Database db = App.Kp2a.GetDb(); + db.MarkAllGroupsAsDirty(); + // We'll manually refresh this group so we can remove it + db.Dirty.Remove(Group); + + // Tell the adapter to refresh it's list + + BaseAdapter adapter = (BaseAdapter)ListAdapter; + adapter.NotifyDataSetChanged(); + + + }) + .SetPositiveButton(Android.Resource.String.Ok, (sender, args) => ((Dialog)sender).Dismiss()) + .Show(); + + + + + } - Finish(false, e.Message); - } + public class RefreshTask : OnFinish + { + readonly GroupBaseActivity _act; + public RefreshTask(Handler handler, GroupBaseActivity act) + : base(handler) + { + _act = act; + } + + public override void Run() + { + if (Success) + { + _act.RefreshIfDirty(); + } + else + { + DisplayMessage(_act); + } + } + } + public class AfterDeleteGroup : OnFinish + { + readonly GroupBaseActivity _act; + public AfterDeleteGroup(Handler handler, GroupBaseActivity act) + : base(handler) + { + _act = act; + } - } - } + public override void Run() + { + if (Success) + { + _act.RefreshIfDirty(); + } + else + { + Handler.Post(() => + { + Toast.MakeText(_act, "Unrecoverable error: " + Message, ToastLength.Long).Show(); + }); + + App.Kp2a.LockDatabase(false); + } + } + + } + + public bool IsBeingMoved(PwUuid uuid) + { + MoveElementsTask moveElementsTask = AppTask as MoveElementsTask; + if (moveElementsTask != null) + { + if (moveElementsTask.Uuids.Any(uuidMoved => uuidMoved.Equals(uuid))) + return true; + } + return false; + } + + public void StartTask(AppTask task) + { + AppTask = task; + task.StartInGroupActivity(this); + } + + + public void StartMovingElements() + { + + ShowInsertElementsButtons(); + BaseAdapter adapter = (BaseAdapter)ListAdapter; + adapter.NotifyDataSetChanged(); + } + + public void ShowInsertElementsButtons() + { + FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNewGroup).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNewEntry).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNew).Visibility = ViewStates.Gone; + + UpdateBottomBarElementVisibility(Resource.Id.insert_element, true); + UpdateBottomBarElementVisibility(Resource.Id.cancel_insert_element, true); + + } + + public void StopMovingElements() + { + try + { + MoveElementsTask moveElementsTask = (MoveElementsTask)AppTask; + foreach (var uuid in moveElementsTask.Uuids) + { + IStructureItem elementToMove = App.Kp2a.GetDb().KpDatabase.RootGroup.FindObject(uuid, true, null); + if (elementToMove.ParentGroup != Group) + App.Kp2a.GetDb().Dirty.Add(elementToMove.ParentGroup); + } + } + catch (Exception e) + { + //don't crash if adding to dirty fails but log the exception: + Kp2aLog.LogUnexpectedError(e); + } + + AppTask = new NullTask(); + AppTask.SetupGroupBaseActivityButtons(this); + BaseAdapter adapter = (BaseAdapter)ListAdapter; + adapter.NotifyDataSetChanged(); + } + + + public void EditGroup(PwGroup pwGroup) + { + GroupEditActivity.Launch(this, pwGroup.ParentGroup, pwGroup); + } + } + + public class GroupListFragment : ListFragment, AbsListView.IMultiChoiceModeListener + { + private ActionMode _mode; + private int _statusBarColor; + + public override void OnActivityCreated(Bundle savedInstanceState) + { + base.OnActivityCreated(savedInstanceState); + if (App.Kp2a.GetDb().CanWrite) + { + ListView.ChoiceMode = ChoiceMode.MultipleModal; + ListView.SetMultiChoiceModeListener(this); + ListView.ItemLongClick += delegate (object sender, AdapterView.ItemLongClickEventArgs args) + { + ListView.SetItemChecked(args.Position, true); + }; + + } - private void Synchronize() - { - var filestorage = App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc); - RunnableOnFinish task; - OnFinish onFinish = new ActionOnFinish((success, message) => - { - if (!String.IsNullOrEmpty(message)) - Toast.MakeText(this, message, ToastLength.Long).Show(); - - // Tell the adapter to refresh it's list - BaseAdapter adapter = (BaseAdapter)ListAdapter; - adapter.NotifyDataSetChanged(); - - if (App.Kp2a.GetDb().OtpAuxFileIoc != null) - { - var task2 = new SyncOtpAuxFile(App.Kp2a.GetDb().OtpAuxFileIoc); - new ProgressTask(App.Kp2a, this, task2).Run(); - } - }); - - if (filestorage is CachingFileStorage) - { - - task = new SynchronizeCachedDatabase(this, App.Kp2a, onFinish); - } - else - { - - task = new CheckDatabaseForChanges(this, App.Kp2a, onFinish); - } - - - - - var progressTask = new ProgressTask(App.Kp2a, this, task); - progressTask.Run(); - - } - - public override void OnBackPressed() - { - AppTask.SetActivityResult(this, KeePass.ExitNormal); - base.OnBackPressed(); - } - - private void ChangeSort() - { - var sortOrderManager = new GroupViewSortOrderManager(this); - IEnumerable sortOptions = sortOrderManager.SortOrders.Select( - o => GetString(o.ResourceId) - ); - - int selectedBefore = sortOrderManager.GetCurrentSortOrderIndex(); - - new AlertDialog.Builder(this) - .SetSingleChoiceItems(sortOptions.ToArray(), selectedBefore, (sender, args) => - { - int selectedAfter = args.Which; - - sortOrderManager.SetNewSortOrder(selectedAfter); - // Refresh menu titles - ActivityCompat.InvalidateOptionsMenu(this); - - // Mark all groups as dirty now to refresh them on load - Database db = App.Kp2a.GetDb(); - db.MarkAllGroupsAsDirty(); - // We'll manually refresh this group so we can remove it - db.Dirty.Remove(Group); - - // Tell the adapter to refresh it's list - - BaseAdapter adapter = (BaseAdapter)ListAdapter; - adapter.NotifyDataSetChanged(); - - - }) - .SetPositiveButton(Android.Resource.String.Ok, (sender, args) => ((Dialog)sender).Dismiss()) - .Show(); - - - - - } - - public class RefreshTask : OnFinish - { - readonly GroupBaseActivity _act; - public RefreshTask(Handler handler, GroupBaseActivity act) - : base(handler) - { - _act = act; - } - - public override void Run() - { - if (Success) - { - _act.RefreshIfDirty(); - } - else - { - DisplayMessage(_act); - } - } - } - public class AfterDeleteGroup : OnFinish - { - readonly GroupBaseActivity _act; - - public AfterDeleteGroup(Handler handler, GroupBaseActivity act) - : base(handler) - { - _act = act; - } - - - public override void Run() - { - if (Success) - { - _act.RefreshIfDirty(); - } - else - { - Handler.Post(() => - { - Toast.MakeText(_act, "Unrecoverable error: " + Message, ToastLength.Long).Show(); - }); - - App.Kp2a.LockDatabase(false); - } - } - - } - - public bool IsBeingMoved(PwUuid uuid) - { - MoveElementsTask moveElementsTask = AppTask as MoveElementsTask; - if (moveElementsTask != null) - { - if (moveElementsTask.Uuids.Any(uuidMoved => uuidMoved.Equals(uuid))) - return true; - } - return false; - } - - public void StartTask(AppTask task) - { - AppTask = task; - task.StartInGroupActivity(this); - } - - - public void StartMovingElements() - { - - ShowInsertElementsButtons(); - BaseAdapter adapter = (BaseAdapter)ListAdapter; - adapter.NotifyDataSetChanged(); - } - - public void ShowInsertElementsButtons() - { - FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNewGroup).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNewEntry).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNew).Visibility = ViewStates.Gone; - - UpdateBottomBarElementVisibility(Resource.Id.insert_element, true); - UpdateBottomBarElementVisibility(Resource.Id.cancel_insert_element, true); - - } - - public void StopMovingElements() - { - try - { - MoveElementsTask moveElementsTask = (MoveElementsTask)AppTask; - foreach (var uuid in moveElementsTask.Uuids) - { - IStructureItem elementToMove = App.Kp2a.GetDb().KpDatabase.RootGroup.FindObject(uuid, true, null); - if (elementToMove.ParentGroup != Group) - App.Kp2a.GetDb().Dirty.Add(elementToMove.ParentGroup); - } - } - catch (Exception e) - { - //don't crash if adding to dirty fails but log the exception: - Kp2aLog.LogUnexpectedError(e); - } - - AppTask = new NullTask(); - AppTask.SetupGroupBaseActivityButtons(this); - BaseAdapter adapter = (BaseAdapter)ListAdapter; - adapter.NotifyDataSetChanged(); - } - - - public void EditGroup(PwGroup pwGroup) - { - GroupEditActivity.Launch(this, pwGroup.ParentGroup, pwGroup); - } - } - - public class GroupListFragment : ListFragment, AbsListView.IMultiChoiceModeListener - { - private ActionMode _mode; - private int _statusBarColor; - - public override void OnActivityCreated(Bundle savedInstanceState) - { - base.OnActivityCreated(savedInstanceState); - if (App.Kp2a.GetDb().CanWrite) - { - ListView.ChoiceMode = ChoiceMode.MultipleModal; - ListView.SetMultiChoiceModeListener(this); - ListView.ItemLongClick += delegate(object sender, AdapterView.ItemLongClickEventArgs args) - { - ListView.SetItemChecked(args.Position, true); - }; - - } - - ListView.ItemClick += (sender, args) => ((GroupListItemView)args.View).OnClick(); - - StyleListView(); - - } - - protected void StyleListView() - { - ListView lv = ListView; - lv.ScrollBarStyle = ScrollbarStyles.InsideInset; - lv.TextFilterEnabled = true; - - lv.Divider = null; - } - - public bool OnActionItemClicked(ActionMode mode, IMenuItem item) - { - var listView = FragmentManager.FindFragmentById(Resource.Id.list_fragment).ListView; - var checkedItemPositions = listView.CheckedItemPositions; - - List checkedItems = new List(); - for (int i = 0; i < checkedItemPositions.Size(); i++) - { - if (checkedItemPositions.ValueAt(i)) - { - checkedItems.Add(((PwGroupListAdapter)ListAdapter).GetItemAtPosition(checkedItemPositions.KeyAt(i))); - } - } - - //shouldn't happen, just in case... - if (!checkedItems.Any()) - { - return false; - } - Handler handler = new Handler(); - switch (item.ItemId) - { - - case Resource.Id.menu_delete: - - DeleteMultipleItems task = new DeleteMultipleItems((GroupBaseActivity)Activity, App.Kp2a.GetDb(), checkedItems, - new GroupBaseActivity.RefreshTask(handler, ((GroupBaseActivity)Activity)), App.Kp2a); - task.Start(); - break; - case Resource.Id.menu_move: - var navMove = new NavigateToFolderAndLaunchMoveElementTask(checkedItems.First().ParentGroup, checkedItems.Select(i => i.Uuid).ToList(), ((GroupBaseActivity)Activity).IsSearchResult); - ((GroupBaseActivity)Activity).StartTask(navMove); - break; - case Resource.Id.menu_copy: - - var copyTask = new CopyEntry((GroupBaseActivity)Activity, App.Kp2a, (PwEntry) checkedItems.First(), - new GroupBaseActivity.RefreshTask(handler, ((GroupBaseActivity)Activity))); - - ProgressTask pt = new ProgressTask(App.Kp2a, Activity, copyTask); - pt.Run(); - break; - - case Resource.Id.menu_navigate: - NavigateToFolder navNavigate = new NavigateToFolder(checkedItems.First().ParentGroup, true); - ((GroupBaseActivity)Activity).StartTask(navNavigate); - break; - case Resource.Id.menu_edit: - GroupEditActivity.Launch(Activity, checkedItems.First().ParentGroup, (PwGroup)checkedItems.First()); - break; - default: - return false; - - - } - listView.ClearChoices(); - ((BaseAdapter)ListAdapter).NotifyDataSetChanged(); - if (_mode != null) - mode.Finish(); - - return true; - } - - public bool OnCreateActionMode(ActionMode mode, IMenu menu) - { - MenuInflater inflater = Activity.MenuInflater; - inflater.Inflate(Resource.Menu.group_entriesselected, menu); - //mode.Title = "Select Items"; - Android.Util.Log.Debug("KP2A", "Create action mode" + mode); - ((PwGroupListAdapter)ListView.Adapter).InActionMode = true; - ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); - _mode = mode; - if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop) - { - _statusBarColor = Activity.Window.StatusBarColor; - Activity.Window.SetStatusBarColor(Activity.Resources.GetColor(Resource.Color.appAccentColorDark)); - } - return true; - } - - public void OnDestroyActionMode(ActionMode mode) - { - Android.Util.Log.Debug("KP2A", "Destroy action mode" + mode); - ((PwGroupListAdapter)ListView.Adapter).InActionMode = false; - ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); - _mode = null; - if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop) - { - Activity.Window.SetStatusBarColor(new Android.Graphics.Color(_statusBarColor)); - } - } - - public bool OnPrepareActionMode(ActionMode mode, IMenu menu) - { - Android.Util.Log.Debug("KP2A", "Prepare action mode" + mode); - ((PwGroupListAdapter)ListView.Adapter).InActionMode = mode != null; - ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); - return true; - } - - public void OnItemCheckedStateChanged(ActionMode mode, int position, long id, bool @checked) - { - var menuItem = mode.Menu.FindItem(Resource.Id.menu_edit); - if (menuItem != null) - { - menuItem.SetVisible(IsOnlyOneGroupChecked()); - } - - menuItem = mode.Menu.FindItem(Resource.Id.menu_navigate); - if (menuItem != null) - { - menuItem.SetVisible(((GroupBaseActivity)Activity).IsSearchResult && IsOnlyOneItemChecked()); - } - - menuItem = mode.Menu.FindItem(Resource.Id.menu_copy); - if (menuItem != null) - { - menuItem.SetVisible(IsOnlyOneEntryChecked()); - } - } - - private bool IsOnlyOneGroupChecked() - { - var checkedItems = ListView.CheckedItemPositions; - bool hadCheckedGroup = false; - if (checkedItems != null) - { - for (int i = 0; i < checkedItems.Size(); i++) - { - if (checkedItems.ValueAt(i)) - { - if (hadCheckedGroup) - { - return false; - } - - if (((PwGroupListAdapter)ListAdapter).IsGroupAtPosition(checkedItems.KeyAt(i))) - { - hadCheckedGroup = true; - } - else - { - return false; - } - } - } - } - return hadCheckedGroup; - } - - private bool IsOnlyOneItemChecked() - { - var checkedItems = ListView.CheckedItemPositions; - bool hadCheckedItem = false; - if (checkedItems != null) - { - for (int i = 0; i < checkedItems.Size(); i++) - { - if (checkedItems.ValueAt(i)) - { - if (hadCheckedItem) - { - return false; - } - - hadCheckedItem = true; - } - } - } - return hadCheckedItem; - } - - private bool IsOnlyOneEntryChecked() - { - return IsOnlyOneItemChecked() && !IsOnlyOneGroupChecked(); - } - } + ListView.ItemClick += (sender, args) => ((GroupListItemView)args.View).OnClick(); + + StyleListView(); + + } + + protected void StyleListView() + { + ListView lv = ListView; + lv.ScrollBarStyle = ScrollbarStyles.InsideInset; + lv.TextFilterEnabled = true; + + lv.Divider = null; + } + + public bool OnActionItemClicked(ActionMode mode, IMenuItem item) + { + var listView = FragmentManager.FindFragmentById(Resource.Id.list_fragment).ListView; + var checkedItemPositions = listView.CheckedItemPositions; + + List checkedItems = new List(); + for (int i = 0; i < checkedItemPositions.Size(); i++) + { + if (checkedItemPositions.ValueAt(i)) + { + checkedItems.Add(((PwGroupListAdapter)ListAdapter).GetItemAtPosition(checkedItemPositions.KeyAt(i))); + } + } + + //shouldn't happen, just in case... + if (!checkedItems.Any()) + { + return false; + } + Handler handler = new Handler(); + switch (item.ItemId) + { + + case Resource.Id.menu_delete: + + DeleteMultipleItems task = new DeleteMultipleItems((GroupBaseActivity)Activity, App.Kp2a.GetDb(), checkedItems, + new GroupBaseActivity.RefreshTask(handler, ((GroupBaseActivity)Activity)), App.Kp2a); + task.Start(); + break; + case Resource.Id.menu_move: + var navMove = new NavigateToFolderAndLaunchMoveElementTask(checkedItems.First().ParentGroup, checkedItems.Select(i => i.Uuid).ToList(), ((GroupBaseActivity)Activity).IsSearchResult); + ((GroupBaseActivity)Activity).StartTask(navMove); + break; + case Resource.Id.menu_copy: + + var copyTask = new CopyEntry((GroupBaseActivity)Activity, App.Kp2a, (PwEntry)checkedItems.First(), + new GroupBaseActivity.RefreshTask(handler, ((GroupBaseActivity)Activity))); + + ProgressTask pt = new ProgressTask(App.Kp2a, Activity, copyTask); + pt.Run(); + break; + + case Resource.Id.menu_navigate: + NavigateToFolder navNavigate = new NavigateToFolder(checkedItems.First().ParentGroup, true); + ((GroupBaseActivity)Activity).StartTask(navNavigate); + break; + case Resource.Id.menu_edit: + GroupEditActivity.Launch(Activity, checkedItems.First().ParentGroup, (PwGroup)checkedItems.First()); + break; + default: + return false; + + + } + listView.ClearChoices(); + ((BaseAdapter)ListAdapter).NotifyDataSetChanged(); + if (_mode != null) + mode.Finish(); + + return true; + } + + public bool OnCreateActionMode(ActionMode mode, IMenu menu) + { + MenuInflater inflater = Activity.MenuInflater; + inflater.Inflate(Resource.Menu.group_entriesselected, menu); + //mode.Title = "Select Items"; + Android.Util.Log.Debug("KP2A", "Create action mode" + mode); + ((PwGroupListAdapter)ListView.Adapter).InActionMode = true; + ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); + _mode = mode; + if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop) + { + _statusBarColor = Activity.Window.StatusBarColor; + Activity.Window.SetStatusBarColor(Activity.Resources.GetColor(Resource.Color.appAccentColorDark)); + } + return true; + } + + public void OnDestroyActionMode(ActionMode mode) + { + Android.Util.Log.Debug("KP2A", "Destroy action mode" + mode); + ((PwGroupListAdapter)ListView.Adapter).InActionMode = false; + ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); + _mode = null; + if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop) + { + Activity.Window.SetStatusBarColor(new Android.Graphics.Color(_statusBarColor)); + } + } + + public bool OnPrepareActionMode(ActionMode mode, IMenu menu) + { + Android.Util.Log.Debug("KP2A", "Prepare action mode" + mode); + ((PwGroupListAdapter)ListView.Adapter).InActionMode = mode != null; + ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); + return true; + } + + public void OnItemCheckedStateChanged(ActionMode mode, int position, long id, bool @checked) + { + var menuItem = mode.Menu.FindItem(Resource.Id.menu_edit); + if (menuItem != null) + { + menuItem.SetVisible(IsOnlyOneGroupChecked()); + } + + menuItem = mode.Menu.FindItem(Resource.Id.menu_navigate); + if (menuItem != null) + { + menuItem.SetVisible(((GroupBaseActivity)Activity).IsSearchResult && IsOnlyOneItemChecked()); + } + + menuItem = mode.Menu.FindItem(Resource.Id.menu_copy); + if (menuItem != null) + { + menuItem.SetVisible(IsOnlyOneEntryChecked()); + } + } + + private bool IsOnlyOneGroupChecked() + { + var checkedItems = ListView.CheckedItemPositions; + bool hadCheckedGroup = false; + if (checkedItems != null) + { + for (int i = 0; i < checkedItems.Size(); i++) + { + if (checkedItems.ValueAt(i)) + { + if (hadCheckedGroup) + { + return false; + } + + if (((PwGroupListAdapter)ListAdapter).IsGroupAtPosition(checkedItems.KeyAt(i))) + { + hadCheckedGroup = true; + } + else + { + return false; + } + } + } + } + return hadCheckedGroup; + } + + private bool IsOnlyOneItemChecked() + { + var checkedItems = ListView.CheckedItemPositions; + bool hadCheckedItem = false; + if (checkedItems != null) + { + for (int i = 0; i < checkedItems.Size(); i++) + { + if (checkedItems.ValueAt(i)) + { + if (hadCheckedItem) + { + return false; + } + + hadCheckedItem = true; + } + } + } + return hadCheckedItem; + } + + private bool IsOnlyOneEntryChecked() + { + return IsOnlyOneItemChecked() && !IsOnlyOneGroupChecked(); + } + } } diff --git a/src/keepass2android/Resources/layout/group.xml b/src/keepass2android/Resources/layout/group.xml index 2a8a85a40..cf3d5a108 100644 --- a/src/keepass2android/Resources/layout/group.xml +++ b/src/keepass2android/Resources/layout/group.xml @@ -77,6 +77,63 @@ style="@style/BottomBarButton" /> + + + + + + + + + + + + - public virtual void Setup(Bundle b) - {} + /// + /// Loads the parameters of the task from the given bundle + /// + public virtual void Setup(Bundle b) + { + CanActivateSearchViewOnStart = b.GetBoolean(CanActivateSearchViewOnStartKey, true); + + } + + public const String CanActivateSearchViewOnStartKey = "CanActivateSearchViewOnStart"; + + /// + /// Can be overwritten to indicate that it is not desired to bring up the search view when starting a groupactivity + /// + public virtual bool CanActivateSearchViewOnStart + { + get; + set; + } - /// - /// Returns the parameters of the task for storage in a bundle or intent - /// - /// The extras. - public virtual IEnumerable Extras { - get - { - yield break; - } - } + + /// + /// Returns the parameters of the task for storage in a bundle or intent + /// + /// The extras. + public virtual IEnumerable Extras + { + get + { + yield return new BoolExtra { Key = CanActivateSearchViewOnStartKey, Value = CanActivateSearchViewOnStart }; + } + } + + public virtual void AfterUnlockDatabase(PasswordActivity act) { @@ -581,7 +599,13 @@ public void AskAddUrlThenCompleteCreate(EntryActivity activity, string url) /// public class MoveElementsTask: AppTask { - public const String UuidsKey = "MoveElement_Uuids"; + public override bool CanActivateSearchViewOnStart + { + get { return false; } + set { } + } + + public const String UuidsKey = "MoveElement_Uuids"; public IEnumerable Uuids { @@ -632,10 +656,16 @@ public CreateEntryThenCloseTask() ShowUserNotifications = true; } - /// - /// extra key if only a URL is passed. optional. - /// - public const String UrlKey = "CreateEntry_Url"; + public override bool CanActivateSearchViewOnStart + { + get { return false; } + set { } + } + + /// + /// extra key if only a URL is passed. optional. + /// + public const String UrlKey = "CreateEntry_Url"; /// /// extra key if a json serialized key/value mapping is passed. optional. @@ -739,9 +769,15 @@ public override void CompleteOnCreateEntryActivity(EntryActivity activity) /// public abstract class NavigateAndLaunchTask: AppTask { - // All group Uuid are stored in guuidKey + indice - // The last one is the destination group - public const String NumberOfGroupsKey = "NumberOfGroups"; + public override bool CanActivateSearchViewOnStart + { + get { return false; } + set { } + } + + // All group Uuid are stored in guuidKey + indice + // The last one is the destination group + public const String NumberOfGroupsKey = "NumberOfGroups"; public const String GUuidKey = "gUuidKey"; public const String FullGroupNameKey = "fullGroupNameKey"; public const String ToastEnableKey = "toastEnableKey"; From 737c63e8b0c39ddd0c37995d1f68556fd4b5c1e0 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Fri, 9 Feb 2018 12:07:21 +0100 Subject: [PATCH 15/75] switch to TargetSDK 26, implement notification channels allowing customization of notification importance on Android 8, see #178 --- .../Properties/AndroidManifest_debug.xml | 2 +- .../Properties/AndroidManifest_net.xml | 2 +- .../Resources/values/strings.xml | 9 ++++ src/keepass2android/app/App.cs | 52 +++++++++++++++++-- .../services/CopyToClipboardService.cs | 2 +- .../services/OngoingNotificationsService.cs | 4 +- 6 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/keepass2android/Properties/AndroidManifest_debug.xml b/src/keepass2android/Properties/AndroidManifest_debug.xml index 41eebd8e7..8904de3d8 100644 --- a/src/keepass2android/Properties/AndroidManifest_debug.xml +++ b/src/keepass2android/Properties/AndroidManifest_debug.xml @@ -1,6 +1,6 @@  - + diff --git a/src/keepass2android/Properties/AndroidManifest_net.xml b/src/keepass2android/Properties/AndroidManifest_net.xml index 51c4108d8..0eca32134 100644 --- a/src/keepass2android/Properties/AndroidManifest_net.xml +++ b/src/keepass2android/Properties/AndroidManifest_net.xml @@ -4,7 +4,7 @@ android:versionName="1.04" package="keepass2android.keepass2android" android:installLocation="auto"> - + diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index aefa97be9..c56f5ffcc 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -689,6 +689,15 @@ + Database unlocked + Notification about the database being unlocked + + QuickUnlock + Notification about the database being locked with QuickUnlock + + Entry notifications + Notification to simplify access to the currently selected entry. + Keepass2Android: An error occurred. An unexpected error occurred while running Keepass2Android. Please help us fix this by allowing the app to send error reports. Error reports will never contain any contents of your database or master password. You can disable them in the application settings. diff --git a/src/keepass2android/app/App.cs b/src/keepass2android/app/App.cs index fc8592545..8992011cc 100644 --- a/src/keepass2android/app/App.cs +++ b/src/keepass2android/app/App.cs @@ -21,6 +21,7 @@ You should have received a copy of the GNU General Public License using System.Net.Security; using Android.App; using Android.Content; +using Android.Graphics; using Android.Graphics.Drawables; using Android.OS; using Android.Runtime; @@ -845,7 +846,11 @@ public void OnScreenOff() #endif public class App : Application { - public App (IntPtr javaReference, JniHandleOwnership transfer) + public const string NotificationChannelIdUnlocked = "channel_db_unlocked_3"; + public const string NotificationChannelIdQuicklocked = "channel_db_quicklocked_3"; + public const string NotificationChannelIdEntry = "channel_db_entry_3"; + + public App (IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { } @@ -857,14 +862,53 @@ public override void OnCreate() { Kp2aLog.Log("Creating application "+PackageName+". Version=" + PackageManager.GetPackageInfo(PackageName, 0).VersionCode); + CreateNotificationChannels(); + Kp2a.OnCreate(this); AndroidEnvironment.UnhandledExceptionRaiser += MyApp_UnhandledExceptionHandler; } + private void CreateNotificationChannels() + { + NotificationManager mNotificationManager = + (NotificationManager)GetSystemService(Context.NotificationService); + + { + string name = GetString(Resource.String.DbUnlockedChannel_name); + string desc = GetString(Resource.String.DbUnlockedChannel_desc); + NotificationChannel mChannel = + new NotificationChannel(NotificationChannelIdUnlocked, name, NotificationImportance.Low); + mChannel.Description = desc; + mChannel.EnableLights(false); + mChannel.EnableVibration(false); + mNotificationManager.CreateNotificationChannel(mChannel); + } + + { + string name = GetString(Resource.String.DbQuicklockedChannel_name); + string desc = GetString(Resource.String.DbQuicklockedChannel_desc); + NotificationChannel mChannel = + new NotificationChannel(NotificationChannelIdQuicklocked, name, NotificationImportance.Min); + mChannel.Description = desc; + mChannel.EnableLights(false); + mChannel.EnableVibration(false); + mNotificationManager.CreateNotificationChannel(mChannel); + } + + { + string name = GetString(Resource.String.EntryChannel_name); + string desc = GetString(Resource.String.EntryChannel_desc); + NotificationChannel mChannel = + new NotificationChannel(NotificationChannelIdEntry, name, NotificationImportance.None); + mChannel.Description = desc; + mChannel.EnableLights(false); + mChannel.EnableVibration(false); + mNotificationManager.CreateNotificationChannel(mChannel); + } + } + - - - public override void OnTerminate() { + public override void OnTerminate() { base.OnTerminate(); Kp2aLog.Log("Terminating application"); Kp2a.OnTerminate(); diff --git a/src/keepass2android/services/CopyToClipboardService.cs b/src/keepass2android/services/CopyToClipboardService.cs index 52b67c870..36344f63d 100644 --- a/src/keepass2android/services/CopyToClipboardService.cs +++ b/src/keepass2android/services/CopyToClipboardService.cs @@ -198,7 +198,7 @@ private NotificationCompat.Builder GetNotificationBuilder(string intentText, int pending = GetPendingIntent(intentText, descResId); } - var builder = new NotificationCompat.Builder(_ctx); + var builder = new NotificationCompat.Builder(_ctx, App.NotificationChannelIdEntry); builder.SetSmallIcon(drawableResId) .SetContentText(desc) .SetContentTitle(entryName) diff --git a/src/keepass2android/services/OngoingNotificationsService.cs b/src/keepass2android/services/OngoingNotificationsService.cs index 976a13739..5fe3c5c71 100644 --- a/src/keepass2android/services/OngoingNotificationsService.cs +++ b/src/keepass2android/services/OngoingNotificationsService.cs @@ -139,7 +139,7 @@ private Notification GetQuickUnlockNotification() grayIconResouceId = Resource.Drawable.transparent; } NotificationCompat.Builder builder = - new NotificationCompat.Builder(this) + new NotificationCompat.Builder(this, App.NotificationChannelIdQuicklocked) .SetSmallIcon(grayIconResouceId) .SetLargeIcon(MakeLargeIcon(BitmapFactory.DecodeResource(Resources, AppNames.NotificationLockedIcon))) .SetVisibility((int)Android.App.NotificationVisibility.Secret) @@ -183,7 +183,7 @@ private Bitmap MakeLargeIcon(Bitmap unscaled) private Notification GetUnlockedNotification() { NotificationCompat.Builder builder = - new NotificationCompat.Builder(this) + new NotificationCompat.Builder(this, App.NotificationChannelIdUnlocked) .SetOngoing(true) .SetSmallIcon(Resource.Drawable.ic_notify) .SetLargeIcon(MakeLargeIcon(BitmapFactory.DecodeResource(Resources, AppNames.NotificationUnlockedIcon))) From 7b63346bd0c3fa4824aa9bab8189b3d585003869 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Fri, 9 Feb 2018 12:22:49 +0100 Subject: [PATCH 16/75] set notification channel importances correctly --- src/keepass2android/app/App.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/keepass2android/app/App.cs b/src/keepass2android/app/App.cs index 8992011cc..d0537368c 100644 --- a/src/keepass2android/app/App.cs +++ b/src/keepass2android/app/App.cs @@ -846,9 +846,9 @@ public void OnScreenOff() #endif public class App : Application { - public const string NotificationChannelIdUnlocked = "channel_db_unlocked_3"; - public const string NotificationChannelIdQuicklocked = "channel_db_quicklocked_3"; - public const string NotificationChannelIdEntry = "channel_db_entry_3"; + public const string NotificationChannelIdUnlocked = "channel_db_unlocked_5"; + public const string NotificationChannelIdQuicklocked = "channel_db_quicklocked_5"; + public const string NotificationChannelIdEntry = "channel_db_entry_5"; public App (IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) @@ -877,7 +877,7 @@ private void CreateNotificationChannels() string name = GetString(Resource.String.DbUnlockedChannel_name); string desc = GetString(Resource.String.DbUnlockedChannel_desc); NotificationChannel mChannel = - new NotificationChannel(NotificationChannelIdUnlocked, name, NotificationImportance.Low); + new NotificationChannel(NotificationChannelIdUnlocked, name, NotificationImportance.Min); mChannel.Description = desc; mChannel.EnableLights(false); mChannel.EnableVibration(false); @@ -899,7 +899,7 @@ private void CreateNotificationChannels() string name = GetString(Resource.String.EntryChannel_name); string desc = GetString(Resource.String.EntryChannel_desc); NotificationChannel mChannel = - new NotificationChannel(NotificationChannelIdEntry, name, NotificationImportance.None); + new NotificationChannel(NotificationChannelIdEntry, name, NotificationImportance.Default); mChannel.Description = desc; mChannel.EnableLights(false); mChannel.EnableVibration(false); From 6fc4741c9a699e21d61080c6f77574e1b68cf148 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Sat, 10 Feb 2018 21:06:50 +0100 Subject: [PATCH 17/75] remove icon preferences on Android 8, show info on how to use notification channels on Android 8 --- src/keepass2android/GroupBaseActivity.cs | 47 +++++++++++++++++++ .../Resources/layout/group.xml | 40 ++++++++++++++++ .../Resources/values/strings.xml | 4 ++ .../settings/DatabaseSettingsActivity.cs | 25 +++++++--- 4 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/keepass2android/GroupBaseActivity.cs b/src/keepass2android/GroupBaseActivity.cs index a0dbd849d..5c82fdddc 100644 --- a/src/keepass2android/GroupBaseActivity.cs +++ b/src/keepass2android/GroupBaseActivity.cs @@ -53,6 +53,7 @@ public abstract class GroupBaseActivity : LockCloseActivity { Resource.Id.cancel_insert_element, 20 }, { Resource.Id.insert_element, 20 }, { Resource.Id.autofill_infotext, 10 }, + { Resource.Id.notification_info_android8_infotext, 10 }, { Resource.Id.infotext, 11 }, { Resource.Id.select_other_entry, 20}, { Resource.Id.add_url_entry, 20}, @@ -240,9 +241,26 @@ protected override void OnResume() UpdateAutofillInfo(); + UpdateAndroid8NotificationInfo(); + RefreshIfDirty(); + } + + private void UpdateAndroid8NotificationInfo(bool hideForever = false) + { + const string prefsKey = "DidShowAndroid8NotificationInfo"; + + bool canShowNotificationInfo = (Build.VERSION.SdkInt >= BuildVersionCodes.O) && (!_prefs.GetBoolean(prefsKey, false)); + if ((canShowNotificationInfo) && hideForever) + { + _prefs.Edit().PutBoolean(prefsKey, true).Commit(); + canShowNotificationInfo = false; + } + UpdateBottomBarElementVisibility(Resource.Id.notification_info_android8_infotext, canShowNotificationInfo); + + } public override bool OnSearchRequested() @@ -340,6 +358,35 @@ protected override void OnCreate(Bundle savedInstanceState) Util.MoveBottomBarButtons(Resource.Id.show_autofill_info, Resource.Id.enable_autofill, Resource.Id.autofill_buttons, this); } + if (FindViewById(Resource.Id.configure_notification_channels) != null) + { + FindViewById(Resource.Id.configure_notification_channels).Click += (sender, args) => + { + Intent intent = new Intent(Settings.ActionChannelNotificationSettings); + intent.PutExtra(Settings.ExtraChannelId, App.NotificationChannelIdQuicklocked); + intent.PutExtra(Settings.ExtraAppPackage, PackageName); + try + { + StartActivity(intent); + } + catch (Exception e) + { + new AlertDialog.Builder(this) + .SetTitle("Unexpected error") + .SetMessage( + "Opening the settings failed. Please report this to crocoapps@gmail.com including information about your device vendor and OS. Please try to configure the notifications by long pressing a KP2A notification. Details: " + e.ToString()) + .Show(); + } + UpdateAndroid8NotificationInfo(true); + }; + FindViewById(Resource.Id.ignore_notification_channel).Click += (sender, args) => + { + UpdateAndroid8NotificationInfo(true); + }; + + } + + string lastInfoText; if (IsTimeForInfotext(out lastInfoText)) diff --git a/src/keepass2android/Resources/layout/group.xml b/src/keepass2android/Resources/layout/group.xml index cf3d5a108..aa74430ae 100644 --- a/src/keepass2android/Resources/layout/group.xml +++ b/src/keepass2android/Resources/layout/group.xml @@ -77,7 +77,47 @@ style="@style/BottomBarButton" /> + + + + + + +