1695 lines
64 KiB
C#
1695 lines
64 KiB
C#
/*
|
|
This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file is based on Keepassdroid, Copyright Brian Pellin.
|
|
|
|
Keepass2Android is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Keepass2Android is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Keepass2Android. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
using System;
|
|
using 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<int /*resource id*/, int /*prio*/> bottomBarElementsPriority = new Dictionary<int, int>()
|
|
{
|
|
{ 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<int /*resource id*/> showableBottomBarElements = new HashSet<int>();
|
|
|
|
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<RelativeLayout>(Resource.Id.bottom_bar);
|
|
//check for null because the "empty" layouts may not have all views
|
|
int highestPrio = -1;
|
|
HashSet<int> highestPrioElements = new HashSet<int>();
|
|
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<GroupListFragment>(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<TextView>(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<string> applicableInfoTextKeys = new List<string> { masterKeyKey };
|
|
|
|
if (App.Kp2a.GetFileStorage(App.Kp2a.CurrentDb.Ioc).UserShouldBackup)
|
|
{
|
|
applicableInfoTextKeys.Add(backupKey);
|
|
}
|
|
if (App.Kp2a.CurrentDb.EntriesById.Count > 15)
|
|
{
|
|
applicableInfoTextKeys.Add(emergencyKey);
|
|
}
|
|
|
|
List<string> enabledInfoTextKeys = new List<string>();
|
|
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<TextView>(Resource.Id.info_head).Text = infoHead;
|
|
FindViewById<TextView>(Resource.Id.info_main).Text = infoMain;
|
|
var additionalInfoText = FindViewById<TextView>(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<GroupListFragment>(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<Google.Android.Material.FloatingActionButton.ExtendedFloatingActionButton>(Resource.Id
|
|
.fabAddNewEntry).Shrink();
|
|
FindViewById<Google.Android.Material.FloatingActionButton.ExtendedFloatingActionButton>(Resource.Id
|
|
.fabAddNewGroup).Extended = false;
|
|
FindViewById<Google.Android.Material.FloatingActionButton.ExtendedFloatingActionButton>(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<UiStringKey> reason = new OptionalOut<UiStringKey>();
|
|
|
|
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<TextView>(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<IStructureItem> 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>();
|
|
|
|
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<string> 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<GroupListFragment>(Resource.Id.list_fragment).ListView;
|
|
var checkedItemPositions = listView.CheckedItemPositions;
|
|
|
|
List<IStructureItem> checkedItems = new List<IStructureItem>();
|
|
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<IStructureItem> checkedItems, OnFinish onFinish, Kp2aApp app)
|
|
{
|
|
if (checkedItems.Any() == false)
|
|
return;
|
|
//sort checkedItems by database
|
|
List<KeyValuePair<Database, List<IStructureItem>>> itemsForDatabases =
|
|
new List<KeyValuePair<Database, List<IStructureItem>>>();
|
|
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<Database, List<IStructureItem>>(db, new List<IStructureItem> { item }));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
int dbIndex = 0;
|
|
Action<bool, string, Activity> 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();
|
|
}
|
|
|
|
}
|
|
}
|
|
|