/* This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file is based on Keepassdroid, Copyright Brian Pellin. Keepass2Android is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Keepass2Android is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Keepass2Android. If not, see . */ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Android.App; using Android.Content; using Android.Content.PM; using Android.OS; using Android.Runtime; using Android.Views; using KeePassLib; using Android.Preferences; using KeePassLib.Interfaces; using KeePassLib.Utility; using keepass2android.Io; using keepass2android.database.edit; using keepass2android.view; using Android.Graphics.Drawables; using Android.Provider; using Android.Views.Autofill; using Object = Java.Lang.Object; using Android.Text; using keepass2android.search; using keepass2android; using KeeTrayTOTP.Libraries; using AndroidX.AppCompat.Widget; using Google.Android.Material.Dialog; using SearchView = AndroidX.AppCompat.Widget.SearchView; namespace keepass2android { public abstract class GroupBaseActivity : LockCloseActivity { public const String KeyEntry = "entry"; public const String KeyMode = "mode"; private float _currentListTextSize; public const int RequestCodeActivateRealSearch = 12366; protected override View? SnackbarAnchorView => FindViewById(Resource.Id.main_content); static readonly Dictionary bottomBarElementsPriority = new Dictionary() { { Resource.Id.cancel_insert_element, 20 }, { Resource.Id.insert_element, 20 }, //only use the same id if elements can be shown simultaneously! { Resource.Id.dbreadonly_infotext, 14 }, { Resource.Id.child_db_infotext, 13 }, { Resource.Id.fingerprint_infotext, 12 }, { Resource.Id.autofill_infotext, 11 }, { Resource.Id.notification_permission_infotext, 10 }, { Resource.Id.notification_info_android8_infotext, 9 }, { Resource.Id.infotext, 8 }, { Resource.Id.select_other_entry, 20}, { Resource.Id.add_url_entry, 20}, }; private readonly HashSet showableBottomBarElements = new HashSet(); private ActivityDesign _design; public virtual void LaunchActivityForEntry(PwEntry pwEntry, int pos) { EntryActivity.Launch(this, pwEntry, pos, AppTask); } protected GroupBaseActivity() { _design = new ActivityDesign(this); } protected GroupBaseActivity(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { } protected override void OnSaveInstanceState(Bundle outState) { base.OnSaveInstanceState(outState); AppTask.ToBundle(outState); } public virtual void SetupNormalButtons() { SetNormalButtonVisibility(AddGroupEnabled, AddEntryEnabled); } protected virtual bool AddGroupEnabled { get { return false; } } protected virtual bool AddEntryEnabled { get { return false; } } 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.fabSearch).Visibility = (showAddGroup || showAddEntry) ? ViewStates.Visible : ViewStates.Gone; FindViewById(Resource.Id.fabTotpOverview).Visibility = CanShowTotpFab() ? ViewStates.Visible : ViewStates.Gone; } 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(); 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]; if (!highestPrioElements.Any() || highestPrio < myPrio) { 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); AppTask a = null; if (AppTask.TryGetFromActivityResult(data, ref a)) { AppTask = a; //make sure the app task is passed to the calling activity this.AppTask.SetActivityResult(this, KeePass.ExitNormal); } if (RequestCodeActivateRealSearch == requestCode) { hasCalledOtherActivity = true; SetSearchItemVisibility(); ActivateSearchView(); } if ((GroupEditActivity.RequestCodeGroupEdit == requestCode) && (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.FindGroup(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.ExitLoadAnotherDb) { AppTask.SetActivityResult(this, KeePass.ExitLoadAnotherDb); Finish(); } if (resultCode == KeePass.ExitReloadDb) { AppTask.SetActivityResult(this, KeePass.ExitReloadDb); Finish(); } } private ISharedPreferences _prefs; protected PwGroup Group; private AppTask _appTask; public AppTask AppTask { get { return _appTask; } set { _appTask = value; Kp2aLog.LogTask(value, MyDebugName); } } private String strCachedGroupUuid = null; private IMenuItem _offlineItem; private IMenuItem _onlineItem; private IMenuItem _syncItem; private SearchView searchView; public String UuidGroup { get { if (strCachedGroupUuid == null) { strCachedGroupUuid = MemUtil.ByteArrayToHexString(Group.Uuid.UuidBytes); } return strCachedGroupUuid; } } private bool hasCalledOtherActivity = false; private IMenuItem searchItem; private IMenuItem searchItemDummy; private bool isPaused; protected override void OnResume() { base.OnResume(); if (IsFinishing) { //can happen e.g. after theme change return; } if (PrefsUtil.GetListTextSize(this) != _currentListTextSize) { Recreate(); return; } AppTask.StartInGroupActivity(this); AppTask.SetupGroupBaseActivityButtons(this); UpdateDbReadOnlyInfo(); UpdateChildDbInfo(); UpdateFingerprintInfo(); UpdateAutofillInfo(); UpdateAndroid8NotificationInfo(); UpdatePostNotificationsPermissionInfo(); UpdateInfotexts(); RefreshIfDirty(); SetSearchItemVisibility(); isPaused = false; System.Threading.Tasks.Task.Run(UpdateTotpCountdown); } private async System.Threading.Tasks.Task UpdateTotpCountdown() { while (!isPaused ) { RunOnUiThread(() => { var listView = FragmentManager?.FindFragmentById(Resource.Id.list_fragment) ?.ListView; if (listView != null) { int count = listView.Count; for (int i = 0; i < count; i++) { var item = listView.GetChildAt(i); if (item is PwEntryView) { var entryView = (PwEntryView)item; entryView.UpdateTotp(); } } } }); await System.Threading.Tasks.Task.Delay(1000); } } private void UpdateInfotexts() { string lastInfoText; if (IsTimeForInfotext(out lastInfoText) && (FindViewById(Resource.Id.info_head) != null)) { FingerprintUnlockMode um; Enum.TryParse(_prefs.GetString(Database.GetFingerprintModePrefKey(App.Kp2a.CurrentDb.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.CurrentDb.Ioc).UserShouldBackup) { applicableInfoTextKeys.Add(backupKey); } if (App.Kp2a.CurrentDb.EntriesById.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; 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); } } } } protected override void OnStop() { base.OnStop(); hasCalledOtherActivity = false; } protected override void OnPause() { base.OnPause(); isPaused = true; } private void UpdatePostNotificationsPermissionInfo(bool hideForever=false) { const string prefsKey = "DidShowNotificationPermissionInfo"; bool canShowNotificationInfo = ((int)Build.VERSION.SdkInt >= 33) && (!_prefs.GetBoolean(prefsKey, false) && (CheckSelfPermission(Android.Manifest.Permission.PostNotifications) != Permission.Granted) && (ShouldShowRequestPermissionRationale(Android.Manifest.Permission.PostNotifications) //this menthod returns false if we haven't asked yet or if the user has denied permission too often || !PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean("RequestedPostNotificationsPermission", false))//use a preference to tell the difference between "haven't asked yet" and "have asked too often" ); if ((canShowNotificationInfo) && hideForever) { _prefs.Edit().PutBoolean(prefsKey, true).Commit(); canShowNotificationInfo = false; } if (canShowNotificationInfo) { RegisterInfoTextDisplay("NotificationPermissionInfo"); //this ensures that we don't show the general info texts too soon } UpdateBottomBarElementVisibility(Resource.Id.notification_permission_infotext, canShowNotificationInfo); } 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; } if (canShowNotificationInfo) { RegisterInfoTextDisplay("Android8Notification"); //this ensures that we don't show the general info texts too soon } UpdateBottomBarElementVisibility(Resource.Id.notification_info_android8_infotext, canShowNotificationInfo); } public override bool OnSearchRequested() { Intent i = new Intent(this, typeof(SearchActivity)); AppTask.ToIntent(i); StartActivityForResult(i, 0); return true; } public void RefreshIfDirty() { if (App.Kp2a.DirtyGroups.Contains(Group)) { App.Kp2a.DirtyGroups.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(); _currentListTextSize = PrefsUtil.GetListTextSize(this); 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.CurrentDb== null) { 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)); try { StartActivity(intent); } catch (ActivityNotFoundException e) { //this exception was reported by many Huawei users Kp2aLog.LogUnexpectedError(e); new MaterialAlertDialogBuilder(this) .SetTitle(Resource.String.autofill_enable) .SetMessage(Resource.String.autofill_enable_failed) .SetPositiveButton(Resource.String.Ok, (o, eventArgs) => { }) .Show(); const string autofillservicewasenabled = "AutofillServiceWasEnabled"; _prefs.Edit().PutBoolean(autofillservicewasenabled, true).Commit(); UpdateBottomBarElementVisibility(Resource.Id.autofill_infotext, false); } }; } if (FindViewById(Resource.Id.info_dont_show_fingerprint_again) != null) { FindViewById(Resource.Id.info_dont_show_fingerprint_again).Click += (sender, args) => { _prefs.Edit().PutBoolean(fingerprintinfohidden_prefskey, true).Commit(); UpdateFingerprintInfo(); }; } if (FindViewById(Resource.Id.hide_fingerprint_info) != null) { FindViewById(Resource.Id.hide_fingerprint_info).Click += (sender, args) => { _prefs.Edit().PutBoolean(fingerprintinfohidden_prefskey + App.Kp2a.CurrentDb.CurrentFingerprintPrefKey, true).Commit(); UpdateFingerprintInfo(); }; } if (FindViewById(Resource.Id.enable_fingerprint) != null) { FindViewById(Resource.Id.enable_fingerprint).Click += (sender, args) => { StartActivity(typeof(BiometricSetupActivity)); }; } if (FindViewById(Resource.Id.info_dont_show_autofill_again) != null) { FindViewById(Resource.Id.info_dont_show_autofill_again).Click += (sender, args) => { _prefs.Edit().PutBoolean(autofillservicewasenabled_prefskey, true).Commit(); UpdateAutofillInfo(); }; } if (FindViewById(Resource.Id.configure_child_db) != null) { FindViewById(Resource.Id.configure_child_db).Click += (sender, args) => { StartActivity(typeof(ConfigureChildDatabasesActivity)); }; } if (FindViewById(Resource.Id.info_dont_show_child_db_again) != null) { FindViewById(Resource.Id.info_dont_show_child_db_again).Click += (sender, args) => { _prefs.Edit().PutBoolean(childdb_ignore_prefskey + App.Kp2a.CurrentDb.CurrentFingerprintPrefKey, true).Commit(); UpdateChildDbInfo(); }; } if (FindViewById(Resource.Id.info_dont_show_dbreadonly_again) != null) { FindViewById(Resource.Id.info_dont_show_dbreadonly_again).Click += (sender, args) => { _prefs.Edit().PutBoolean(dbreadonly_ignore_prefskey + App.Kp2a.CurrentDb.CurrentFingerprintPrefKey, true).Commit(); UpdateDbReadOnlyInfo(); }; } if (FindViewById(Resource.Id.fabSearch) != null) { FindViewById(Resource.Id.fabSearch).Click += (sender, args) => { if (searchView?.Iconified != false) ActivateSearchView(); else searchView.Iconified = true; }; } if (FindViewById(Resource.Id.fabTotpOverview) != null) { FindViewById(Resource.Id.fabTotpOverview).Click += (sender, args) => { SearchTotpResults.Launch(this, this.AppTask); }; } 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 .fabAddNewEntry).Shrink(); FindViewById(Resource.Id .fabAddNewGroup).Extended = false; FindViewById(Resource.Id .fabAddNewGroup).Extend(); FindViewById(Resource.Id.fabAddNew).Visibility = ViewStates.Gone; FindViewById(Resource.Id.fabSearch).Visibility = ViewStates.Gone; FindViewById(Resource.Id.fabTotpOverview).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; FindViewById(Resource.Id.fabSearch).Visibility = ViewStates.Visible; FindViewById(Resource.Id.fabTotpOverview).Visibility = CanShowTotpFab() ? ViewStates.Visible : ViewStates.Gone; }; } 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); } 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 MaterialAlertDialogBuilder(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); }; } if (FindViewById(Resource.Id.post_notification_button_allow) != null) { FindViewById(Resource.Id.post_notification_button_allow).Click += (sender, args) => { //remember that we did ask for permission at least once: var edit = PreferenceManager.GetDefaultSharedPreferences(this).Edit(); edit.PutBoolean("RequestedPostNotificationsPermission", true); edit.Commit(); AndroidX.Core.App.ActivityCompat.RequestPermissions(this, new[] { Android.Manifest.Permission.PostNotifications }, 0); UpdatePostNotificationsPermissionInfo(true); }; FindViewById(Resource.Id.post_notification_button_dont_show_again).Click += (sender, args) => { UpdatePostNotificationsPermissionInfo(true); }; } SetResult(KeePass.ExitNormal); } protected virtual bool CanShowTotpFab() { return App.Kp2a.CurrentDb.HasTotpEntries && Group == App.Kp2a.CurrentDb.Root; } 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); } const string dbreadonly_ignore_prefskey = "dbreadonly_ignore_prefskey"; const string childdb_ignore_prefskey = "childdb_ignore_prefskey"; const string autofillservicewasenabled_prefskey = "AutofillServiceWasEnabled"; const string fingerprintinfohidden_prefskey = "fingerprintinfohidden_prefskey"; 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)) { if (!((AutofillManager)GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager)))) .HasEnabledAutofillServices) { if (!_prefs.GetBoolean(autofillservicewasenabled_prefskey, false)) canShowAutofillInfo = true; } else { _prefs.Edit().PutBoolean(autofillservicewasenabled_prefskey, true).Commit(); } } if (canShowAutofillInfo) { RegisterInfoTextDisplay("AutofillSuggestion"); //this ensures that we don't show the general info texts too soon } UpdateBottomBarElementVisibility(Resource.Id.autofill_infotext, canShowAutofillInfo); } private void UpdateFingerprintInfo() { bool canShowFingerprintInfo = false; bool disabledForDatabase = _prefs.GetBoolean(fingerprintinfohidden_prefskey + App.Kp2a.CurrentDb.CurrentFingerprintPrefKey, false); bool disabledForAll = _prefs.GetBoolean(fingerprintinfohidden_prefskey, false); if (!disabledForAll && !disabledForDatabase && !App.Kp2a.IsChildDatabase(App.Kp2a.CurrentDb)) { BiometricModule biometricModule = new BiometricModule(this); if (biometricModule.IsAvailable) { FingerprintUnlockMode um; Enum.TryParse(_prefs.GetString(Database.GetFingerprintModePrefKey(App.Kp2a.CurrentDb.Ioc), ""), out um); canShowFingerprintInfo = um == FingerprintUnlockMode.Disabled; } } if (canShowFingerprintInfo) { RegisterInfoTextDisplay("FingerprintSuggestion"); //this ensures that we don't show the general info texts too soon } UpdateBottomBarElementVisibility(Resource.Id.fingerprint_infotext, canShowFingerprintInfo); } private void UpdateChildDbInfo() { bool canShow = Group == App.Kp2a.CurrentDb.Root && KeeAutoExecExt.GetAutoExecItems(App.Kp2a.CurrentDb.KpDatabase).Any(item => { bool isexplicit; KeeAutoExecExt.IsDeviceEnabled(item, KeeAutoExecExt.ThisDeviceId, out isexplicit); return !isexplicit; }); bool disabledForDatabase = _prefs.GetBoolean(childdb_ignore_prefskey+ App.Kp2a.CurrentDb.CurrentFingerprintPrefKey, false); if (canShow && !disabledForDatabase) { RegisterInfoTextDisplay("ChildDb"); //this ensures that we don't show the general info texts too soon } UpdateBottomBarElementVisibility(Resource.Id.child_db_infotext, canShow && !disabledForDatabase); } private void UpdateDbReadOnlyInfo() { bool disabledForDatabase = _prefs.GetBoolean(dbreadonly_ignore_prefskey + App.Kp2a.CurrentDb.CurrentFingerprintPrefKey, false); bool canShow = false; if (!disabledForDatabase) { var ioc = App.Kp2a.CurrentDb.Ioc; OptionalOut reason = new OptionalOut(); if (App.Kp2a.GetFileStorage(ioc).IsReadOnly(ioc, reason)) { canShow = true; RegisterInfoTextDisplay( "DbReadOnly"); //this ensures that we don't show the general info texts too soon FindViewById(Resource.Id.dbreadonly_infotext_text).Text = (GetString(Resource.String.FileReadOnlyMessagePre) + " " + App.Kp2a.GetResourceString(reason.Result)); } } UpdateBottomBarElementVisibility(Resource.Id.dbreadonly_infotext, 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.FindStructureItem(uuid)); var moveElement = new MoveElements(elementsToMove.ToList(), Group, this, App.Kp2a, new ActionOnFinish(this, (success, message, activity) => { ((GroupBaseActivity)activity)?.StopMovingElements(); if (!String.IsNullOrEmpty(message)) App.Kp2a.ShowMessage(activity, message, MessageSeverity.Error); })); 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) || App.Kp2a.OpenDatabases.Count() > 1); 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) { SupportActionBar.SetDisplayShowHomeEnabled(true); } } class SuggestionListener : Java.Lang.Object, AndroidX.AppCompat.Widget.SearchView.IOnSuggestionListener { private readonly AndroidX.CursorAdapter.Widget.CursorAdapter _suggestionsAdapter; private readonly GroupBaseActivity _activity; private readonly IMenuItem _searchItem; public SuggestionListener(AndroidX.CursorAdapter.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); ElementAndDatabaseId fullId = new ElementAndDatabaseId(cursor.GetString(cursor.GetColumnIndexOrThrow(SearchManager.SuggestColumnIntentDataId))); var entryId = fullId.ElementId; EntryActivity.Launch(_activity, App.Kp2a.GetDatabase(fullId.DatabaseId).EntriesById[entryId], -1, _activity.AppTask); return true; } public bool OnSuggestionSelect(int position) { return false; } } class OnQueryTextListener : Java.Lang.Object, 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); /*This is the start of a pretty hacky workaround to avoid a crash on Samsung devices with Android 9. * The crash stacktrace is pretty unspecific (see https://stackoverflow.com/questions/54530604/app-crash-but-no-app-specific-code-in-stack-trace) * It points to InputMethodService.java which seems to be modified by Samsung. Hard to tell what's going on. * The problem only occurs, if our own keyboard is activated. * Users found that the crash does not appear if another activity was launched and closed before activating search view. * That's what we do as a workaround: We display another search menu option in case a crash would occur. When that search option is clicked, * we launch an activity which immediately finished. In the activity result, we can activate the search view safely. * If anybody reading this has a better idea, please let me know :-) */ searchItem = menu.FindItem(Resource.Id.menu_search); searchItemDummy = menu.FindItem(Resource.Id.menu_search_dummy); SetSearchItemVisibility(); var view = searchItem.ActionView; searchView = view.JavaCast(); searchView.SetSearchableInfo(searchManager.GetSearchableInfo(ComponentName)); searchView.SetOnSuggestionListener(new SuggestionListener(searchView.SuggestionsAdapter, this, searchItem)); searchView.SetOnQueryTextListener(new OnQueryTextListener(this)); if (_prefs.GetBoolean("ActivateSearchView", false) && AppTask.CanActivateSearchViewOnStart) { //need to use PostDelayed, otherwise the menu_lock item completely disappears searchView.PostDelayed(ActivateSearchView, 500); } 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 SetSearchItemVisibility() { if ((searchItem == null) || (searchItemDummy == null)) return; if (Build.Manufacturer.ToLowerInvariant() == "samsung" && ((int) Build.VERSION.SdkInt >= 28) && (IsKp2aKeyboardActive()) && !hasCalledOtherActivity) { searchItem.SetVisible(false); searchItemDummy.SetVisible(true); } else { searchItem.SetVisible(true); searchItemDummy.SetVisible(false); } } private string Kp2aInputMethodName { get { return PackageName + "/keepass2android.softkeyboard.KP2AKeyboard"; } } private bool IsKp2aKeyboardActive() { string currentIme = Android.Provider.Settings.Secure.GetString( ContentResolver, Android.Provider.Settings.Secure.DefaultInputMethod); return Kp2aInputMethodName == currentIme; } private void ActivateSearchView() { searchView.Iconified = false; AppTask.CanActivateSearchViewOnStart = false; } private void UpdateOfflineModeMenu() { try { if (_syncItem != null) { if (((App.Kp2a.OpenDatabases.Count() == 1) || (EntriesBelongToCurrentDatabaseOnly)) && App.Kp2a.CurrentDb.Ioc.IsLocalFile()) _syncItem.SetVisible(false); else _syncItem.SetVisible(!App.Kp2a.OfflineMode); } if (((App.Kp2a.OpenDatabases.Count() == 1) || (EntriesBelongToCurrentDatabaseOnly)) && (App.Kp2a.GetFileStorage(App.Kp2a.CurrentDb.Ioc) is IOfflineSwitchable)) { _offlineItem?.SetVisible(App.Kp2a.OfflineMode == false); _onlineItem?.SetVisible(App.Kp2a.OfflineMode); } else { _offlineItem?.SetVisible(false); _onlineItem?.SetVisible(false); } } catch (Exception e) { Kp2aLog.LogUnexpectedError(new Exception("Cannot UpdateOfflineModeMenu " + (App.Kp2a == null) + " " + ((App.Kp2a == null) || (App.Kp2a.CurrentDb== null)) + " " + (((App.Kp2a == null) || (App.Kp2a.CurrentDb== null) || (App.Kp2a.CurrentDb.Ioc == null)) + " " + (_syncItem != null) + " " + (_offlineItem != null) + " " + (_onlineItem != null)))); } } public abstract bool EntriesBelongToCurrentDatabaseOnly { get; } public abstract ElementAndDatabaseId FullGroupId { get; } public virtual bool MayPreviewTotp { get { return !PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(GetString(Resource.String.masktotp_key), Resources.GetBoolean(Resource.Boolean.masktotp_default)); } } 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.Lock(); Finish(); return true; case Resource.Id.menu_search_dummy: StartActivityForResult(typeof(CloseImmediatelyActivity), RequestCodeActivateRealSearch); OverridePendingTransition(0, 0); hasCalledOtherActivity = true; //TODO transition? 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: new SyncUtil(this).SynchronizeDatabase(() => { }); 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(); new SyncUtil(this).SynchronizeDatabase(() => { }); return true; case Resource.Id.menu_open_other_db: AppTask.SetActivityResult(this, KeePass.ExitLoadAnotherDb); Finish(); 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 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 MaterialAlertDialogBuilder(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 App.Kp2a.MarkAllGroupsAsDirty(); // We'll manually refresh this group so we can remove it App.Kp2a.DirtyGroups.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 { public RefreshTask(Handler handler, GroupBaseActivity act) : base(act, handler) { } public override void Run() { if (Success) { ((GroupBaseActivity)ActiveActivity)?.RefreshIfDirty(); } else { DisplayMessage(ActiveActivity); } } } public class AfterDeleteGroup : OnFinish { public AfterDeleteGroup(Handler handler, GroupBaseActivity act) : base(act, handler) { } public override void Run() { if (Success) { ((GroupBaseActivity)ActiveActivity)?.RefreshIfDirty(); } else { Handler.Post(() => { App.Kp2a.ShowMessage(ActiveActivity ?? LocaleManager.LocalizedAppContext, "Unrecoverable error: " + Message, MessageSeverity.Error); }); App.Kp2a.Lock(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; FindViewById(Resource.Id.fabSearch).Visibility = ViewStates.Gone; FindViewById(Resource.Id.fabTotpOverview).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.FindStructureItem(uuid); if (elementToMove.ParentGroup != Group) App.Kp2a.DirtyGroups.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); ListView.SetMultiChoiceModeListener(this); if (App.Kp2a.OpenDatabases.Any(db => db.CanWrite)) { ListView.ChoiceMode = ChoiceMode.MultipleModal; ListView.ItemLongClick += delegate(object sender, AdapterView.ItemLongClickEventArgs args) { ListView.SetItemChecked(args.Position, true); }; } else { ListView.ChoiceMode = ChoiceMode.None; } 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((GroupBaseActivity)Activity, checkedItems, new GroupBaseActivity.RefreshTask(handler, ((GroupBaseActivity)Activity)), App.Kp2a); break; case Resource.Id.menu_move: var navMove = new NavigateToFolderAndLaunchMoveElementTask(App.Kp2a.CurrentDb, 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)), App.Kp2a.CurrentDb); ProgressTask pt = new ProgressTask(App.Kp2a, Activity, copyTask); pt.Run(); break; case Resource.Id.menu_navigate: NavigateToFolder navNavigate = new NavigateToFolder(App.Kp2a.CurrentDb, 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.md_theme_secondary)); } 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(); UpdateMenuItemVisibilities(mode); return true; } public void OnItemCheckedStateChanged(ActionMode mode, int position, long id, bool @checked) { UpdateMenuItemVisibilities(mode); } private void UpdateMenuItemVisibilities(ActionMode mode) { 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(); } public void DeleteMultipleItems(GroupBaseActivity activity, List checkedItems, OnFinish onFinish, Kp2aApp app) { if (checkedItems.Any() == false) return; //sort checkedItems by database List>> itemsForDatabases = new List>>(); foreach (var item in checkedItems) { var db = app.FindDatabaseForElement(item); if (db != null) { bool foundDatabase = false; foreach (var listEntry in itemsForDatabases) { if (listEntry.Key == db) { foundDatabase = true; listEntry.Value.Add(item); break; } } if (!foundDatabase) { itemsForDatabases.Add(new KeyValuePair>(db, new List { item })); } } } int dbIndex = 0; Action action = null; action = (success, message, activeActivity) => { if (success) { dbIndex++; if (dbIndex == itemsForDatabases.Count) { onFinish.SetResult(true); onFinish.Run(); return; } new DeleteMultipleItemsFromOneDatabase(activity, itemsForDatabases[dbIndex].Key, itemsForDatabases[dbIndex].Value, new ActionOnFinish(activeActivity, (b, s, activity1) => action(b, s, activity1)), app) .Start(); } else { onFinish.SetResult(false, message, true, null); } }; new DeleteMultipleItemsFromOneDatabase(activity, itemsForDatabases[dbIndex].Key, itemsForDatabases[dbIndex].Value, new ActionOnFinish(activity, (b, s, activity1) => action(b, s, activity1)), app) .Start(); } } }