keepass2android/src/keepass2android/app/App.cs

887 lines
25 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 System.IO;
using System.Net.Security;
using Android.App;
using Android.Content;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using KeePassLib;
using KeePassLib.Cryptography.Cipher;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using Android.Preferences;
using Android.Support.V4.App;
#if !EXCLUDE_TWOFISH
using TwofishCipher;
#endif
using Keepass2android.Pluginsdk;
using keepass2android.Io;
using keepass2android.addons.OtpKeyProv;
namespace keepass2android
{
#if NoNet
/// <summary>
/// Static strings containing App names for the Offline ("nonet") release
/// </summary>
public static class AppNames
{
public const string AppName = "@string/app_name_nonet";
public const int AppNameResource = Resource.String.app_name_nonet;
public const string AppNameShort = "@string/short_app_name_nonet";
public const string AppLauncherTitle = "@string/short_app_name_nonet";
public const string PackagePart = "keepass2android_nonet";
public const int LauncherIcon = Resource.Drawable.ic_launcher_offline;
public const int NotificationLockedIcon = Resource.Drawable.ic_notify_offline;
public const int NotificationUnlockedIcon = Resource.Drawable.ic_notify_locked;
public const string Searchable = "@xml/searchable_offline";
}
#else
/// <summary>
/// Static strings containing App names for the Online release
/// </summary>
public static class AppNames
{
#if DEBUG
public const string AppName = "@string/app_name_debug";
#else
public const string AppName = "@string/app_name";
#endif
public const int AppNameResource = Resource.String.app_name;
public const string AppNameShort = "@string/short_app_name" + "DBG";
public const string AppLauncherTitle = "@string/app_name" + " Debug";
#if DEBUG
public const string PackagePart = "keepass2android_debug";
public const string Searchable = "@xml/searchable_debug";
#else
public const string PackagePart = "keepass2android";
public const string Searchable = "@xml/searchable";
#endif
public const int LauncherIcon = Resource.Drawable.ic_launcher_online;
public const int NotificationLockedIcon = Resource.Drawable.ic_notify_loaded;
public const int NotificationUnlockedIcon = Resource.Drawable.ic_notify_locked;
}
#endif
/// <summary>
/// Main implementation of the IKp2aApp interface for usage in the real app.
/// </summary>
public class Kp2aApp: IKp2aApp, ICacheSupervisor
{
public void LockDatabase(bool allowQuickUnlock = true)
{
if (GetDb().Loaded)
{
if (QuickUnlockEnabled && allowQuickUnlock &&
_db.KpDatabase.MasterKey.ContainsType(typeof(KcpPassword)) &&
!((KcpPassword)App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey(typeof(KcpPassword))).Password.IsEmpty)
{
if (!QuickLocked)
{
Kp2aLog.Log("QuickLocking database");
BroadcastDatabaseAction(Application.Context, Strings.ActionLockDatabase);
QuickLocked = true;
_db.LastOpenedEntry = null;
}
else
{
Kp2aLog.Log("Database already QuickLocked");
}
}
else
{
Kp2aLog.Log("Locking database");
BroadcastDatabaseAction(Application.Context, Strings.ActionCloseDatabase);
// Couldn't quick-lock, so unload database instead
_db.Clear();
QuickLocked = false;
}
}
else
{
Kp2aLog.Log("Database not loaded, couldn't lock");
}
UpdateOngoingNotification();
Application.Context.SendBroadcast(new Intent(Intents.DatabaseLocked));
}
public void BroadcastDatabaseAction(Context ctx, string action)
{
Intent i = new Intent(action);
//seems like this can happen. This code is for debugging.
if (App.Kp2a.GetDb().Ioc == null)
{
Kp2aLog.LogUnexpectedError(new Exception("App.Kp2a.GetDb().Ioc is null"));
return;
}
i.PutExtra(Strings.ExtraDatabaseFileDisplayname, App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc).GetDisplayName(App.Kp2a.GetDb().Ioc));
i.PutExtra(Strings.ExtraDatabaseFilepath, App.Kp2a.GetDb().Ioc.Path);
foreach (var plugin in new PluginDatabase(ctx).GetPluginsWithAcceptedScope(Strings.ScopeDatabaseActions))
{
i.SetPackage(plugin);
ctx.SendBroadcast(i);
}
}
public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compositeKey, ProgressDialogStatusLogger statusLogger, IDatabaseFormat databaseFormat)
{
_db.LoadData(this, ioConnectionInfo, memoryStream, compositeKey, statusLogger, databaseFormat);
UpdateOngoingNotification();
}
internal void UnlockDatabase()
{
QuickLocked = false;
UpdateOngoingNotification();
BroadcastDatabaseAction(Application.Context, Strings.ActionUnlockDatabase);
}
public void UpdateOngoingNotification()
{
// Start or update the notification icon service to reflect the current state
var ctx = Application.Context;
StartOnGoingService(ctx);
}
public static void StartOnGoingService(Context ctx)
{
ctx.StartService(new Intent(ctx, typeof (OngoingNotificationsService)));
}
public bool DatabaseIsUnlocked
{
get { return _db.Loaded && !QuickLocked; }
}
#region QuickUnlock
public void SetQuickUnlockEnabled(bool enabled)
{
if (enabled)
{
//Set KeyLength of QuickUnlock at time of enabling.
//This is important to not allow an attacker to set the length to 1 when QuickUnlock is started already.
var ctx = Application.Context;
var prefs = PreferenceManager.GetDefaultSharedPreferences(ctx);
QuickUnlockKeyLength = Math.Max(1, int.Parse(prefs.GetString(ctx.GetString(Resource.String.QuickUnlockLength_key), ctx.GetString(Resource.String.QuickUnlockLength_default))));
}
QuickUnlockEnabled = enabled;
}
public bool QuickUnlockEnabled { get; private set; }
public int QuickUnlockKeyLength { get; private set; }
/// <summary>
/// If true, the database must be regarded as locked and not exposed to the user.
/// </summary>
public bool QuickLocked { get; private set; }
#endregion
private Database _db;
/// <summary>
/// See comments to EntryEditActivityState.
/// </summary>
internal EntryEditActivityState EntryEditActivityState = null;
public FileDbHelper FileDbHelper;
private List<IFileStorage> _fileStorages;
public Database GetDb()
{
if (_db == null)
{
_db = CreateNewDatabase();
}
return _db;
}
public bool GetBooleanPreference(PreferenceKey key)
{
Context ctx = Application.Context;
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(ctx);
switch (key)
{
case PreferenceKey.remember_keyfile:
return prefs.GetBoolean(ctx.Resources.GetString(Resource.String.keyfile_key), ctx.Resources.GetBoolean(Resource.Boolean.keyfile_default));
case PreferenceKey.UseFileTransactions:
return prefs.GetBoolean(ctx.Resources.GetString(Resource.String.UseFileTransactions_key), true);
case PreferenceKey.CheckForFileChangesOnSave:
return prefs.GetBoolean(ctx.Resources.GetString(Resource.String.CheckForFileChangesOnSave_key), true);
default:
throw new Exception("unexpected key!");
}
}
public void CheckForOpenFileChanged(Activity activity)
{
if (_db.DidOpenFileChange())
{
if (_db.ReloadRequested)
{
LockDatabase(false);
activity.SetResult(KeePass.ExitReloadDb);
activity.Finish();
//todo: return?
}
AskForReload(activity);
}
}
private void AskForReload(Activity activity)
{
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.SetTitle(activity.GetString(Resource.String.AskReloadFile_title));
builder.SetMessage(activity.GetString(Resource.String.AskReloadFile));
builder.SetPositiveButton(activity.GetString(Android.Resource.String.Yes),
(dlgSender, dlgEvt) =>
{
_db.ReloadRequested = true;
activity.SetResult(KeePass.ExitReloadDb);
activity.Finish();
});
builder.SetNegativeButton(activity.GetString(Android.Resource.String.No), (dlgSender, dlgEvt) =>
{
});
Dialog dialog = builder.Create();
dialog.Show();
}
public void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile)
{
FileDbHelper.CreateFile(ioc, keyfile);
}
public string GetResourceString(UiStringKey key)
{
return GetResourceString(key.ToString());
}
public string GetResourceString(string key)
{
var field = typeof(Resource.String).GetField(key);
if (field == null)
throw new Exception("Invalid key " + key);
return Application.Context.GetString((int)field.GetValue(null));
}
public Drawable GetResourceDrawable(string key)
{
var field = typeof(Resource.Drawable).GetField(key);
if (field == null)
throw new Exception("Invalid key " + key);
return Application.Context.Resources.GetDrawable((int)field.GetValue(null));
}
public void AskYesNoCancel(UiStringKey titleKey, UiStringKey messageKey,
EventHandler<DialogClickEventArgs> yesHandler,
EventHandler<DialogClickEventArgs> noHandler,
EventHandler<DialogClickEventArgs> cancelHandler,
Context ctx)
{
AskYesNoCancel(titleKey, messageKey, UiStringKey.yes, UiStringKey.no,
yesHandler, noHandler, cancelHandler, ctx);
}
public void AskYesNoCancel(UiStringKey titleKey, UiStringKey messageKey,
UiStringKey yesString, UiStringKey noString,
EventHandler<DialogClickEventArgs> yesHandler,
EventHandler<DialogClickEventArgs> noHandler,
EventHandler<DialogClickEventArgs> cancelHandler,
Context ctx)
{
AskYesNoCancel(titleKey, messageKey, yesString, noString, yesHandler, noHandler, cancelHandler, null, ctx);
}
public void AskYesNoCancel(UiStringKey titleKey, UiStringKey messageKey,
UiStringKey yesString, UiStringKey noString,
EventHandler<DialogClickEventArgs> yesHandler,
EventHandler<DialogClickEventArgs> noHandler,
EventHandler<DialogClickEventArgs> cancelHandler,
EventHandler dismissHandler,
Context ctx)
{
Handler handler = new Handler(Looper.MainLooper);
handler.Post(() =>
{
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.SetTitle(GetResourceString(titleKey));
builder.SetMessage(GetResourceString(messageKey));
string yesText = GetResourceString(yesString);
builder.SetPositiveButton(yesText, yesHandler);
string noText = "";
if (noHandler != null)
{
noText = GetResourceString(noString);
builder.SetNegativeButton(noText, noHandler);
}
string cancelText = "";
if (cancelHandler != null)
{
cancelText = ctx.GetString(Android.Resource.String.Cancel);
builder.SetNeutralButton(cancelText,
cancelHandler);
}
AlertDialog dialog = builder.Create();
if (dismissHandler != null)
dialog.SetOnDismissListener(new Util.DismissListener(() => dismissHandler(dialog, EventArgs.Empty)));
dialog.Show();
if (yesText.Length + noText.Length + cancelText.Length >= 20)
{
try
{
Button button = dialog.GetButton((int)DialogButtonType.Positive);
LinearLayout linearLayout = (LinearLayout)button.Parent;
linearLayout.Orientation = Orientation.Vertical;
}
catch (Exception e)
{
Kp2aLog.LogUnexpectedError(e);
}
}
}
);
}
public Handler UiThreadHandler
{
get { return new Handler(); }
}
/// <summary>
/// Simple wrapper around ProgressDialog implementing IProgressDialog
/// </summary>
private class RealProgressDialog : IProgressDialog
{
private readonly ProgressDialog _pd;
public RealProgressDialog(Context ctx)
{
_pd = new ProgressDialog(ctx);
_pd.SetCancelable(false);
}
public void SetTitle(string title)
{
_pd.SetTitle(title);
}
public void SetMessage(string message)
{
_pd.SetMessage(message);
}
public void Dismiss()
{
try
{
_pd.Dismiss();
}
catch (Exception e)
{
Kp2aLog.LogUnexpectedError(e);
}
}
public void Show()
{
_pd.Show();
}
}
public IProgressDialog CreateProgressDialog(Context ctx)
{
return new RealProgressDialog(ctx);
}
public IFileStorage GetFileStorage(IOConnectionInfo iocInfo)
{
return GetFileStorage(iocInfo, true);
}
public IFileStorage GetFileStorage(IOConnectionInfo iocInfo, bool allowCache)
{
IFileStorage fileStorage;
if (iocInfo.IsLocalFile())
fileStorage = new LocalFileStorage(this);
else
{
IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo);
if (DatabaseCacheEnabled && allowCache)
{
fileStorage = new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this);
}
else
{
fileStorage = innerFileStorage;
}
}
if (fileStorage is IOfflineSwitchable)
{
((IOfflineSwitchable)fileStorage).IsOffline = App.Kp2a.OfflineMode;
}
return fileStorage;
}
private IFileStorage GetCloudFileStorage(IOConnectionInfo iocInfo)
{
foreach (IFileStorage fs in FileStorages)
{
foreach (string protocolId in fs.SupportedProtocols)
{
if (iocInfo.Path.StartsWith(protocolId + "://"))
return fs;
}
}
//TODO: catch!
throw new NoFileStorageFoundException("Unknown protocol " + iocInfo.Path);
}
public IEnumerable<IFileStorage> FileStorages
{
get
{
if (_fileStorages == null)
{
_fileStorages = new List<IFileStorage>
{
new AndroidContentStorage(Application.Context),
#if !EXCLUDE_JAVAFILESTORAGE
#if !NoNet
new DropboxFileStorage(Application.Context, this),
new DropboxAppFolderFileStorage(Application.Context, this),
new GoogleDriveFileStorage(Application.Context, this),
new SkyDriveFileStorage(Application.Context, this),
new SftpFileStorage(this),
new NetFtpFileStorage(Application.Context, this),
#endif
#endif
new LocalFileStorage(this)
};
}
return _fileStorages;
}
}
public void TriggerReload(Context ctx)
{
Handler handler = new Handler(Looper.MainLooper);
handler.Post(() =>
{
AskForReload((Activity) ctx);
});
}
public RemoteCertificateValidationCallback CertificateValidationCallback
{
get
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
ValidationMode validationMode = ValidationMode.Warn;
string strValMode = prefs.GetString(Application.Context.Resources.GetString(Resource.String.AcceptAllServerCertificates_key),
Application.Context.Resources.GetString(Resource.String.AcceptAllServerCertificates_default));
if (strValMode == "IGNORE")
validationMode = ValidationMode.Ignore;
else if (strValMode == "ERROR")
validationMode = ValidationMode.Error;
;
switch (validationMode)
{
case ValidationMode.Ignore:
return (sender, certificate, chain, errors) => true;
case ValidationMode.Warn:
return (sender, certificate, chain, errors) =>
{
if (errors != SslPolicyErrors.None)
ShowToast(Application.Context.GetString(Resource.String.CertificateWarning,
new Java.Lang.Object[]
{
errors.ToString()
}));
return true;
};
case ValidationMode.Error:
return (sender, certificate, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
return true;
return false;
};;
default:
throw new ArgumentOutOfRangeException();
}
}
}
public bool CheckForDuplicateUuids
{
get
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
return prefs.GetBoolean(Application.Context.GetString(Resource.String.CheckForDuplicateUuids_key), true);
}
}
public enum ValidationMode
{
Ignore, Warn, Error
}
internal void OnTerminate()
{
if (_db != null)
{
_db.Clear();
}
if (FileDbHelper != null && FileDbHelper.IsOpen())
{
FileDbHelper.Close();
}
}
internal void OnCreate(Application app)
{
FileDbHelper = new FileDbHelper(app);
FileDbHelper.Open();
#if DEBUG
foreach (UiStringKey key in Enum.GetValues(typeof(UiStringKey)))
{
GetResourceString(key);
}
#endif
#if !EXCLUDE_TWOFISH
CipherPool.GlobalPool.AddCipher(new TwofishCipherEngine());
#endif
}
public Database CreateNewDatabase()
{
_db = new Database(new DrawableFactory(), this);
return _db;
}
internal void ShowToast(string message)
{
var handler = new Handler(Looper.MainLooper);
handler.Post(() => { var toast = Toast.MakeText(Application.Context, message, ToastLength.Long);
toast.SetGravity(GravityFlags.Center, 0, 0);
toast.Show();
});
}
public void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e)
{
var errorMessage = GetErrorMessageForFileStorageException(e);
ShowToast(Application.Context.GetString(Resource.String.CouldNotSaveToRemote, errorMessage));
}
private string GetErrorMessageForFileStorageException(Exception e)
{
string errorMessage = e.Message;
if (e is OfflineModeException)
errorMessage = GetResourceString(UiStringKey.InOfflineMode);
return errorMessage;
}
public void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex)
{
var errorMessage = GetErrorMessageForFileStorageException(ex);
ShowToast(Application.Context.GetString(Resource.String.CouldNotLoadFromRemote, errorMessage));
}
public void UpdatedCachedFileOnLoad(IOConnectionInfo ioc)
{
ShowToast(Application.Context.GetString(Resource.String.UpdatedCachedFileOnLoad,
new Java.Lang.Object[] { Application.Context.GetString(Resource.String.database_file) }));
}
public void UpdatedRemoteFileOnLoad(IOConnectionInfo ioc)
{
ShowToast(Application.Context.GetString(Resource.String.UpdatedRemoteFileOnLoad));
}
public void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc)
{
ShowToast(Application.Context.GetString(Resource.String.NotifyOpenFromLocalDueToConflict));
}
public void LoadedFromRemoteInSync(IOConnectionInfo ioc)
{
ShowToast(Application.Context.GetString(Resource.String.LoadedFromRemoteInSync));
}
public void ClearOfflineCache()
{
new CachingFileStorage(new LocalFileStorage(this), Application.Context.CacheDir.Path, this).ClearCache();
}
public IFileStorage GetFileStorage(string protocolId)
{
return GetFileStorage(new IOConnectionInfo() {Path = protocolId + "://"});
}
/// <summary>
/// returns a file storage object to be used when accessing the auxiliary OTP file
/// </summary>
/// The reason why this requires a different file storage is the different caching behavior.
public IFileStorage GetOtpAuxFileStorage(IOConnectionInfo iocInfo)
{
if (iocInfo.IsLocalFile())
return new LocalFileStorage(this);
else
{
IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo);
if (DatabaseCacheEnabled)
{
return new OtpAuxCachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, new OtpAuxCacheSupervisor(this));
}
else
{
return innerFileStorage;
}
}
}
private static bool DatabaseCacheEnabled
{
get
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
bool cacheEnabled = prefs.GetBoolean(Application.Context.Resources.GetString(Resource.String.UseOfflineCache_key),
#if NoNet
false
#else
true
#endif
);
return cacheEnabled;
}
}
public bool OfflineModePreference
{
get
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
return prefs.GetBoolean(Application.Context.GetString(Resource.String.OfflineMode_key), false);
}
set
{
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
ISharedPreferencesEditor edit = prefs.Edit();
edit.PutBoolean(Application.Context.GetString(Resource.String.OfflineMode_key), value);
edit.Commit();
}
}
/// <summary>
/// true if the app is used in offline mode
/// </summary>
public bool OfflineMode
{
get; set;
}
public void OnScreenOff()
{
if (PreferenceManager.GetDefaultSharedPreferences(Application.Context)
.GetBoolean(
Application.Context.GetString(Resource.String.LockWhenScreenOff_key),
false))
{
App.Kp2a.LockDatabase();
}
}
}
///Application class for Keepass2Android: Contains static Database variable to be used by all components.
#if NoNet
[Application(Debuggable=false, Label=AppNames.AppName)]
#else
#if RELEASE
[Application(Debuggable=false, Label=AppNames.AppName)]
#else
[Application(Debuggable = true, Label = AppNames.AppName)]
#endif
#endif
public class App : Application {
public const string PrefErrorreportmode = "pref_ErrorReportMode";
public const string PrefHaspendingerrorreport = "pref_hasPendingErrorReport";
public App (IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
public static readonly Kp2aApp Kp2a = new Kp2aApp();
public enum ErrorReportMode
{
AskAgain=0,
Disabled=1,
Enabled=2
}
public override void OnCreate() {
base.OnCreate();
Kp2aLog.Log("Creating application "+PackageName+". Version=" + PackageManager.GetPackageInfo(PackageName, 0).VersionCode);
Kp2a.OnCreate(this);
AndroidEnvironment.UnhandledExceptionRaiser += MyApp_UnhandledExceptionHandler;
#if !NoNet
Kp2aLog.OnUnexpectedError += (sender, exception) =>
{
var currentErrorReportMode = GetErrorReportMode(ApplicationContext);
if (currentErrorReportMode != ErrorReportMode.Disabled)
{
Xamarin.Insights.Report(exception);
if (Xamarin.Insights.DisableDataTransmission)
{
PreferenceManager.GetDefaultSharedPreferences(ApplicationContext)
.Edit().PutBoolean(PrefHaspendingerrorreport, true).Commit();
}
}
};
Xamarin.Insights.Initialize("fed2b273ed2a964d0ba6acc3743e68f7a04da957", ApplicationContext);
Xamarin.Insights.DisableExceptionCatching = true;
var errorReportMode = GetErrorReportMode(ApplicationContext);
SetErrorReportMode(ApplicationContext, errorReportMode);
#endif
}
public static ErrorReportMode GetErrorReportMode(Context ctx)
{
ErrorReportMode errorReportMode;
Enum.TryParse(PreferenceManager.GetDefaultSharedPreferences(ctx)
.GetString(PrefErrorreportmode, ErrorReportMode.AskAgain.ToString()), out errorReportMode);
return errorReportMode;
}
public override void OnTerminate() {
base.OnTerminate();
Kp2aLog.Log("Terminating application");
Kp2a.OnTerminate();
}
#if !NoNet
void MyApp_UnhandledExceptionHandler(object sender, RaiseThrowableEventArgs e)
{
Kp2aLog.LogUnexpectedError(e.Exception);
Xamarin.Insights.Save();
// Do your error handling here.
//throw e.Exception;
}
protected override void Dispose(bool disposing)
{
AndroidEnvironment.UnhandledExceptionRaiser -= MyApp_UnhandledExceptionHandler;
base.Dispose(disposing);
}
public static void SetErrorReportMode(Context ctx, ErrorReportMode mode)
{
Xamarin.Insights.DisableCollection = (mode == ErrorReportMode.Disabled);
Xamarin.Insights.DisableDataTransmission = mode != ErrorReportMode.Enabled;
var pref = PreferenceManager.GetDefaultSharedPreferences(ctx);
var edit = pref.Edit();
if (mode != ErrorReportMode.AskAgain)
{
edit.PutBoolean(PrefHaspendingerrorreport, false);
}
edit.PutString(PrefErrorreportmode, mode.ToString());
edit.Commit();
}
#else
private void MyApp_UnhandledExceptionHandler(object sender, RaiseThrowableEventArgs e)
{
Kp2aLog.LogUnexpectedError(e.Exception);
}
#endif
}
}