Files
keepass2android/src/keepass2android-app/settings/AppSettingsActivity.cs
2025-04-29 13:45:24 +02:00

1240 lines
51 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.Generic;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Content.Res;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using Android.Util;
using Android.Views;
using Android.Widget;
using AndroidX.AppCompat.App;
using AndroidX.Preference;
using keepass2android.Io;
using keepass2android.settings;
using keepass2android;
using KeePassLib.Cryptography.KeyDerivation;
using KeePassLib.Utility;
using KeePassLib;
using static Android.Icu.Text.CaseMap;
using Object = Java.Lang.Object;
using Preference = AndroidX.Preference.Preference;
using PreferenceFragment = AndroidX.Preference.PreferenceFragment;
using Toolbar = AndroidX.AppCompat.Widget.Toolbar;
using Android.Views.Autofill;
using Google.Android.Material.Dialog;
using AlertDialog = AndroidX.AppCompat.App.AlertDialog;
using String = System.String;
using KeePassLib.Cryptography.Cipher;
using keepass2android.Utils;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using FragmentManager = AndroidX.Fragment.App.FragmentManager;
namespace keepass2android
{
namespace settings
{
#region preference fragments
public class PreferenceFragmentWithResource : PreferenceFragmentCompat
{
private readonly int _resXml;
public PreferenceFragmentWithResource(int resXml)
{
_resXml = resXml;
}
public override void OnCreatePreferences(Bundle savedInstanceState, string rootKey)
{
SetPreferencesFromResource(_resXml, rootKey);
}
public override void OnResume()
{
base.OnResume();
if (Activity != null && !string.IsNullOrEmpty(PreferenceScreen?.Title))
{
Activity.Title = PreferenceScreen?.Title;
}
}
}
public class MainPreferenceFragment : PreferenceFragmentWithResource
{
public MainPreferenceFragment() : base(Resource.Xml.preferences)
{
}
public override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
FindPreference(GetString(Resource.String.db_key)).Enabled = (App.Kp2a.CurrentDb != null);
}
}
public class SettingsFragmentDatabase : PreferenceFragmentWithResource
{
public SettingsFragmentDatabase() : base(Resource.Xml.pref_database)
{
}
private void PrepareDefaultUsername(Database db)
{
Preference defaultUser = FindPreference(GetString(Resource.String.default_username_key));
if (!db.DatabaseFormat.HasDefaultUsername)
{
((PreferenceScreen)FindPreference(GetString(Resource.String.db_key))).RemovePreference(defaultUser);
}
else
{
defaultUser.Enabled = db.CanWrite;
//TODO test this
((EditTextPreference)defaultUser).Text = db.KpDatabase.DefaultUserName;
defaultUser.PreferenceChange += (sender, e) =>
{
DateTime previousUsernameChanged = db.KpDatabase.DefaultUserNameChanged;
var previousUsername = db.KpDatabase.DefaultUserName;
db.KpDatabase.DefaultUserName = e.NewValue.ToString();
SaveDb save = new SaveDb(Activity, App.Kp2a, App.Kp2a.CurrentDb, new ActionOnOperationFinished(Activity, (success, message, activity) =>
{
if (!success)
{
db.KpDatabase.DefaultUserName = previousUsername;
db.KpDatabase.DefaultUserNameChanged = previousUsernameChanged;
App.Kp2a.ShowMessage(activity, message, MessageSeverity.Error);
}
}));
ProgressTask pt = new ProgressTask(App.Kp2a, Activity, save);
pt.Run();
};
}
}
private void PrepareTemplates(Database db)
{
Preference pref = FindPreference("AddTemplates_pref_key");
if ((!db.DatabaseFormat.SupportsTemplates) || (AddTemplateEntries.ContainsAllTemplates(App.Kp2a.CurrentDb)))
{
pref.Enabled = false;
}
else
{
pref.PreferenceClick += (sender, args) =>
{
ProgressTask pt = new ProgressTask(App.Kp2a, Activity,
new AddTemplateEntries(Activity, App.Kp2a, new ActionOnOperationFinished(Activity,
delegate
{
pref.Enabled = false;
})));
pt.Run();
};
}
}
private void PrepareMasterPassword()
{
Preference changeMaster = FindPreference(GetString(Resource.String.master_pwd_key));
if (App.Kp2a.CurrentDb.CanWrite)
{
changeMaster.Enabled = true;
changeMaster.PreferenceClick += delegate { new SetPasswordDialog(Activity).Show(); };
}
}
private void PrepareDatabaseName(Database db)
{
Preference databaseName = FindPreference(GetString(Resource.String.database_name_key));
if (!db.DatabaseFormat.HasDatabaseName)
{
((PreferenceScreen)FindPreference(GetString(Resource.String.db_key))).RemovePreference(databaseName);
}
else
{
databaseName.Enabled = db.CanWrite;
((EditTextPreference)databaseName).Text = db.KpDatabase.Name;
databaseName.PreferenceChange += (sender, e) =>
{
DateTime previousNameChanged = db.KpDatabase.NameChanged;
String previousName = db.KpDatabase.Name;
db.KpDatabase.Name = e.NewValue.ToString();
SaveDb save = new SaveDb(Activity, App.Kp2a, App.Kp2a.CurrentDb, new ActionOnOperationFinished(Activity, (success, message, activity) =>
{
if (!success)
{
db.KpDatabase.Name = previousName;
db.KpDatabase.NameChanged = previousNameChanged;
App.Kp2a.ShowMessage(activity, message, MessageSeverity.Error);
}
else
{
// Name is reflected in notification, so update it
App.Kp2a.UpdateOngoingNotification();
}
}));
ProgressTask pt = new ProgressTask(App.Kp2a, Activity, save);
pt.Run();
};
}
}
private void UpdateImportDbPref()
{
//Import db/key file preferences:
Preference importDb = FindPreference("import_db_prefs");
bool isLocalOrContent =
App.Kp2a.CurrentDb.Ioc.IsLocalFile() || App.Kp2a.CurrentDb.Ioc.Path.StartsWith("content://");
if (!isLocalOrContent)
{
importDb.Summary = GetString(Resource.String.OnlyAvailableForLocalFiles);
importDb.Enabled = false;
}
else
{
if (IoUtil.IsInInternalDirectory(App.Kp2a.CurrentDb.Ioc.Path, Activity))
{
importDb.Summary = GetString(Resource.String.FileIsInInternalDirectory);
importDb.Enabled = false;
}
else
{
importDb.Enabled = true;
importDb.PreferenceClick += delegate { MoveDbToInternalFolder(); };
}
}
}
private void MoveDbToInternalFolder()
{
Func<Action> copyAndReturnPostExecute = () =>
{
try
{
var sourceIoc = App.Kp2a.CurrentDb.Ioc;
var newIoc = IoUtil.ImportFileToInternalDirectory(sourceIoc, Activity, App.Kp2a);
return () =>
{
var builder = new MaterialAlertDialogBuilder(Activity);
builder
.SetMessage(Resource.String.DatabaseFileMoved);
builder.SetPositiveButton(Android.Resource.String.Ok, (sender, args) =>
{
var key = App.Kp2a.CurrentDb.KpDatabase.MasterKey;
App.Kp2a.CloseDatabase(App.Kp2a.CurrentDb);
PasswordActivity.Launch(Activity, newIoc, key, new ActivityLaunchModeSimple(), false);
});
builder.Show();
};
}
catch (System.Exception e)
{
return () =>
{
App.Kp2a.ShowMessage(Activity, App.Kp2a.GetResourceString(UiStringKey.ErrorOcurred) + " " + Util.GetErrorMessage(e), MessageSeverity.Error);
};
}
};
new SimpleLoadingDialog(Activity, GetString(Resource.String.CopyingFile), false,
copyAndReturnPostExecute
).Execute();
}
private void UpdateImportKeyfilePref()
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(Activity);
var rememberKeyfile = prefs.GetBoolean(GetString(Resource.String.keyfile_key), Resources.GetBoolean(Resource.Boolean.keyfile_default));
Preference importKeyfile = FindPreference("import_keyfile_prefs");
Preference exportKeyfile = FindPreference("export_keyfile_prefs");
importKeyfile.Summary = "";
if (!rememberKeyfile)
{
importKeyfile.Summary = GetString(Resource.String.KeyfileMoveRequiresRememberKeyfile);
importKeyfile.Enabled = false;
exportKeyfile.Enabled = false;
return;
}
CompositeKey masterKey = App.Kp2a.CurrentDb.KpDatabase.MasterKey;
if (masterKey.ContainsType(typeof(KcpKeyFile)))
{
IOConnectionInfo iocKeyfile = ((KcpKeyFile)masterKey.GetUserKey(typeof(KcpKeyFile))).Ioc;
if (iocKeyfile.IsLocalFile() && IoUtil.IsInInternalDirectory(iocKeyfile.Path, Activity))
{
importKeyfile.Enabled = false;
exportKeyfile.Enabled = true;
exportKeyfile.PreferenceClick += (sender, args) => { ExportKeyfileFromInternalFolder(); };
importKeyfile.Summary = GetString(Resource.String.FileIsInInternalDirectory);
}
else
{
exportKeyfile.Enabled = false;
importKeyfile.Enabled = true;
importKeyfile.PreferenceClick += (sender, args) => { MoveKeyfileToInternalFolder(); };
}
}
else
{
exportKeyfile.Enabled = false;
importKeyfile.Enabled = false;
}
}
private void ExportKeyfileFromInternalFolder()
{
StartActivity(new Intent(Activity.ApplicationContext, typeof(ExportKeyfileActivity)));
}
private void MoveKeyfileToInternalFolder()
{
Func<Action> copyAndReturnPostExecute = () =>
{
try
{
CompositeKey masterKey = App.Kp2a.CurrentDb.KpDatabase.MasterKey;
var sourceIoc = ((KcpKeyFile)masterKey.GetUserKey(typeof(KcpKeyFile))).Ioc;
var newIoc = IoUtil.ImportFileToInternalDirectory(sourceIoc, Activity, App.Kp2a);
((KcpKeyFile)masterKey.GetUserKey(typeof(KcpKeyFile))).ResetIoc(newIoc);
var keyfileString = IOConnectionInfo.SerializeToString(newIoc);
App.Kp2a.StoreOpenedFileAsRecent(App.Kp2a.CurrentDb.Ioc, keyfileString, false);
return () =>
{
UpdateImportKeyfilePref();
var builder = new MaterialAlertDialogBuilder(Activity);
builder
.SetMessage(Resource.String.KeyfileMoved);
builder.SetPositiveButton(Android.Resource.String.Ok, (sender, args) => { });
builder.Show();
};
}
catch (System.Exception e)
{
return () =>
{
App.Kp2a.ShowMessage(Activity, App.Kp2a.GetResourceString(UiStringKey.ErrorOcurred) + " " + Util.GetErrorMessage(e), MessageSeverity.Error);
};
}
};
new SimpleLoadingDialog(Activity, GetString(Resource.String.CopyingFile), false,
copyAndReturnPostExecute
).Execute();
}
public override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
var db = App.Kp2a.CurrentDb;
if (db != null)
{
PrepareDefaultUsername(App.Kp2a.CurrentDb);
PrepareDatabaseName(db);
PrepareMasterPassword();
PrepareTemplates(db);
PrepareEncryptionAlgorithm(db);
UpdateImportDbPref();
UpdateImportKeyfilePref();
}
}
private void PrepareEncryptionAlgorithm(Database db)
{
ListPreference algorithmPref = (ListPreference)FindPreference(GetString(Resource.String.algorithm_key));
algorithmPref.SetEntries(CipherPool.GlobalPool.Engines.Select(eng => eng.DisplayName).ToArray());
string[] algoValues = CipherPool.GlobalPool.Engines.Select(eng => eng.CipherUuid.ToHexString()).ToArray();
algorithmPref.SetEntryValues(algoValues);
algorithmPref.SetValueIndex(algoValues.Select((v, i) => new { kdf = v, index = i }).First(el => el.kdf == db.KpDatabase.DataCipherUuid.ToHexString()).index);
algorithmPref.PreferenceChange += AlgorithmPrefChange;
algorithmPref.Summary =
CipherPool.GlobalPool.GetCipher(App.Kp2a.CurrentDb.KpDatabase.DataCipherUuid).DisplayName;
}
private void AlgorithmPrefChange(object sender, Preference.PreferenceChangeEventArgs preferenceChangeEventArgs)
{
var db = App.Kp2a.CurrentDb;
var previousCipher = db.KpDatabase.DataCipherUuid;
db.KpDatabase.DataCipherUuid = new PwUuid(MemUtil.HexStringToByteArray((string)preferenceChangeEventArgs.NewValue));
SaveDb save = new SaveDb(Activity, App.Kp2a, App.Kp2a.CurrentDb, new ActionOnOperationFinished(Activity, (success, message, activity) =>
{
if (!success)
{
db.KpDatabase.DataCipherUuid = previousCipher;
App.Kp2a.ShowMessage(activity, message, MessageSeverity.Error);
return;
}
preferenceChangeEventArgs.Preference.Summary =
CipherPool.GlobalPool.GetCipher(db.KpDatabase.DataCipherUuid).DisplayName;
}));
ProgressTask pt = new ProgressTask(App.Kp2a, Activity, save);
pt.Run();
}
}
public class SettingsFragmentApp : PreferenceFragmentWithResource
{
public SettingsFragmentApp() : base(Resource.Xml.pref_app)
{
}
}
public class SecurityPreferenceFragment : PreferenceFragmentWithResource
{
public SecurityPreferenceFragment() : base(Resource.Xml.pref_app_security)
{
}
void OnRememberKeyFileHistoryChanged(object sender, Preference.PreferenceChangeEventArgs eventArgs)
{
if (!(bool)eventArgs.NewValue)
{
App.Kp2a.FileDbHelper.DeleteAllKeys();
}
}
public override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
FindPreference(GetString(Resource.String.keyfile_key)).PreferenceChange += OnRememberKeyFileHistoryChanged;
}
}
public class DisplayPreferenceFragment : PreferenceFragmentWithResource
{
public DisplayPreferenceFragment() : base(Resource.Xml.pref_app_display)
{
}
void OnShowUnlockedNotificationChanged(object sender, Preference.PreferenceChangeEventArgs eventArgs)
{
App.Kp2a.UpdateOngoingNotification();
}
private void PrepareNoDonationReminderPreference(Activity ctx, PreferenceScreen screen, Preference preference)
{
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(ctx);
if (!prefs.GetBoolean("DismissedDonateReminder", false))
{
screen.RemovePreference(preference);
}
}
public void PrepareNoDonatePreference(Context ctx, Preference preference)
{
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(ctx);
long usageCount = prefs.GetLong(ctx.GetString(Resource.String.UsageCount_key), 0);
#if DEBUG
preference.Enabled = (usageCount > 1);
#else
preference.Enabled = (usageCount > 50);
#endif
preference.PreferenceChange += delegate (object sender, Preference.PreferenceChangeEventArgs args)
{
if ((bool)args.NewValue)
{
new MaterialAlertDialogBuilder(ctx)
.SetTitle(ctx.GetString(AppNames.AppNameResource))
.SetCancelable(false)
.SetPositiveButton(Android.Resource.String.Ok, delegate (object o, DialogClickEventArgs eventArgs)
{
Util.GotoDonateUrl(ctx);
((Dialog)o).Dismiss();
})
.SetMessage(Resource.String.NoDonateOption_question)
.Create().Show();
}
};
}
public override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
var unlockedNotificationPref = FindPreference(GetString(Resource.String.ShowUnlockedNotification_key));
unlockedNotificationPref.PreferenceChange += OnShowUnlockedNotificationChanged;
if ((int)Build.VERSION.SdkInt >= 26)
{
//use system notification channels to control notification visibility
unlockedNotificationPref.Parent.RemovePreference(unlockedNotificationPref);
}
unlockedNotificationPref.PreferenceChange += (sender, args) => App.Kp2a.UpdateOngoingNotification();
HashSet<string> supportedLocales = new HashSet<string>() { "en", "af", "ar", "az", "be", "bg", "ca", "cs", "da", "de", "el", "es", "eu", "fa", "fi", "fr", "gl", "he", "hr", "hu", "id", "in", "it", "iw", "ja", "ko", "ml", "nb", "nl", "nn", "no", "pl", "pt", "ro", "ru", "si", "sk", "sl", "sr", "sv", "tr", "uk", "vi", "zh" };
var languagePref = (ListPreference)FindPreference(GetString(Resource.String.app_language_pref_key));
new AppLanguageManager(this, languagePref, supportedLocales);
PrepareNoDonatePreference(Activity, FindPreference(GetString(Resource.String.NoDonateOption_key)));
var displayPrefScreen = ((PreferenceScreen)FindPreference(GetString(Resource.String.display_prefs_key)));
PrepareNoDonationReminderPreference(Activity, displayPrefScreen, FindPreference(GetString(Resource.String.NoDonationReminder_key)));
FindPreference(GetString(Resource.String.design_key)).PreferenceChange += (sender, args) =>
{
//it would be nicer to recreate, but that - for some reason - causes GroupActivity to be twice on the backstack afterwards :-(
//So better finish here.
Activity.Finish();
};
displayPrefScreen.RemovePreference(unlockedNotificationPref);
FindPreference("IconSetKey").PreferenceChange += (sender, args) =>
{
if (App.Kp2a.CurrentDb != null)
App.Kp2a.CurrentDb.DrawableFactory.Clear();
};
}
}
public class QuickUnlockPreferenceFragment : PreferenceFragmentWithResource
{
public QuickUnlockPreferenceFragment() : base(Resource.Xml.pref_app_quick_unlock)
{
}
public override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
try
{
//depending on Android version, we offer to use a transparent icon for QuickUnlock or use the notification priority (since API level 16)
Preference hideQuickUnlockTranspIconPref = FindPreference(GetString(Resource.String.QuickUnlockIconHidden_key));
Preference hideQuickUnlockIconPref = FindPreference(GetString(Resource.String.QuickUnlockIconHidden16_key));
var quickUnlockScreen = ((PreferenceScreen)FindPreference(GetString(Resource.String.QuickUnlock_prefs_key)));
if ((int)Android.OS.Build.VERSION.SdkInt >= 26)
{
//use notification channels
quickUnlockScreen.RemovePreference(hideQuickUnlockTranspIconPref);
quickUnlockScreen.RemovePreference(hideQuickUnlockIconPref);
}
else if ((int)Android.OS.Build.VERSION.SdkInt >= 16)
{
quickUnlockScreen.RemovePreference(hideQuickUnlockTranspIconPref);
hideQuickUnlockIconPref.PreferenceChange += delegate { App.Kp2a.UpdateOngoingNotification(); };
}
else
{
//old version: only show transparent quickUnlock and no option to hide unlocked icon:
quickUnlockScreen.RemovePreference(hideQuickUnlockIconPref);
FindPreference(GetString(Resource.String.QuickUnlockIconHidden_key)).PreferenceChange +=
delegate { App.Kp2a.UpdateOngoingNotification(); };
}
}
catch (Exception ex)
{
Kp2aLog.LogUnexpectedError(ex);
}
}
}
public class FileHandlingPreferenceFragment : PreferenceFragmentWithResource
{
public FileHandlingPreferenceFragment() : base(Resource.Xml.pref_app_file_handling)
{
}
private void OnUseOfflineCacheChanged(object sender, Preference.PreferenceChangeEventArgs e)
{
if (!(bool)e.NewValue)
{
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(Activity);
builder.SetTitle(GetString(Resource.String.ClearOfflineCache_title));
builder.SetMessage(GetString(Resource.String.ClearOfflineCache_question));
builder.SetPositiveButton(App.Kp2a.GetResourceString(UiStringKey.yes), (o, args) =>
{
try
{
App.Kp2a.ClearOfflineCache();
}
catch (Exception ex)
{
Kp2aLog.LogUnexpectedError(ex);
App.Kp2a.ShowMessage(LocaleManager.LocalizedAppContext, Util.GetErrorMessage(ex), MessageSeverity.Error);
}
}
);
builder.SetNegativeButton(App.Kp2a.GetResourceString(UiStringKey.no), (o, args) =>
{
((CheckBoxPreference)e.Preference).Checked = true;
}
);
builder.SetCancelable(false);
Dialog dialog = builder.Create();
dialog.Show();
}
}
public override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
Preference cachingPreference = FindPreference(GetString(Resource.String.UseOfflineCache_key));
cachingPreference.PreferenceChange += OnUseOfflineCacheChanged;
}
}
public class TotpPreferenceFragment : PreferenceFragmentWithResource
{
public TotpPreferenceFragment() : base(Resource.Xml.pref_app_traytotp)
{
}
}
public class DebugLogPreferenceFragment : PreferenceFragmentWithResource
{
public DebugLogPreferenceFragment() : base(Resource.Xml.pref_app_debug)
{
}
private void UpdateDependingPreferences(bool debugLogEnabled)
{
FindPreference(GetString(Resource.String.DebugLog_send_key)).Visible = debugLogEnabled;
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
FindPreference(GetString(Resource.String.FtpDebug_key)).Visible = debugLogEnabled;
#endif
}
private void OnSendDebug(object sender, Preference.PreferenceClickEventArgs e)
{
Kp2aLog.SendLog(this.Activity);
}
private void OnDebugLogChanged(object sender, Preference.PreferenceChangeEventArgs e)
{
if ((bool)e.NewValue)
Kp2aLog.CreateLogFile();
else
Kp2aLog.FinishLogFile();
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
SetJSchLogging(PreferenceManager.GetDefaultSharedPreferences(Application.Context)
.GetBoolean(Application.Context.GetString(Resource.String.FtpDebug_key), false));
#endif
UpdateDependingPreferences((bool)e.NewValue);
}
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
private void OnJSchDebugChanged(object sender, Preference.PreferenceChangeEventArgs e)
{
bool debugEnabled = (bool)e.NewValue;
SetJSchLogging(debugEnabled);
string prefKey = Application.Context.GetString(Resource.String.FtpDebug_key);
PreferenceManager.SharedPreferences.Edit().PutBoolean(prefKey, debugEnabled).Apply();
}
private void SetJSchLogging(bool enabled)
{
var sftpStorage = new Keepass2android.Javafilestorage.SftpStorage(Context);
string? logFilename = null;
if (Kp2aLog.LogToFile)
{
logFilename = Kp2aLog.LogFilename;
}
sftpStorage.SetJschLogging(enabled, logFilename);
}
#endif
public override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
FindPreference(GetString(Resource.String.DebugLog_key)).PreferenceChange += OnDebugLogChanged;
FindPreference(GetString(Resource.String.DebugLog_send_key)).PreferenceClick += OnSendDebug;
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
FindPreference(GetString(Resource.String.FtpDebug_key)).PreferenceChange += OnJSchDebugChanged;
#else
FindPreference(GetString(Resource.String.FtpDebug_key)).Enabled = false;
#endif
bool debugLogEnabled = (PreferenceManager.GetDefaultSharedPreferences(Application.Context)
.GetBoolean(Application.Context.GetString(Resource.String.DebugLog_key), false));
UpdateDependingPreferences(debugLogEnabled);
}
}
public class KeyboardSwitchPreferenceFragment : PreferenceFragmentWithResource
{
public class KeyboardSwitchPrefManager
{
private readonly Activity _act;
private readonly CheckBoxPreference _switchPref;
private readonly CheckBoxPreference _openKp2aAutoPref;
private readonly CheckBoxPreference _openOnlyOnSearchPref;
private CheckBoxPreference _switchBackPref;
private readonly PreferenceScreen _screen;
private readonly PreferenceFragmentCompat _fragment;
public KeyboardSwitchPrefManager(PreferenceFragmentCompat fragment)
{
var act = fragment.Activity;
this._act = act;
this._fragment = fragment;
this._screen = (PreferenceScreen)_fragment.FindPreference(act.GetString(Resource.String.keyboardswitch_prefs_key));
var keyboardSwapPref = _fragment.FindPreference("get_keyboardswap");
var pm = act.PackageManager;
var intnt = Keepass2android.Kbbridge.ImeSwitcher.GetLaunchIntentForKeyboardSwap(act);
if ((intnt != null) && pm.QueryIntentActivities(intnt, 0).Any())
{
_screen.RemovePreference(keyboardSwapPref);
}
else
{
keyboardSwapPref.PreferenceClick += (sender, args) =>
{
Util.GotoUrl(act, act.GetString(Resource.String.MarketURL) + "keepass2android.plugin.keyboardswap2");
};
}
_switchPref = (CheckBoxPreference)_fragment.FindPreference("kp2a_switch_rooted");
_openKp2aAutoPref =
(CheckBoxPreference)_fragment.FindPreference(act.GetString(Resource.String.OpenKp2aKeyboardAutomatically_key));
_openOnlyOnSearchPref =
(CheckBoxPreference)
_fragment.FindPreference(act.GetString(Resource.String.OpenKp2aKeyboardAutomaticallyOnlyAfterSearch_key));
_switchBackPref =
(CheckBoxPreference)_fragment.FindPreference(act.GetString(Resource.String.AutoSwitchBackKeyboard_key));
EnableSwitchPreferences(_switchPref.Checked);
_switchPref.PreferenceChange += (sender, args) =>
{
bool switchOnRooted = (bool)args.NewValue;
EnableSwitchPreferences(switchOnRooted);
};
}
private void EnableSwitchPreferences(bool switchOnRooted)
{
if (!switchOnRooted)
{
if (_fragment.FindPreference(_act.GetString(Resource.String.OpenKp2aKeyboardAutomatically_key)) == null)
{
_screen.AddPreference(_openKp2aAutoPref);
}
if (_fragment.FindPreference(_act.GetString(Resource.String.OpenKp2aKeyboardAutomaticallyOnlyAfterSearch_key)) != null)
{
_screen.RemovePreference(_openOnlyOnSearchPref);
}
}
else
{
{
_screen.RemovePreference(_openKp2aAutoPref);
}
if (_fragment.FindPreference(_act.GetString(Resource.String.OpenKp2aKeyboardAutomaticallyOnlyAfterSearch_key)) == null)
{
_screen.AddPreference(_openOnlyOnSearchPref);
}
}
/*_openKp2aAutoPref.Enabled = !switchOnRooted;
_openOnlyOnSearchPref.Enabled = switchOnRooted;
_switchBackPref.Enabled = switchOnRooted;*/
}
}
KeyboardSwitchPrefManager _manager;
public KeyboardSwitchPreferenceFragment() : base(Resource.Xml.pref_app_password_access_keyboard_switch)
{
}
public override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
_manager = new KeyboardSwitchPrefManager(this);
}
}
public class AutofillPreferenceFragment : PreferenceFragmentWithResource
{
public AutofillPreferenceFragment() : base(Resource.Xml.pref_app_password_access_autofill)
{
}
private void UpdateAutofillPref()
{
var autofillScreen = FindPreference(GetString(Resource.String.AutoFill_prefs_screen_key));
var autofillPref = FindPreference(GetString(Resource.String.AutoFill_prefs_key));
var autofillDisabledPref = FindPreference(GetString(Resource.String.AutofillDisabledQueriesPreference_key));
var autofillSavePref = FindPreference(GetString(Resource.String.OfferSaveCredentials_key));
var autofillInlineSuggestions = FindPreference(GetString(Resource.String.InlineSuggestions_key));
var noAutofillDisablingPref = FindPreference(GetString(Resource.String.NoAutofillDisabling_key));
var autofillNoDalVerification = FindPreference(GetString(Resource.String.NoDalVerification_key));
if (autofillPref == null)
return;
if ((Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.O) ||
!((AutofillManager)Activity.GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager))))
.IsAutofillSupported)
{
var passwordAccessScreen =
(PreferenceScreen)FindPreference(Activity.GetString(Resource.String.password_access_prefs_key));
passwordAccessScreen.RemovePreference(autofillScreen);
}
else
{
if (((AutofillManager)Activity.GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager))))
.HasEnabledAutofillServices)
{
autofillDisabledPref.Enabled = true;
autofillSavePref.Enabled = true;
autofillNoDalVerification.Enabled = true;
autofillInlineSuggestions.Enabled = true;
noAutofillDisablingPref.Enabled = true;
autofillPref.Summary = Activity.GetString(Resource.String.plugin_enabled);
autofillPref.Intent = new Intent(Intent.ActionView);
autofillPref.Intent.SetData(Android.Net.Uri.Parse("https://philippc.github.io/keepass2android/OreoAutoFill.html"));
}
else
{
autofillNoDalVerification.Enabled = false;
autofillDisabledPref.Enabled = false;
autofillSavePref.Enabled = false;
noAutofillDisablingPref.Enabled = false;
autofillInlineSuggestions.Enabled = false;
autofillPref.Summary = Activity.GetString(Resource.String.not_enabled);
}
if ((int)Android.OS.Build.VERSION.SdkInt < 30)
{
autofillInlineSuggestions.Summary = Activity.GetString(Resource.String.requires_android11);
CheckBoxPreference cbp = autofillInlineSuggestions as CheckBoxPreference;
if (cbp != null)
cbp.Checked = false;
autofillInlineSuggestions.Enabled = false;
}
}
}
public override void OnResume()
{
base.OnResume();
UpdateAutofillPref();
}
public override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
UpdateAutofillPref();
var autofillPref = FindPreference(GetString(Resource.String.AutoFill_prefs_key));
if (autofillPref != null)
{
autofillPref.PreferenceClick += (sender, args) =>
{
var intent = new Intent(Android.Provider.Settings.ActionRequestSetAutofillService);
if (((AutofillManager)Activity.GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager))))
.HasEnabledAutofillServices)
{
intent.SetData(Android.Net.Uri.Parse("package:" + Context.PackageName + "notexisting")); //if we use our package name, the activity won't launch
}
else
{
intent.SetData(Android.Net.Uri.Parse("package:" + Context.PackageName));
}
try
{
Context.StartActivity(intent);
}
catch (ActivityNotFoundException e)
{
//this exception was reported by many Huawei users
Kp2aLog.LogUnexpectedError(e);
new MaterialAlertDialogBuilder(Context)
.SetTitle(Resource.String.autofill_enable)
.SetMessage(Resource.String.autofill_enable_failed)
.SetPositiveButton(Android.Resource.String.Ok, (o, eventArgs) => { })
.Show();
}
catch (System.Exception e)
{
Kp2aLog.LogUnexpectedError(e);
}
};
}
}
}
public class AutofillTotpPreferenceFragment : PreferenceFragmentWithResource
{
public AutofillTotpPreferenceFragment() : base(Resource.Xml.pref_app_password_access_autofill_totp)
{
}
}
public class PasswordAccessPreferenceFragment : PreferenceFragmentWithResource
{
public PasswordAccessPreferenceFragment() : base(Resource.Xml.pref_app_password_access)
{
}
}
public class KeyDerivFuncPreferenceFragment : PreferenceFragmentWithResource
{
private Preference aesRounds, argon2parallelism, argon2rounds, argon2memory;
public KeyDerivFuncPreferenceFragment() : base(Resource.Xml.pref_database_key_deriv_func)
{
}
public override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
aesRounds = FindPreference(GetString(Resource.String.rounds_key));
argon2rounds = FindPreference("argon2rounds");
argon2memory = FindPreference("argon2memory");
argon2parallelism = FindPreference("argon2parallelism");
aesRounds.PreferenceChange += (sender, e) => UpdateKdfSummary(e.Preference);
argon2rounds.PreferenceChange += (sender, e) => UpdateKdfSummary(e.Preference);
argon2memory.PreferenceChange += (sender, e) => UpdateKdfSummary(e.Preference);
argon2parallelism.PreferenceChange += (sender, e) => UpdateKdfSummary(e.Preference);
var db = App.Kp2a.CurrentDb;
if (db != null)
{
ListPreference kdfPref = (ListPreference)FindPreference(GetString(Resource.String.kdf_key));
kdfPref.SetEntries(KdfPool.Engines.Select(eng => eng.Name).ToArray());
string[] kdfValues = KdfPool.Engines.Select(eng => eng.Uuid.ToHexString()).ToArray();
kdfPref.SetEntryValues(kdfValues);
kdfPref.SetValueIndex(kdfValues.Select((v, i) => new { kdf = v, index = i }).First(el => el.kdf == db.KpDatabase.KdfParameters.KdfUuid.ToHexString()).index);
kdfPref.PreferenceChange += OnKdfChange;
UpdateKdfScreen();
}
}
public override void OnDisplayPreferenceDialog(Preference preference)
{
if (preference is KdfNumberDialogPreference dialogPreference)
{
dialogPreference.ShowDialog(this);
}
else
base.OnDisplayPreferenceDialog(preference);
}
private void UpdateKdfScreen()
{
var db = App.Kp2a.CurrentDb;
var kdf = KdfPool.Get(db.KpDatabase.KdfParameters.KdfUuid);
var kdfpref = FindPreference(GetString(Resource.String.kdf_key));
kdfpref.Summary = kdf.Name;
var kdfscreen = ((PreferenceScreen)FindPreference(GetString(Resource.String.kdf_screen_key)));
if (kdf is AesKdf)
{
if (kdfscreen.FindPreference(GetString(Resource.String.rounds_key)) == null)
kdfscreen.AddPreference(aesRounds);
kdfscreen.RemovePreference(argon2rounds);
kdfscreen.RemovePreference(argon2memory);
kdfscreen.RemovePreference(argon2parallelism);
aesRounds.Enabled = db.CanWrite;
UpdateKdfSummary(aesRounds);
}
else
{
kdfscreen.RemovePreference(aesRounds);
if (kdfscreen.FindPreference("argon2rounds") == null)
{
kdfscreen.AddPreference(argon2rounds);
kdfscreen.AddPreference(argon2memory);
kdfscreen.AddPreference(argon2parallelism);
}
UpdateKdfSummary(argon2rounds);
UpdateKdfSummary(argon2memory);
UpdateKdfSummary(argon2parallelism);
}
}
private void OnKdfChange(object sender, Preference.PreferenceChangeEventArgs preferenceChangeEventArgs)
{
var db = App.Kp2a.CurrentDb;
var previousKdfParams = db.KpDatabase.KdfParameters;
Kp2aLog.Log("previous kdf: " + KdfPool.Get(db.KpDatabase.KdfParameters.KdfUuid) + " " + db.KpDatabase.KdfParameters.KdfUuid.ToHexString());
db.KpDatabase.KdfParameters =
KdfPool.Get(
new PwUuid(MemUtil.HexStringToByteArray((string)preferenceChangeEventArgs.NewValue)))
.GetDefaultParameters();
Kp2aLog.Log("--new kdf: " + KdfPool.Get(db.KpDatabase.KdfParameters.KdfUuid) + " " + db.KpDatabase.KdfParameters.KdfUuid.ToHexString());
SaveDb save = new SaveDb(Activity, App.Kp2a, App.Kp2a.CurrentDb, new ActionOnOperationFinished(Activity, (success, message, activity) =>
{
if (!success)
{
db.KpDatabase.KdfParameters = previousKdfParams;
App.Kp2a.ShowMessage(activity, message, MessageSeverity.Error);
return;
}
UpdateKdfScreen();
}));
ProgressTask pt = new ProgressTask(App.Kp2a, Activity, save);
pt.Run();
}
private void UpdateKdfSummary(Preference preference)
{
preference.Summary = ((KdfNumberDialogPreference)preference).ParamValue.ToString();
}
}
#endregion
}
public class SettingsFragmentManager
{
private readonly LifecycleAwareActivity _parentActivity;
private const string TitleTag = "settingsActivityTitle";
public SettingsFragmentManager(LifecycleAwareActivity parentActivity)
{
_parentActivity = parentActivity;
_parentActivity.OnCreateListener = OnCreate;
_parentActivity.OnSaveInstanceStateListener = OnSaveInstanceState;
_parentActivity.OnSupportNavigateUpListener = OnSupportNavigateUp;
}
protected void OnCreate(Bundle savedInstanceState)
{
_parentActivity.SetContentView(Resource.Layout.preference);
if (savedInstanceState == null)
{
_parentActivity.SupportFragmentManager
.BeginTransaction()
.Replace(Resource.Id.settings, new MainPreferenceFragment())
.Commit();
}
else
{
_parentActivity.Title = savedInstanceState.GetCharSequence(TitleTag);
}
_parentActivity.SupportActionBar?.SetDisplayHomeAsUpEnabled(true);
}
protected void OnSaveInstanceState(Bundle outState)
{
// Save the current activity title to restore after configuration changes
outState.PutCharSequence(TitleTag, _parentActivity.Title);
}
public bool? OnSupportNavigateUp()
{
if (_parentActivity.SupportFragmentManager.PopBackStackImmediate())
{
if (_parentActivity.SupportFragmentManager.BackStackEntryCount == 0)
{
_parentActivity.SetTitle(Resource.String.app_name);
}
return true;
}
else
{
_parentActivity.Finish();
return true;
}
}
public bool OnPreferenceStartFragment(PreferenceFragmentCompat caller, AndroidX.Preference.Preference pref)
{
var t = Type.GetType(pref.Fragment);
var javaName = Java.Lang.Class.FromType(t).Name;
// Instantiate the new Fragment
var args = pref.Extras;
var fragment = _parentActivity.SupportFragmentManager.FragmentFactory.Instantiate(
_parentActivity.ClassLoader,
javaName);
fragment.Arguments = args;
fragment.SetTargetFragment(caller, 0);
// Replace the existing Fragment with the new Fragment
_parentActivity.SupportFragmentManager.BeginTransaction()
.Replace(Resource.Id.settings, fragment)
.AddToBackStack(null)
.Commit();
_parentActivity.Title = pref.Title;
return true;
}
public void OnBackStackChanged()
{
}
}
/// <summary>
/// Activity to configure the application, without database settings. Does not require an unlocked database, or close when the database is locked
/// </summary>
[Activity(Label = "@string/app_name", Theme = "@style/Kp2aTheme_BlueActionBar", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden)]
public class AppSettingsActivity : LockingActivity, PreferenceFragmentCompat.IOnPreferenceStartFragmentCallback, FragmentManager.IOnBackStackChangedListener
{
private ActivityDesign _design;
public static bool BeingRecreated = false;
public AppSettingsActivity()
{
_design = new ActivityDesign(this);
settingsFragmentManager = new SettingsFragmentManager(this);
//TODO adding this makes the app crash on back (https://github.com/dotnet/android-libraries/issues/1055)
//We need this in order to update the activity title to the re-activated preference
//SupportFragmentManager.AddOnBackStackChangedListener(this);
}
public static void Launch(Context ctx)
{
ctx.StartActivity(new Intent(ctx, typeof(AppSettingsActivity)));
}
protected override void OnCreate(Bundle savedInstanceState)
{
_design.ApplyTheme();
base.OnCreate(savedInstanceState);
}
public SettingsFragmentManager settingsFragmentManager;
public bool OnPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref)
{
return settingsFragmentManager.OnPreferenceStartFragment(caller, pref);
}
public void OnBackStackChanged()
{
settingsFragmentManager.OnBackStackChanged();
}
}
}