rename back to current names
36
src/keepass2android-app/AboutActivity.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name",
|
||||
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden,
|
||||
Theme = "@style/Kp2aTheme_ActionBar",
|
||||
Exported = true)]
|
||||
[IntentFilter(new[] { "kp2a.action.AboutActivity" }, Categories = new[] { Intent.CategoryDefault })]
|
||||
public class AboutActivity: Activity, IDialogInterfaceOnDismissListener
|
||||
{
|
||||
private AboutDialog _dialog;
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
if ((_dialog == null) || (_dialog.IsShowing == false))
|
||||
{
|
||||
_dialog = new AboutDialog(this);
|
||||
_dialog.SetOnDismissListener(this);
|
||||
_dialog.Show();
|
||||
}
|
||||
base.OnResume();
|
||||
}
|
||||
|
||||
public void OnDismiss(IDialogInterface dialog)
|
||||
{
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
}
|
126
src/keepass2android-app/AboutDialog.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
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 Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Text.Method;
|
||||
using Android.Widget;
|
||||
using Android.Content.PM;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
|
||||
public class AboutDialog : Dialog {
|
||||
|
||||
public AboutDialog(Context context):base (context) {
|
||||
}
|
||||
public AboutDialog(Context context, int theme)
|
||||
: base(context, theme)
|
||||
{
|
||||
}
|
||||
|
||||
public AboutDialog(IntPtr javaRef, JniHandleOwnership transfer) : base(javaRef, transfer)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState) {
|
||||
base.OnCreate(savedInstanceState);
|
||||
SetContentView(Resource.Layout.about);
|
||||
SetTitle(Resource.String.app_name);
|
||||
|
||||
SetVersion();
|
||||
SetContributors();
|
||||
|
||||
FindViewById(Resource.Id.suggest).Click += delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
Util.GotoUrl(Context, Resource.String.SuggestionsURL);
|
||||
}
|
||||
catch (ActivityNotFoundException)
|
||||
{
|
||||
Toast.MakeText(Context, Resource.String.no_url_handler, ToastLength.Long).Show();
|
||||
}
|
||||
|
||||
};
|
||||
FindViewById(Resource.Id.rate).Click += delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
Util.GotoMarket(Context);
|
||||
}
|
||||
catch (ActivityNotFoundException)
|
||||
{
|
||||
Toast.MakeText(Context, Resource.String.no_url_handler, ToastLength.Long).Show();
|
||||
}
|
||||
};
|
||||
FindViewById(Resource.Id.translate).Click += delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
Util.GotoUrl(Context, Resource.String.TranslationURL);
|
||||
}
|
||||
catch (ActivityNotFoundException)
|
||||
{
|
||||
Toast.MakeText(Context, Resource.String.no_url_handler, ToastLength.Long).Show();
|
||||
}
|
||||
}; FindViewById(Resource.Id.donate).Click += delegate
|
||||
{
|
||||
Util.GotoDonateUrl(Context);
|
||||
};
|
||||
}
|
||||
|
||||
private void SetContributors()
|
||||
{
|
||||
TextView tv = (TextView)FindViewById(Resource.Id.further_authors);
|
||||
tv.Text = Context.GetString(Resource.String.further_authors, new Java.Lang.Object[] { Context.GetString(Resource.String.further_author_names) });
|
||||
|
||||
TextView tvdesigners = (TextView)FindViewById(Resource.Id.designers);
|
||||
tvdesigners.Text = Context.GetString(Resource.String.designers, new Java.Lang.Object[] { Context.GetString(Resource.String.designer_names) });
|
||||
|
||||
TextView tvsupporters = (TextView)FindViewById(Resource.Id.supporters);
|
||||
tvsupporters.Text = Context.GetString(Resource.String.supporters, new Java.Lang.Object[] { Context.GetString(Resource.String.supporter_names) });
|
||||
}
|
||||
|
||||
private void SetVersion() {
|
||||
Context ctx = Context;
|
||||
|
||||
String version;
|
||||
try {
|
||||
PackageInfo packageInfo = ctx.PackageManager.GetPackageInfo(ctx.PackageName, 0);
|
||||
version = packageInfo.VersionName;
|
||||
|
||||
} catch (PackageManager.NameNotFoundException) {
|
||||
version = "";
|
||||
}
|
||||
|
||||
TextView tv = (TextView) FindViewById(Resource.Id.versionX);
|
||||
tv.Text = version;
|
||||
|
||||
FindViewById(Resource.Id.versionB).Click += (sender, args) => ChangeLog.ShowChangeLog(ctx, () => { });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
50
src/keepass2android-app/ActivityLaunchMode.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public abstract class ActivityLaunchMode
|
||||
{
|
||||
public abstract void Launch(Activity act, Intent i);
|
||||
}
|
||||
|
||||
public class ActivityLaunchModeForward : ActivityLaunchMode
|
||||
{
|
||||
public override void Launch(Activity act, Intent i)
|
||||
{
|
||||
i.AddFlags(ActivityFlags.ForwardResult);
|
||||
act.StartActivity(i);
|
||||
}
|
||||
}
|
||||
|
||||
public class ActivityLaunchModeRequestCode : ActivityLaunchMode
|
||||
{
|
||||
private readonly int _reqCode;
|
||||
|
||||
public ActivityLaunchModeRequestCode(int reqCode)
|
||||
{
|
||||
_reqCode = reqCode;
|
||||
}
|
||||
public override void Launch(Activity act, Intent i)
|
||||
{
|
||||
act.StartActivityForResult(i, _reqCode);
|
||||
}
|
||||
}
|
||||
|
||||
public class ActivityLaunchModeSimple : ActivityLaunchMode
|
||||
{
|
||||
public override void Launch(Activity act, Intent i)
|
||||
{
|
||||
act.StartActivity(i);
|
||||
}
|
||||
}
|
||||
}
|
50
src/keepass2android-app/AppKilledInfo.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Google.Android.Material.Dialog;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = AppNames.AppName)]
|
||||
public class AppKilledInfo : Activity, IDialogInterfaceOnDismissListener
|
||||
{
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
base.OnCreate(bundle);
|
||||
//as we expect this to happen only rarely (having a foreground service running when unlocked),
|
||||
//we don't try to handle this better
|
||||
//But at least explain to the user what happened!
|
||||
((NotificationManager)GetSystemService(Context.NotificationService)).CancelAll();
|
||||
|
||||
MaterialAlertDialogBuilder b = new MaterialAlertDialogBuilder(this);
|
||||
b.SetMessage(Resource.String.killed_by_os);
|
||||
b.SetPositiveButton(Android.Resource.String.Ok, delegate
|
||||
{
|
||||
Intent i = new Intent(this, typeof(SelectCurrentDbActivity));
|
||||
i.AddFlags(ActivityFlags.ClearTask | ActivityFlags.NewTask);
|
||||
StartActivity(i);
|
||||
|
||||
});
|
||||
b.SetNegativeButton(Resource.String.cancel, delegate { });
|
||||
b.SetTitle(GetString(AppNames.AppNameResource));
|
||||
|
||||
var dialog = b.Create();
|
||||
dialog.SetOnDismissListener(this);
|
||||
dialog.Show();
|
||||
}
|
||||
|
||||
public void OnDismiss(IDialogInterface dialog)
|
||||
{
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
}
|
21
src/keepass2android-app/AssemblyInfo.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll.
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
93
src/keepass2android-app/Assets/LICENSE_SourceCodePro.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
src/keepass2android-app/Assets/SourceCodePro-Regular.ttf
Normal file
BIN
src/keepass2android-app/Assets/fontawesome-webfont.ttf
Normal file
12742
src/keepass2android-app/Assets/publicsuffix.txt
Normal file
94
src/keepass2android-app/AttachmentContentProvider.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Util;
|
||||
using Java.IO;
|
||||
using File = Java.IO.File;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes attachments of PwEntries accessible when they are stored in the app cache
|
||||
/// </summary>
|
||||
[ContentProvider(new[]{"keepass2android."+AppNames.PackagePart+".provider"},Exported = true)]
|
||||
public class AttachmentContentProvider : ContentProvider {
|
||||
public const string AttachmentCacheSubDir = "AttachmentCache";
|
||||
|
||||
private const String ClassName = "AttachmentContentProvider";
|
||||
|
||||
// The authority is the symbolic name for the provider class
|
||||
public const String Authority = "keepass2android."+AppNames.PackagePart+".provider";
|
||||
|
||||
|
||||
public override bool OnCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public override ParcelFileDescriptor OpenFile(Android.Net.Uri uri, String mode)
|
||||
{
|
||||
|
||||
const string logTag = ClassName + " - openFile";
|
||||
|
||||
Log.Verbose(logTag,
|
||||
"Called with uri: '" + uri + "'." + uri.LastPathSegment);
|
||||
|
||||
if (uri.ToString().StartsWith("content://" + Authority))
|
||||
{
|
||||
// The desired file name is specified by the last segment of the
|
||||
// path
|
||||
// E.g.
|
||||
// 'content://keepass2android.provider/Test.txt'
|
||||
// Take this and build the path to the file
|
||||
|
||||
//Protect against path traversal with an uri like content://keepass2android.keepass2android.provider/..%2F..%2Fshared_prefs%2FKP2A.Plugin.keepass2android.plugin.qr.xml
|
||||
if (uri.LastPathSegment.Contains("/"))
|
||||
throw new Exception("invalid path ");
|
||||
|
||||
String fileLocation = Context.CacheDir + File.Separator + AttachmentCacheSubDir + File.Separator
|
||||
+ uri.LastPathSegment;
|
||||
|
||||
// Create & return a ParcelFileDescriptor pointing to the file
|
||||
// Note: I don't care what mode they ask for - they're only getting
|
||||
// read only
|
||||
ParcelFileDescriptor pfd = ParcelFileDescriptor.Open(new File(
|
||||
fileLocation), ParcelFileMode.ReadOnly);
|
||||
return pfd;
|
||||
|
||||
}
|
||||
Log.Verbose(logTag, "Unsupported uri: '" + uri + "'.");
|
||||
throw new Java.IO.FileNotFoundException("Unsupported uri: "
|
||||
+ uri.ToString());
|
||||
}
|
||||
|
||||
// //////////////////////////////////////////////////////////////
|
||||
// Not supported / used / required for this example
|
||||
// //////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
public override int Update(Android.Net.Uri uri, ContentValues contentvalues, String s,
|
||||
String[] strings) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override int Delete(Android.Net.Uri uri, String s, String[] strings) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
public override Android.Net.Uri Insert(Android.Net.Uri uri, ContentValues contentvalues) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public override String GetType(Android.Net.Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public override Android.Database.ICursor Query(Android.Net.Uri uri, string[] projection, string selection, string[] selectionArgs, string sortOrder)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
169
src/keepass2android-app/AutoOpenEdit.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Android.App;
|
||||
using keepass2android;
|
||||
using KeePassLib;
|
||||
using KeePassLib.Security;
|
||||
using KeePassLib.Serialization;
|
||||
using Object = Java.Lang.Object;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Edit mode implementation for AutoOpen entries
|
||||
/// </summary>
|
||||
public class AutoOpenEdit : EditModeBase
|
||||
{
|
||||
private const string strVisible = "Visible";
|
||||
private const string strEnabled = "Enabled";
|
||||
private const string strUiKeyFile = "_ui_KeyFile";
|
||||
private const string strUiDatabaseFile = "_ui_DatabaseFile";
|
||||
private const string strUiIfDevice = "_ui_IfDevice_";
|
||||
|
||||
public AutoOpenEdit(PwEntry entry)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override bool IsVisible(string fieldKey)
|
||||
{
|
||||
if (fieldKey == PwDefs.TitleField
|
||||
|| fieldKey == PwDefs.PasswordField
|
||||
|| fieldKey == strVisible
|
||||
|| fieldKey == strEnabled
|
||||
|| fieldKey.StartsWith("_ui_"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override IEnumerable<string> SortExtraFieldKeys(IEnumerable<string> keys)
|
||||
{
|
||||
return keys.OrderBy(s =>
|
||||
{
|
||||
if (s == strUiDatabaseFile) return 1;
|
||||
if (s == strEnabled) return 2;
|
||||
|
||||
if (s == strUiKeyFile) return 10000;
|
||||
if (s == strVisible) return 10001;
|
||||
return 10;
|
||||
|
||||
}).ThenBy(s => s);
|
||||
|
||||
}
|
||||
|
||||
public override bool ShowAddAttachments
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override bool ShowAddExtras
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override string GetTitle(string key)
|
||||
{
|
||||
if (key == strVisible)
|
||||
return LocaleManager.LocalizedAppContext.GetString(Resource.String.Visible_title);
|
||||
if (key == strEnabled)
|
||||
return LocaleManager.LocalizedAppContext.GetString(Resource.String.child_db_Enabled_title);
|
||||
if (key == strUiKeyFile)
|
||||
return LocaleManager.LocalizedAppContext.GetString(Resource.String.keyfile_heading);
|
||||
if (key == strUiDatabaseFile)
|
||||
return LocaleManager.LocalizedAppContext.GetString(Resource.String.database_file_heading);
|
||||
if (key.StartsWith(strUiIfDevice))
|
||||
{
|
||||
return LocaleManager.LocalizedAppContext.GetString(Resource.String.if_device_text,new Object[]{key.Substring(strUiIfDevice.Length)});
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
public override string GetFieldType(string key)
|
||||
{
|
||||
if ((key == strEnabled)
|
||||
|| key == strVisible
|
||||
|| key.StartsWith(strUiIfDevice))
|
||||
return "bool";
|
||||
|
||||
if ((key == strUiDatabaseFile)
|
||||
|| (key == strUiKeyFile))
|
||||
return "file";
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public override void InitializeEntry(PwEntry entry)
|
||||
{
|
||||
base.InitializeEntry(entry);
|
||||
if (!entry.Strings.Exists(strVisible))
|
||||
{
|
||||
entry.Strings.Set(strVisible, new ProtectedString(false, "true"));
|
||||
}
|
||||
if (!entry.Strings.Exists(strEnabled))
|
||||
{
|
||||
entry.Strings.Set(strEnabled, new ProtectedString(false, "true"));
|
||||
}
|
||||
var autoExecItem = KeeAutoExecExt.MakeAutoExecItem(App.Kp2a.CurrentDb.KpDatabase, entry, 0);
|
||||
IOConnectionInfo ioc;
|
||||
if (!KeeAutoExecExt.TryGetDatabaseIoc(autoExecItem, out ioc))
|
||||
ioc = IOConnectionInfo.FromPath(entry.Strings.ReadSafe(PwDefs.UrlField));
|
||||
string path = ioc.Path;
|
||||
try
|
||||
{
|
||||
var filestorage = App.Kp2a.GetFileStorage(ioc);
|
||||
if (filestorage != null)
|
||||
{
|
||||
path = filestorage.IocToPath(ioc);
|
||||
}
|
||||
}
|
||||
catch (NoFileStorageFoundException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
entry.Strings.Set(strUiDatabaseFile, new ProtectedString(false, path));
|
||||
entry.Strings.Set(strUiKeyFile,new ProtectedString(false,entry.Strings.ReadSafe(PwDefs.UserNameField)));
|
||||
|
||||
var devices =
|
||||
KeeAutoExecExt.GetIfDevice(KeeAutoExecExt.MakeAutoExecItem(App.Kp2a.CurrentDb.KpDatabase, entry, 0));
|
||||
//make sure user can enable/disable on this device explicitly:
|
||||
if (!devices.ContainsKey(KeeAutoExecExt.ThisDeviceId))
|
||||
devices[KeeAutoExecExt.ThisDeviceId] = false;
|
||||
foreach (var ifDevice in devices)
|
||||
{
|
||||
entry.Strings.Set(strUiIfDevice + ifDevice.Key, new ProtectedString(false, ifDevice.Value.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
public override void PrepareForSaving(PwEntry entry)
|
||||
{
|
||||
entry.Strings.Set(PwDefs.UrlField, new ProtectedString(false, entry.Strings.ReadSafe(strUiDatabaseFile)));
|
||||
entry.Strings.Set(PwDefs.UserNameField, new ProtectedString(false, entry.Strings.ReadSafe(strUiKeyFile)));
|
||||
entry.Strings.Remove(strUiDatabaseFile);
|
||||
entry.Strings.Remove(strUiKeyFile);
|
||||
|
||||
Dictionary<string, bool> devices = new Dictionary<string, bool>();
|
||||
foreach (string key in entry.Strings.GetKeys())
|
||||
{
|
||||
if (key.StartsWith(strUiIfDevice))
|
||||
{
|
||||
string device = key.Substring(strUiIfDevice.Length);
|
||||
devices[device] = entry.Strings.ReadSafe(key).Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
entry.Strings.Set(KeeAutoExecExt._ifDevice,new ProtectedString(false,KeeAutoExecExt.BuildIfDevice(devices)));
|
||||
foreach (string device in devices.Keys)
|
||||
{
|
||||
entry.Strings.Remove(strUiIfDevice + device);
|
||||
}
|
||||
|
||||
base.PrepareForSaving(entry);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
493
src/keepass2android-app/BiometricModule.cs
Normal file
@@ -0,0 +1,493 @@
|
||||
using System;
|
||||
using Android.Content;
|
||||
using Javax.Crypto;
|
||||
using Java.Security;
|
||||
using Java.Lang;
|
||||
using Android.Views.InputMethods;
|
||||
using Android.App;
|
||||
using Android.OS;
|
||||
using Android.Security.Keystore;
|
||||
using Android.Preferences;
|
||||
using Android.Util;
|
||||
using Android.Widget;
|
||||
using AndroidX.Biometric;
|
||||
using AndroidX.Fragment.App;
|
||||
using Java.IO;
|
||||
using Java.Security.Cert;
|
||||
using Java.Util.Concurrent;
|
||||
using Javax.Crypto.Spec;
|
||||
using keepass2android;
|
||||
using Exception = System.Exception;
|
||||
using File = System.IO.File;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public interface IBiometricAuthCallback
|
||||
{
|
||||
void OnBiometricAuthSucceeded();
|
||||
void OnBiometricError(string toString);
|
||||
void OnBiometricAttemptFailed(string message);
|
||||
}
|
||||
|
||||
public class BiometricModule
|
||||
{
|
||||
public AndroidX.Fragment.App.FragmentActivity Activity { get; set; }
|
||||
|
||||
public BiometricModule(AndroidX.Fragment.App.FragmentActivity activity)
|
||||
{
|
||||
Activity = activity;
|
||||
}
|
||||
|
||||
|
||||
public KeyguardManager KeyguardManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return (KeyguardManager)Activity.GetSystemService("keyguard");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public KeyStore Keystore
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return KeyStore.GetInstance("AndroidKeyStore");
|
||||
}
|
||||
catch (KeyStoreException e)
|
||||
{
|
||||
throw new RuntimeException("Failed to get an instance of KeyStore", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public KeyGenerator KeyGenerator
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, "AndroidKeyStore");
|
||||
}
|
||||
catch (NoSuchAlgorithmException e)
|
||||
{
|
||||
throw new RuntimeException("Failed to get an instance of KeyGenerator", e);
|
||||
}
|
||||
catch (NoSuchProviderException e)
|
||||
{
|
||||
throw new RuntimeException("Failed to get an instance of KeyGenerator", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Cipher Cipher
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return Cipher.GetInstance(KeyProperties.KeyAlgorithmAes + "/"
|
||||
+ KeyProperties.BlockModeCbc + "/"
|
||||
+ KeyProperties.EncryptionPaddingPkcs7);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e)
|
||||
{
|
||||
throw new RuntimeException("Failed to get an instance of Cipher", e);
|
||||
}
|
||||
catch (NoSuchPaddingException e)
|
||||
{
|
||||
throw new RuntimeException("Failed to get an instance of Cipher", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ISharedPreferences SharedPreferences
|
||||
{
|
||||
get { return PreferenceManager.GetDefaultSharedPreferences(Activity); }
|
||||
}
|
||||
|
||||
public bool IsAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
return BiometricManager.From(Activity).CanAuthenticate() ==
|
||||
BiometricManager.BiometricSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsHardwareAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = BiometricManager.From(Activity).CanAuthenticate();
|
||||
Kp2aLog.Log("BiometricHardware available = " + result);
|
||||
return result == BiometricManager.BiometricSuccess
|
||||
|| result == BiometricManager.BiometricErrorNoneEnrolled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BiometricCrypt : IBiometricIdentifier
|
||||
{
|
||||
protected const string FailedToInitCipher = "Failed to init Cipher";
|
||||
|
||||
protected readonly string _keyId;
|
||||
|
||||
protected Cipher _cipher;
|
||||
private CancellationSignal _cancellationSignal;
|
||||
protected BiometricPrompt.CryptoObject _cryptoObject;
|
||||
|
||||
protected KeyStore _keystore;
|
||||
|
||||
private BiometricPrompt _biometricPrompt;
|
||||
private FragmentActivity _activity;
|
||||
private BiometricAuthCallbackAdapter _biometricAuthCallbackAdapter;
|
||||
|
||||
public BiometricCrypt(BiometricModule biometric, string keyId)
|
||||
{
|
||||
Kp2aLog.Log("FP: Create " + this.GetType().Name);
|
||||
_keyId = keyId;
|
||||
_cipher = biometric.Cipher;
|
||||
_keystore = biometric.Keystore;
|
||||
_activity = biometric.Activity;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public abstract bool Init();
|
||||
|
||||
|
||||
protected static string GetAlias(string keyId)
|
||||
{
|
||||
return "keepass2android." + keyId;
|
||||
}
|
||||
protected static string GetIvPrefKey(string prefKey)
|
||||
{
|
||||
return prefKey + "_iv";
|
||||
}
|
||||
|
||||
public void StartListening(IBiometricAuthCallback callback)
|
||||
{
|
||||
_biometricAuthCallbackAdapter = new BiometricAuthCallbackAdapter(callback, _activity);
|
||||
StartListening(_biometricAuthCallbackAdapter);
|
||||
}
|
||||
|
||||
public void StopListening()
|
||||
{
|
||||
Kp2aLog.Log("Fingerprint: StopListening " + (_biometricPrompt != null ? " having prompt " : " without prompt"));
|
||||
_biometricAuthCallbackAdapter?.IgnoreNextError();
|
||||
_biometricPrompt?.CancelAuthentication();
|
||||
}
|
||||
|
||||
public bool HasUserInterface
|
||||
{
|
||||
get { return true; }
|
||||
|
||||
}
|
||||
|
||||
public void StartListening(BiometricPrompt.AuthenticationCallback callback)
|
||||
{
|
||||
|
||||
Kp2aLog.Log("Fingerprint: StartListening ");
|
||||
|
||||
var executor = Executors.NewSingleThreadExecutor();
|
||||
_biometricPrompt = new BiometricPrompt(_activity, executor, callback);
|
||||
|
||||
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
|
||||
.SetTitle(_activity.GetString(AppNames.AppNameResource))
|
||||
.SetSubtitle(_activity.GetString(Resource.String.unlock_database_title))
|
||||
.SetNegativeButtonText(_activity.GetString(Android.Resource.String.Cancel))
|
||||
.SetConfirmationRequired(false)
|
||||
.Build();
|
||||
|
||||
|
||||
_biometricPrompt.Authenticate(promptInfo, _cryptoObject);
|
||||
|
||||
}
|
||||
|
||||
public string Encrypt(string textToEncrypt)
|
||||
{
|
||||
Kp2aLog.Log("FP: Encrypting");
|
||||
return Base64.EncodeToString(_cipher.DoFinal(System.Text.Encoding.UTF8.GetBytes(textToEncrypt)), 0);
|
||||
}
|
||||
|
||||
|
||||
public void StoreEncrypted(string textToEncrypt, string prefKey, Context context)
|
||||
{
|
||||
var edit = PreferenceManager.GetDefaultSharedPreferences(context).Edit();
|
||||
StoreEncrypted(textToEncrypt, prefKey, edit);
|
||||
edit.Commit();
|
||||
}
|
||||
|
||||
public void StoreEncrypted(string textToEncrypt, string prefKey, ISharedPreferencesEditor edit)
|
||||
{
|
||||
edit.PutString(prefKey, Encrypt(textToEncrypt));
|
||||
edit.PutString(GetIvPrefKey(prefKey), Base64.EncodeToString(CipherIv, 0));
|
||||
|
||||
}
|
||||
|
||||
|
||||
private byte[] CipherIv
|
||||
{
|
||||
get { return _cipher.GetIV(); }
|
||||
}
|
||||
}
|
||||
|
||||
public interface IBiometricIdentifier
|
||||
{
|
||||
bool Init();
|
||||
void StartListening(IBiometricAuthCallback callback);
|
||||
|
||||
void StopListening();
|
||||
bool HasUserInterface { get; }
|
||||
}
|
||||
|
||||
public class BiometricDecryption : BiometricCrypt
|
||||
{
|
||||
private readonly Context _context;
|
||||
private readonly byte[] _iv;
|
||||
|
||||
|
||||
public BiometricDecryption(BiometricModule biometric, string keyId, byte[] iv) : base(biometric, keyId)
|
||||
{
|
||||
_iv = iv;
|
||||
}
|
||||
|
||||
public BiometricDecryption(BiometricModule biometric, string keyId, Context context, string prefKey)
|
||||
: base(biometric, keyId)
|
||||
{
|
||||
_context = context;
|
||||
_iv = Base64.Decode(PreferenceManager.GetDefaultSharedPreferences(context).GetString(GetIvPrefKey(prefKey), null), 0);
|
||||
}
|
||||
|
||||
public static bool IsSetUp(Context context, string prefKey)
|
||||
{
|
||||
return PreferenceManager.GetDefaultSharedPreferences(context).GetString(GetIvPrefKey(prefKey), null) != null;
|
||||
}
|
||||
|
||||
public override bool Init()
|
||||
{
|
||||
Kp2aLog.Log("FP: Init for Dec");
|
||||
try
|
||||
{
|
||||
_keystore.Load(null);
|
||||
var aliases = _keystore.Aliases();
|
||||
if (aliases == null)
|
||||
{
|
||||
Kp2aLog.Log("KS: no aliases");
|
||||
}
|
||||
else
|
||||
{
|
||||
while (aliases.HasMoreElements)
|
||||
{
|
||||
var o = aliases.NextElement();
|
||||
Kp2aLog.Log("alias: " + o?.ToString());
|
||||
}
|
||||
Kp2aLog.Log("KS: end aliases");
|
||||
|
||||
}
|
||||
var key = _keystore.GetKey(GetAlias(_keyId), null);
|
||||
if (key == null)
|
||||
throw new Exception("Failed to init cipher for fingerprint Init: key is null");
|
||||
var ivParams = new IvParameterSpec(_iv);
|
||||
_cipher.Init(CipherMode.DecryptMode, key, ivParams);
|
||||
|
||||
_cryptoObject = new BiometricPrompt.CryptoObject(_cipher);
|
||||
return true;
|
||||
}
|
||||
catch (KeyPermanentlyInvalidatedException e)
|
||||
{
|
||||
Kp2aLog.Log("FP: KeyPermanentlyInvalidatedException." + e.ToString());
|
||||
return false;
|
||||
}
|
||||
catch (KeyStoreException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher + " (keystore)", e);
|
||||
}
|
||||
catch (CertificateException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher + " (CertificateException)", e);
|
||||
}
|
||||
catch (UnrecoverableKeyException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher + " (UnrecoverableKeyException)", e);
|
||||
}
|
||||
catch (Java.IO.IOException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher + " (IOException)", e);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher + " (NoSuchAlgorithmException)", e);
|
||||
}
|
||||
catch (InvalidKeyException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher + " (InvalidKeyException)" + e.ToString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string Decrypt(string encryted)
|
||||
{
|
||||
Kp2aLog.Log("FP: Decrypting ");
|
||||
byte[] encryptedBytes = Base64.Decode(encryted, 0);
|
||||
return System.Text.Encoding.UTF8.GetString(_cipher.DoFinal(encryptedBytes));
|
||||
}
|
||||
|
||||
public string DecryptStored(string prefKey)
|
||||
{
|
||||
string enc = PreferenceManager.GetDefaultSharedPreferences(_context).GetString(prefKey, null);
|
||||
return Decrypt(enc);
|
||||
}
|
||||
}
|
||||
|
||||
public class BiometricEncryption : BiometricCrypt
|
||||
{
|
||||
|
||||
private KeyGenerator _keyGen;
|
||||
|
||||
|
||||
public BiometricEncryption(BiometricModule biometric, string keyId) :
|
||||
base(biometric, keyId)
|
||||
{
|
||||
_keyGen = biometric.KeyGenerator;
|
||||
Kp2aLog.Log("FP: CreateKey ");
|
||||
CreateKey();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a symmetric key in the Android Key Store which can only be used after the user
|
||||
/// has authenticated with biometry.
|
||||
/// </summary>
|
||||
private void CreateKey()
|
||||
{
|
||||
try
|
||||
{
|
||||
_keystore.Load(null);
|
||||
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(GetAlias(_keyId),
|
||||
KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
|
||||
.SetBlockModes(KeyProperties.BlockModeCbc)
|
||||
// Require the user to authenticate with biometry to authorize every use
|
||||
// of the key
|
||||
.SetEncryptionPaddings(KeyProperties.EncryptionPaddingPkcs7)
|
||||
.SetUserAuthenticationRequired(true);
|
||||
|
||||
if ((int)Build.VERSION.SdkInt >= 24)
|
||||
builder.SetInvalidatedByBiometricEnrollment(true);
|
||||
|
||||
_keyGen.Init(
|
||||
builder
|
||||
.Build());
|
||||
_keyGen.GenerateKey();
|
||||
}
|
||||
catch (NoSuchAlgorithmException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
catch (InvalidAlgorithmParameterException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
catch (CertificateException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
catch (Java.IO.IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Init()
|
||||
{
|
||||
Kp2aLog.Log("FP: Init for Enc ");
|
||||
try
|
||||
{
|
||||
_keystore.Load(null);
|
||||
var key = _keystore.GetKey(GetAlias(_keyId), null);
|
||||
_cipher.Init(CipherMode.EncryptMode, key);
|
||||
|
||||
_cryptoObject = new BiometricPrompt.CryptoObject(_cipher);
|
||||
return true;
|
||||
}
|
||||
catch (KeyPermanentlyInvalidatedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (KeyStoreException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher, e);
|
||||
}
|
||||
catch (CertificateException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher, e);
|
||||
}
|
||||
catch (UnrecoverableKeyException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher, e);
|
||||
}
|
||||
catch (Java.IO.IOException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher, e);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher, e);
|
||||
}
|
||||
catch (InvalidKeyException e)
|
||||
{
|
||||
throw new RuntimeException(FailedToInitCipher, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class BiometricAuthCallbackAdapter : BiometricPrompt.AuthenticationCallback
|
||||
{
|
||||
private readonly IBiometricAuthCallback _callback;
|
||||
private readonly Context _context;
|
||||
private bool _ignoreNextError;
|
||||
|
||||
public BiometricAuthCallbackAdapter(IBiometricAuthCallback callback, Context context)
|
||||
{
|
||||
_callback = callback;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
|
||||
public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result)
|
||||
{
|
||||
new Handler(Looper.MainLooper).Post(() => _callback.OnBiometricAuthSucceeded());
|
||||
}
|
||||
|
||||
public override void OnAuthenticationError(int errorCode, ICharSequence errString)
|
||||
{
|
||||
if (_ignoreNextError)
|
||||
{
|
||||
_ignoreNextError = false;
|
||||
return;
|
||||
}
|
||||
new Handler(Looper.MainLooper).Post(() => _callback.OnBiometricError(errString.ToString()));
|
||||
}
|
||||
|
||||
|
||||
public override void OnAuthenticationFailed()
|
||||
{
|
||||
new Handler(Looper.MainLooper).Post(() => _callback.OnBiometricAttemptFailed(_context.Resources.GetString(Resource.String.fingerprint_not_recognized)));
|
||||
}
|
||||
|
||||
public void IgnoreNextError()
|
||||
{
|
||||
_ignoreNextError = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
42
src/keepass2android-app/CancelDialog.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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 Android.App;
|
||||
using Android.Content;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
|
||||
public class CancelDialog : Dialog {
|
||||
protected readonly Activity _activity;
|
||||
|
||||
public CancelDialog(Activity activity): base(activity)
|
||||
{
|
||||
_activity = activity;
|
||||
}
|
||||
|
||||
public bool Canceled { get; private set; }
|
||||
|
||||
|
||||
public override void Cancel() {
|
||||
base.Cancel();
|
||||
Canceled = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
217
src/keepass2android-app/ChallengeInfo.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
//
|
||||
// ChallengeInfo.cs
|
||||
//
|
||||
// Author:
|
||||
// Ben.Rush <>
|
||||
//
|
||||
// Copyright (c) 2014 Ben.Rush
|
||||
//
|
||||
// This program 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 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program 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 this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
using System;
|
||||
using System.Xml;
|
||||
using System.IO;
|
||||
using keepass2android;
|
||||
using KeePassLib.Serialization;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace KeeChallenge
|
||||
{
|
||||
public class ChallengeInfo
|
||||
{
|
||||
private bool m_LT64;
|
||||
|
||||
public byte[] EncryptedSecret {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public byte[] IV {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public byte[] Challenge {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public byte[] Verification {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool LT64
|
||||
{
|
||||
get { return m_LT64; }
|
||||
private set { m_LT64 = value; }
|
||||
}
|
||||
|
||||
private ChallengeInfo()
|
||||
{
|
||||
LT64 = false;
|
||||
}
|
||||
|
||||
public ChallengeInfo(byte[] encryptedSecret, byte[] iv, byte[] challenge, byte[] verification, bool lt64)
|
||||
{
|
||||
EncryptedSecret = encryptedSecret;
|
||||
IV = iv;
|
||||
Challenge = challenge;
|
||||
Verification = verification;
|
||||
LT64 = lt64;
|
||||
}
|
||||
|
||||
public static ChallengeInfo Load(IOConnectionInfo ioc)
|
||||
{
|
||||
Stream sIn = null;
|
||||
ChallengeInfo inf = new ChallengeInfo();
|
||||
try
|
||||
{
|
||||
sIn = App.Kp2a.GetOtpAuxFileStorage(ioc).OpenFileForRead(ioc);
|
||||
|
||||
if (!inf.LoadStream(sIn)) return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if(sIn != null) sIn.Close();
|
||||
}
|
||||
|
||||
return inf;
|
||||
}
|
||||
|
||||
private bool LoadStream(Stream AuxFile)
|
||||
{
|
||||
//read file
|
||||
XmlReader xml;
|
||||
try
|
||||
{
|
||||
XmlReaderSettings settings = new XmlReaderSettings();
|
||||
settings.CloseInput = true;
|
||||
settings.XmlResolver = null;
|
||||
xml = XmlReader.Create(AuxFile,settings);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (xml.Read())
|
||||
{
|
||||
if (xml.IsStartElement())
|
||||
{
|
||||
switch (xml.Name)
|
||||
{
|
||||
case "encrypted":
|
||||
xml.Read();
|
||||
EncryptedSecret = Convert.FromBase64String(xml.Value.Trim());
|
||||
break;
|
||||
case "iv":
|
||||
xml.Read();
|
||||
IV = Convert.FromBase64String(xml.Value.Trim());
|
||||
break;
|
||||
case "challenge":
|
||||
xml.Read();
|
||||
Challenge = Convert.FromBase64String(xml.Value.Trim());
|
||||
break;
|
||||
case "verification":
|
||||
xml.Read();
|
||||
Verification = Convert.FromBase64String(xml.Value.Trim());
|
||||
break;
|
||||
case "lt64":
|
||||
xml.Read();
|
||||
if (!bool.TryParse(xml.Value.Trim(), out m_LT64)) throw new Exception("Unable to parse LT64 flag");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
xml.Close();
|
||||
//if failed, return false
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Save(IOConnectionInfo ioc)
|
||||
{
|
||||
Stream sOut = null;
|
||||
try
|
||||
{
|
||||
using (var trans = App.Kp2a.GetOtpAuxFileStorage(ioc)
|
||||
.OpenWriteTransaction(ioc, App.Kp2a.GetBooleanPreference(PreferenceKey.UseFileTransactions)))
|
||||
{
|
||||
sOut = trans.OpenFile();
|
||||
if (SaveStream(sOut))
|
||||
{
|
||||
trans.CommitWrite();
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch(Exception) { return false; }
|
||||
finally
|
||||
{
|
||||
if (sOut != null) sOut.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private bool SaveStream(Stream file)
|
||||
{
|
||||
try
|
||||
{
|
||||
XmlWriterSettings settings = new XmlWriterSettings();
|
||||
settings.CloseOutput = true;
|
||||
settings.Indent = true;
|
||||
settings.IndentChars = "\t";
|
||||
settings.NewLineOnAttributes = true;
|
||||
|
||||
XmlWriter xml = XmlWriter.Create(file,settings);
|
||||
xml.WriteStartDocument();
|
||||
xml.WriteStartElement("data");
|
||||
|
||||
xml.WriteStartElement("aes");
|
||||
xml.WriteElementString("encrypted", Convert.ToBase64String(EncryptedSecret));
|
||||
xml.WriteElementString("iv", Convert.ToBase64String(IV));
|
||||
xml.WriteEndElement();
|
||||
|
||||
xml.WriteElementString("challenge", Convert.ToBase64String(Challenge));
|
||||
xml.WriteElementString("verification", Convert.ToBase64String(Verification));
|
||||
xml.WriteElementString("lt64", LT64.ToString());
|
||||
|
||||
xml.WriteEndElement();
|
||||
xml.WriteEndDocument();
|
||||
xml.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
96
src/keepass2android-app/ChallengeXCKey.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Java.Lang;
|
||||
using keepass2android;
|
||||
using KeePassLib.Cryptography;
|
||||
using KeePassLib.Keys;
|
||||
using KeePassLib.Security;
|
||||
using KeePassLib.Serialization;
|
||||
using Exception = System.Exception;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public class ChallengeXCKey : IUserKey, ISeedBasedUserKey
|
||||
{
|
||||
private readonly int _requestCode;
|
||||
|
||||
public ProtectedBinary KeyData
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Activity == null)
|
||||
throw new Exception("Need an active Keepass2Android activity to challenge Yubikey!");
|
||||
Activity.RunOnUiThread(
|
||||
() =>
|
||||
{
|
||||
byte[] challenge = _kdfSeed;
|
||||
byte[] challenge64 = new byte[64];
|
||||
for (int i = 0; i < 64; i++)
|
||||
{
|
||||
if (i < challenge.Length)
|
||||
{
|
||||
challenge64[i] = challenge[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
challenge64[i] = (byte)(challenge64.Length - challenge.Length);
|
||||
}
|
||||
|
||||
}
|
||||
var chalIntent = Activity.TryGetYubichallengeIntentOrPrompt(challenge64, true);
|
||||
|
||||
if (chalIntent == null)
|
||||
{
|
||||
Error = Activity.GetString(Resource.String.NoChallengeApp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Activity.StartActivityForResult(chalIntent, _requestCode);
|
||||
}
|
||||
|
||||
});
|
||||
while ((Response == null) && (Error == null))
|
||||
{
|
||||
System.Threading.Thread.Sleep(50);
|
||||
}
|
||||
if (Error != null)
|
||||
{
|
||||
var error = Error;
|
||||
Error = null;
|
||||
throw new Exception("YubiChallenge failed: " + error);
|
||||
}
|
||||
|
||||
var result = CryptoUtil.HashSha256(Response);
|
||||
Response = null;
|
||||
return new ProtectedBinary(true, result);
|
||||
}
|
||||
}
|
||||
|
||||
public uint GetMinKdbxVersion()
|
||||
{
|
||||
return KdbxFile.FileVersion32_4;
|
||||
}
|
||||
|
||||
private byte[] _kdfSeed;
|
||||
|
||||
public ChallengeXCKey(LockingActivity activity, int requestCode)
|
||||
{
|
||||
this.Activity = activity;
|
||||
_requestCode = requestCode;
|
||||
Response = null;
|
||||
}
|
||||
|
||||
public void SetParams(byte[] masterSeed, byte[] mPbKdfSeed)
|
||||
{
|
||||
_kdfSeed = mPbKdfSeed;
|
||||
}
|
||||
|
||||
public byte[] Response { get; set; }
|
||||
|
||||
public string Error { get; set; }
|
||||
|
||||
public LockingActivity Activity
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
265
src/keepass2android-app/ChangeLog.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Content.Res;
|
||||
using Android.Graphics;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Runtime;
|
||||
using Android.Text;
|
||||
using Android.Text.Method;
|
||||
using Android.Text.Util;
|
||||
using Android.Views;
|
||||
using Android.Webkit;
|
||||
using Android.Widget;
|
||||
using Google.Android.Material.Dialog;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public static class ChangeLog
|
||||
{
|
||||
public static void ShowChangeLog(Context ctx, Action onDismiss)
|
||||
{
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ctx);
|
||||
builder.SetTitle(ctx.GetString(Resource.String.ChangeLog_title));
|
||||
List<string> changeLog = new List<string>{
|
||||
BuildChangelogString(ctx, new List<int>{Resource.Array.ChangeLog_1_11,Resource.Array.ChangeLog_1_11_net}, "1.11"),
|
||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_10, "1.10"),
|
||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09e, "1.09e"),
|
||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09d, "1.09d"),
|
||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09c, "1.09c"),
|
||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09b, "1.09b"),
|
||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_09a, "1.09a"),
|
||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_08d, "1.08d"),
|
||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_08c, "1.08c"),
|
||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_08b, "1.08b"),
|
||||
BuildChangelogString(ctx, Resource.Array.ChangeLog_1_08, "1.08"),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_07b),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_07),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_06),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_05),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_04b),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_04),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_03),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_02),
|
||||
#if !NoNet
|
||||
ctx.GetString(Resource.String.ChangeLog_1_01g),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_01d),
|
||||
#endif
|
||||
ctx.GetString(Resource.String.ChangeLog_1_01),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_0_0e),
|
||||
ctx.GetString(Resource.String.ChangeLog_1_0_0),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_9c),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_9),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_8c),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_8b),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_8),
|
||||
#if !NoNet
|
||||
//0.9.7b fixes were already included in 0.9.7 offline
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_7b),
|
||||
#endif
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_7),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_6),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_5),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_4),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_3_r5),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_3),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_2),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9_1),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_9),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_8_6),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_8_5),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_8_4),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_8_3),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_8_2),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_8_1),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_8),
|
||||
ctx.GetString(Resource.String.ChangeLog_0_7),
|
||||
ctx.GetString(Resource.String.ChangeLog)
|
||||
};
|
||||
|
||||
String version;
|
||||
try {
|
||||
PackageInfo packageInfo = ctx.PackageManager.GetPackageInfo(ctx.PackageName, 0);
|
||||
version = packageInfo.VersionName;
|
||||
|
||||
} catch (PackageManager.NameNotFoundException) {
|
||||
version = "";
|
||||
}
|
||||
|
||||
string warning = "";
|
||||
if (version.Contains("pre"))
|
||||
{
|
||||
warning = ctx.GetString(Resource.String.PreviewWarning);
|
||||
}
|
||||
|
||||
builder.SetPositiveButton(Android.Resource.String.Ok, (dlgSender, dlgEvt) => {((AlertDialog)dlgSender).Dismiss(); });
|
||||
builder.SetCancelable(false);
|
||||
|
||||
WebView wv = new WebView(ctx);
|
||||
|
||||
wv.SetBackgroundColor(Color.White);
|
||||
wv.LoadDataWithBaseURL(null, GetLog(changeLog, warning, ctx), "text/html", "UTF-8", null);
|
||||
|
||||
|
||||
//builder.SetMessage("");
|
||||
builder.SetView(wv);
|
||||
Dialog dialog = builder.Create();
|
||||
dialog.DismissEvent += (sender, e) =>
|
||||
{
|
||||
onDismiss();
|
||||
};
|
||||
dialog.Show();
|
||||
/*TextView message = (TextView)dialog.FindViewById(Android.Resource.Id.Message);
|
||||
|
||||
|
||||
message.TextFormatted = Html.FromHtml(ConcatChangeLog(ctx, changeLog.ToArray()));
|
||||
message.AutoLinkMask=MatchOptions.WebUrls;*/
|
||||
|
||||
}
|
||||
|
||||
private static string BuildChangelogString(Context ctx, int changeLogResId, string version)
|
||||
{
|
||||
return BuildChangelogString(ctx, new List<int>() { changeLogResId }, version);
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static string BuildChangelogString(Context ctx, List<int> changeLogResIds, string version)
|
||||
{
|
||||
string result = "Version " + version + "\n";
|
||||
string previous = "";
|
||||
foreach (var changeLogResId in changeLogResIds)
|
||||
{
|
||||
foreach (var item in ctx.Resources.GetStringArray(changeLogResId))
|
||||
{
|
||||
if (item == previous) //there was some trouble with crowdin translations, remove duplicates
|
||||
continue;
|
||||
result += " * " + item + "\n";
|
||||
previous = item;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
private const string HtmlStart = @"<html>
|
||||
<head>
|
||||
<style type='text/css'>
|
||||
a { color:#000000 }
|
||||
div.title {
|
||||
color:287AA9;
|
||||
font-size:1.2em;
|
||||
font-weight:bold;
|
||||
margin-top:1em;
|
||||
margin-bottom:0.5em;
|
||||
text-align:center }
|
||||
div.subtitle {
|
||||
color:287AA9;
|
||||
font-size:0.8em;
|
||||
margin-bottom:1em;
|
||||
text-align:center }
|
||||
div.freetext { color:#000000 }
|
||||
div.list { color:#000000 }
|
||||
</style>
|
||||
</head>
|
||||
<body>";
|
||||
private const string HtmlEnd = @"</body>
|
||||
</html>";
|
||||
private static string GetLog(List<string> changeLog, string warning, Context ctx)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(HtmlStart);
|
||||
if (!string.IsNullOrEmpty(warning))
|
||||
{
|
||||
sb.Append(warning);
|
||||
}
|
||||
bool inList = false;
|
||||
bool isFirst = true;
|
||||
foreach (string versionLog in changeLog)
|
||||
{
|
||||
string versionLog2 = versionLog;
|
||||
bool title = true;
|
||||
if (isFirst)
|
||||
{
|
||||
|
||||
bool showDonateOption = true;
|
||||
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(ctx);
|
||||
if (prefs.GetBoolean(ctx.GetString(Resource.String.NoDonationReminder_key), false))
|
||||
showDonateOption = false;
|
||||
|
||||
long usageCount = prefs.GetLong(ctx.GetString(Resource.String.UsageCount_key), 0);
|
||||
|
||||
if (usageCount <= 5)
|
||||
showDonateOption = false;
|
||||
|
||||
if (showDonateOption)
|
||||
{
|
||||
if (versionLog2.EndsWith("\n") == false)
|
||||
versionLog2 += "\n";
|
||||
string donateUrl = ctx.GetString(Resource.String.donate_url,
|
||||
new Java.Lang.Object[]{ctx.Resources.Configuration.Locale.Language,
|
||||
ctx.PackageName
|
||||
});
|
||||
|
||||
versionLog2 += " * <a href=\"" + donateUrl
|
||||
+ "\">" +
|
||||
ctx.GetString(Resource.String.ChangeLog_keptDonate)
|
||||
+ "<a/>";
|
||||
}
|
||||
isFirst = false;
|
||||
}
|
||||
foreach (string line in versionLog2.Split('\n'))
|
||||
{
|
||||
string w = line.Trim();
|
||||
if (title)
|
||||
{
|
||||
if (inList)
|
||||
{
|
||||
sb.Append("</ul></div>\n");
|
||||
inList = false;
|
||||
}
|
||||
w = w.Replace("<b>","");
|
||||
w = w.Replace("</b>", "");
|
||||
w = w.Replace("\\n", "");
|
||||
sb.Append("<div class='title'>"
|
||||
+ w.Trim() + "</div>\n");
|
||||
title = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
w = w.Replace("\\n", "<br />");
|
||||
if ((w.StartsWith("*") || (w.StartsWith("•"))))
|
||||
{
|
||||
if (!inList)
|
||||
{
|
||||
sb.Append("<div class='list'><ul>\n");
|
||||
inList = true;
|
||||
}
|
||||
sb.Append("<li>");
|
||||
sb.Append(w.Substring(1).Trim());
|
||||
sb.Append("</li>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (inList)
|
||||
{
|
||||
sb.Append("</ul></div>\n");
|
||||
inList = false;
|
||||
}
|
||||
sb.Append(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.Append(HtmlEnd);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
32
src/keepass2android-app/CloseImmediatelyActivity.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = AppNames.AppName, Theme = "@style/Kp2aTheme_ActionBar", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden)]
|
||||
public class CloseImmediatelyActivity : AndroidX.AppCompat.App.AppCompatActivity
|
||||
{
|
||||
protected override void OnResume()
|
||||
{
|
||||
SetContentView(Resource.Layout.group);
|
||||
base.OnResume();
|
||||
SetResult(Result.Ok);
|
||||
FindViewById<RelativeLayout>(Resource.Id.bottom_bar).PostDelayed(() =>
|
||||
{
|
||||
Finish();
|
||||
OverridePendingTransition(0, 0);
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
}
|
436
src/keepass2android-app/ConfigureChildDatabasesActivity.cs
Normal file
@@ -0,0 +1,436 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Graphics;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Android.Runtime;
|
||||
using Android.Text;
|
||||
using Android.Util;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Google.Android.Material.Dialog;
|
||||
using keepass2android.database.edit;
|
||||
using KeePass.Util.Spr;
|
||||
using keepass2android;
|
||||
using KeePassLib;
|
||||
using KeePassLib.Keys;
|
||||
using KeePassLib.Security;
|
||||
using KeePassLib.Serialization;
|
||||
using AlertDialog = Android.App.AlertDialog;
|
||||
using Object = Java.Lang.Object;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/child_dbs_title", MainLauncher = false, Theme = "@style/Kp2aTheme_BlueNoActionBar", LaunchMode = LaunchMode.SingleInstance, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden, Exported = true)]
|
||||
[IntentFilter(new[] { "kp2a.action.ConfigureChildDatabasesActivity" }, Categories = new[] { Intent.CategoryDefault })]
|
||||
public class ConfigureChildDatabasesActivity : LockCloseActivity
|
||||
{
|
||||
private ChildDatabasesAdapter _adapter;
|
||||
|
||||
public class ChildDatabasesAdapter : BaseAdapter
|
||||
{
|
||||
|
||||
private readonly ConfigureChildDatabasesActivity _context;
|
||||
internal List<AutoExecItem> _displayedChildDatabases;
|
||||
|
||||
public ChildDatabasesAdapter(ConfigureChildDatabasesActivity context)
|
||||
{
|
||||
_context = context;
|
||||
Update();
|
||||
|
||||
}
|
||||
|
||||
public override Object GetItem(int position)
|
||||
{
|
||||
return position;
|
||||
}
|
||||
|
||||
public override long GetItemId(int position)
|
||||
{
|
||||
return position;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private LayoutInflater cursorInflater;
|
||||
|
||||
public override View GetView(int position, View convertView, ViewGroup parent)
|
||||
{
|
||||
if (cursorInflater == null)
|
||||
cursorInflater = (LayoutInflater)_context.GetSystemService(Context.LayoutInflaterService);
|
||||
|
||||
View view;
|
||||
|
||||
if (convertView == null)
|
||||
{
|
||||
// if it's not recycled, initialize some attributes
|
||||
|
||||
view = cursorInflater.Inflate(Resource.Layout.child_db_config_row, parent, false);
|
||||
|
||||
|
||||
view.FindViewById<Button>(Resource.Id.child_db_enable_on_this_device).Click += (sender, args) =>
|
||||
{
|
||||
View sending_view = (View) sender;
|
||||
_context.OnEnable(_displayedChildDatabases[GetClickedPos(sending_view)]);
|
||||
};
|
||||
|
||||
view.FindViewById<Button>(Resource.Id.child_db_disable_on_this_device).Click += (sender, args) =>
|
||||
{
|
||||
View sending_view = (View)sender;
|
||||
_context.OnDisable(_displayedChildDatabases[GetClickedPos(sending_view)]);
|
||||
};
|
||||
|
||||
|
||||
view.FindViewById<Button>(Resource.Id.child_db_edit).Click += (sender, args) =>
|
||||
{
|
||||
View sending_view = (View)sender;
|
||||
_context.OnEdit(_displayedChildDatabases[GetClickedPos(sending_view)]);
|
||||
};
|
||||
|
||||
|
||||
view.FindViewById<Button>(Resource.Id.child_db_open).Click += (sender, args) =>
|
||||
{
|
||||
View sending_view = (View)sender;
|
||||
_context.OnOpen(_displayedChildDatabases[GetClickedPos(sending_view)]);
|
||||
};
|
||||
|
||||
|
||||
view.FindViewById<Button>(Resource.Id.child_db_enable_a_copy_for_this_device).Click += (sender, args) =>
|
||||
{
|
||||
View sending_view = (View)sender;
|
||||
_context.OnEnableCopy(_displayedChildDatabases[GetClickedPos(sending_view)]);
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
view = convertView;
|
||||
}
|
||||
|
||||
|
||||
var iv = view.FindViewById<ImageView>(Resource.Id.child_db_icon);
|
||||
var autoExecItem = _displayedChildDatabases[position];
|
||||
var pw = autoExecItem.Entry;
|
||||
|
||||
SprContext ctx = new SprContext(pw, App.Kp2a.FindDatabaseForElement(pw).KpDatabase, SprCompileFlags.All);
|
||||
|
||||
string deviceId = KeeAutoExecExt.ThisDeviceId;
|
||||
|
||||
view.FindViewById<TextView>(Resource.Id.child_db_title).Text =
|
||||
SprEngine.Compile(pw.Strings.GetSafe(PwDefs.TitleField).ReadString(), ctx);
|
||||
|
||||
view.FindViewById<TextView>(Resource.Id.child_db_url).Text =
|
||||
_context.GetString(Resource.String.entry_url) + ": " + SprEngine.Compile(pw.Strings.GetSafe(PwDefs.UrlField).ReadString(),ctx);
|
||||
|
||||
bool deviceEnabledExplict;
|
||||
bool deviceEnabled = KeeAutoExecExt.IsDeviceEnabled(autoExecItem, deviceId, out deviceEnabledExplict);
|
||||
deviceEnabled &= deviceEnabledExplict;
|
||||
|
||||
|
||||
if (!autoExecItem.Enabled)
|
||||
{
|
||||
view.FindViewById<TextView>(Resource.Id.child_db_enabled_here).Text =
|
||||
_context.GetString(Resource.String.plugin_disabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
view.FindViewById<TextView>(Resource.Id.child_db_enabled_here).Text =
|
||||
_context.GetString(Resource.String.child_db_enabled_on_this_device) + ": " +
|
||||
(!deviceEnabledExplict ?
|
||||
_context.GetString(Resource.String.unspecified)
|
||||
:
|
||||
((autoExecItem.Enabled && deviceEnabled)
|
||||
? _context.GetString(Resource.String.yes)
|
||||
: _context.GetString(Resource.String.no)));
|
||||
}
|
||||
|
||||
view.FindViewById(Resource.Id.child_db_enable_on_this_device).Visibility = !deviceEnabled && autoExecItem.Enabled ? ViewStates.Visible : ViewStates.Gone;
|
||||
view.FindViewById(Resource.Id.child_db_disable_on_this_device).Visibility = (deviceEnabled || !deviceEnabledExplict) && autoExecItem.Enabled ? ViewStates.Visible : ViewStates.Gone;
|
||||
view.FindViewById(Resource.Id.child_db_enable_a_copy_for_this_device_container).Visibility = !deviceEnabled && autoExecItem.Enabled ? ViewStates.Visible : ViewStates.Gone;
|
||||
view.FindViewById(Resource.Id.child_db_edit).Visibility = deviceEnabledExplict || !autoExecItem.Enabled ? ViewStates.Visible : ViewStates.Gone;
|
||||
IOConnectionInfo ioc;
|
||||
if ((KeeAutoExecExt.TryGetDatabaseIoc(autoExecItem, out ioc)) && App.Kp2a.TryGetDatabase(ioc) == null)
|
||||
{
|
||||
view.FindViewById(Resource.Id.child_db_open).Visibility = ViewStates.Visible;
|
||||
}
|
||||
else view.FindViewById(Resource.Id.child_db_open).Visibility = ViewStates.Gone;
|
||||
|
||||
|
||||
Database db = App.Kp2a.FindDatabaseForElement(pw);
|
||||
|
||||
bool isExpired = pw.Expires && pw.ExpiryTime < DateTime.Now;
|
||||
if (isExpired)
|
||||
{
|
||||
db.DrawableFactory.AssignDrawableTo(iv, _context, db.KpDatabase, PwIcon.Expired, PwUuid.Zero, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
db.DrawableFactory.AssignDrawableTo(iv, _context, db.KpDatabase, pw.IconId, pw.CustomIconUuid, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
view.Tag = position.ToString();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private static int GetClickedPos(View sending_view)
|
||||
{
|
||||
View viewWithTag = sending_view;
|
||||
while (viewWithTag.Tag == null)
|
||||
viewWithTag = (View) viewWithTag.Parent;
|
||||
int clicked_pos = int.Parse((string) viewWithTag.Tag);
|
||||
return clicked_pos;
|
||||
}
|
||||
|
||||
public override int Count
|
||||
{
|
||||
get { return _displayedChildDatabases.Count; }
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
|
||||
_displayedChildDatabases = KeeAutoExecExt.GetAutoExecItems(App.Kp2a.CurrentDb.KpDatabase)
|
||||
.Where(e => App.Kp2a.TryFindDatabaseForElement(e.Entry) != null) //Update() can be called while we're adding entries to the database. They may be part of the group but without saving complete
|
||||
.OrderBy(e => SprEngine.Compile(e.Entry.Strings.ReadSafe(PwDefs.TitleField),new SprContext(e.Entry, App.Kp2a.FindDatabaseForElement(e.Entry).KpDatabase, SprCompileFlags.All)))
|
||||
.ThenByDescending(e => e.Entry.LastModificationTime)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpen(AutoExecItem item)
|
||||
{
|
||||
KeeAutoExecExt.AutoOpenEntry(this, item, true, new ActivityLaunchModeSimple());
|
||||
}
|
||||
|
||||
private void OnEnableCopy(AutoExecItem item)
|
||||
{
|
||||
//disable this device for the cloned entry
|
||||
KeeAutoExecExt.SetDeviceEnabled(item, KeeAutoExecExt.ThisDeviceId, false);
|
||||
//remember the original device settings
|
||||
ProtectedString ifDeviceOrig = item.Entry.Strings.GetSafe(KeeAutoExecExt._ifDevice);
|
||||
//reset device settings so only the current device is enabled
|
||||
item.Entry.Strings.Set(KeeAutoExecExt._ifDevice,new ProtectedString(false,""));
|
||||
KeeAutoExecExt.SetDeviceEnabled(item, KeeAutoExecExt.ThisDeviceId, true);
|
||||
//now clone
|
||||
var newEntry = item.Entry.CloneDeep();
|
||||
//reset device settings
|
||||
item.Entry.Strings.Set(KeeAutoExecExt._ifDevice, ifDeviceOrig);
|
||||
newEntry.SetUuid(new PwUuid(true), true); // Create new UUID
|
||||
string strTitle = newEntry.Strings.ReadSafe(PwDefs.TitleField);
|
||||
newEntry.Strings.Set(PwDefs.TitleField, new ProtectedString(false, strTitle + " (" + Android.OS.Build.Model + ")"));
|
||||
var addTask = new AddEntry(this, App.Kp2a.CurrentDb, App.Kp2a, newEntry,item.Entry.ParentGroup,new ActionOnFinish(this, (success, message, activity) => ((ConfigureChildDatabasesActivity)activity).Update()));
|
||||
|
||||
ProgressTask pt = new ProgressTask(App.Kp2a, this, addTask);
|
||||
pt.Run();
|
||||
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
_adapter.Update();
|
||||
_adapter.NotifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void OnEdit(AutoExecItem item)
|
||||
{
|
||||
EntryEditActivity.Launch(this,item.Entry,new NullTask());
|
||||
}
|
||||
|
||||
private void OnDisable(AutoExecItem item)
|
||||
{
|
||||
KeeAutoExecExt.SetDeviceEnabled(item,KeeAutoExecExt.ThisDeviceId, false);
|
||||
Save(item);
|
||||
}
|
||||
|
||||
private void OnEnable(AutoExecItem item)
|
||||
{
|
||||
KeeAutoExecExt.SetDeviceEnabled(item, KeeAutoExecExt.ThisDeviceId, true);
|
||||
Save(item);
|
||||
}
|
||||
|
||||
private void Save(AutoExecItem item)
|
||||
{
|
||||
var addTask = new SaveDb(this, App.Kp2a, App.Kp2a.FindDatabaseForElement(item.Entry), new ActionOnFinish(this, (success, message, activity) => ((ConfigureChildDatabasesActivity)activity).Update()));
|
||||
|
||||
ProgressTask pt = new ProgressTask(App.Kp2a, this, addTask);
|
||||
pt.Run();
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
_adapter.Update();
|
||||
_adapter.NotifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
SetContentView(Resource.Layout.config_child_db);
|
||||
|
||||
_adapter = new ChildDatabasesAdapter(this);
|
||||
var listView = FindViewById<ListView>(Android.Resource.Id.List);
|
||||
listView.Adapter = _adapter;
|
||||
|
||||
SetSupportActionBar(FindViewById<AndroidX.AppCompat.Widget.Toolbar>(Resource.Id.mytoolbar));
|
||||
|
||||
FindViewById<Button>(Resource.Id.add_child_db_button).Click += (sender, args) =>
|
||||
{
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
|
||||
builder.SetTitle(Resource.String.add_child_db);
|
||||
|
||||
List<string> items = new List<string>();
|
||||
Dictionary<int, Database> indexToDb = new Dictionary<int, Database>();
|
||||
|
||||
foreach (var db in App.Kp2a.OpenDatabases)
|
||||
{
|
||||
if (db != App.Kp2a.CurrentDb)
|
||||
{
|
||||
indexToDb[items.Count] = db;
|
||||
items.Add(App.Kp2a.GetFileStorage(db.Ioc).GetDisplayName(db.Ioc));
|
||||
}
|
||||
}
|
||||
indexToDb[items.Count] = null;
|
||||
items.Add(GetString(Resource.String.open_other_db));
|
||||
|
||||
builder.SetItems(items.ToArray(), (o, eventArgs) =>
|
||||
{
|
||||
Database db;
|
||||
if (!indexToDb.TryGetValue(eventArgs.Which, out db) || (db == null))
|
||||
{
|
||||
StartFileSelect();
|
||||
}
|
||||
else
|
||||
{
|
||||
AddAutoOpenEntryForDatabase(db);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
var dialog = builder.Create();
|
||||
dialog.Show();
|
||||
};
|
||||
}
|
||||
|
||||
private void AddAutoOpenEntryForDatabase(Database db)
|
||||
{
|
||||
PwGroup autoOpenGroup = null;
|
||||
var rootGroup = App.Kp2a.CurrentDb.KpDatabase.RootGroup;
|
||||
foreach (PwGroup pgSub in rootGroup.Groups)
|
||||
{
|
||||
if (pgSub.Name == "AutoOpen")
|
||||
{
|
||||
autoOpenGroup = pgSub;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
if (autoOpenGroup == null)
|
||||
{
|
||||
AddGroup addGroupTask = AddGroup.GetInstance(this, App.Kp2a, "AutoOpen", 1, null, rootGroup, null, true);
|
||||
addGroupTask.Run();
|
||||
autoOpenGroup = addGroupTask.Group;
|
||||
}
|
||||
|
||||
PwEntry newEntry = new PwEntry(true, true);
|
||||
newEntry.Strings.Set(PwDefs.TitleField, new ProtectedString(false, App.Kp2a.GetFileStorage(db.Ioc).GetDisplayName(db.Ioc)));
|
||||
newEntry.Strings.Set(PwDefs.UrlField, new ProtectedString(false, TryMakeRelativePath(App.Kp2a.CurrentDb, db.Ioc)));
|
||||
var password = db.KpDatabase.MasterKey.GetUserKey<KcpPassword>();
|
||||
newEntry.Strings.Set(PwDefs.PasswordField, password == null ? new ProtectedString(true, "") : password.Password);
|
||||
|
||||
var keyfile = db.KpDatabase.MasterKey.GetUserKey<KcpKeyFile>();
|
||||
if ((keyfile != null) && (keyfile.Ioc != null))
|
||||
{
|
||||
newEntry.Strings.Set(PwDefs.UserNameField, new ProtectedString(false, TryMakeRelativePath(App.Kp2a.CurrentDb, keyfile.Ioc)));
|
||||
}
|
||||
|
||||
newEntry.Strings.Set(KeeAutoExecExt._ifDevice,
|
||||
new ProtectedString(false,
|
||||
KeeAutoExecExt.BuildIfDevice(new Dictionary<string, bool>()
|
||||
{
|
||||
{KeeAutoExecExt.ThisDeviceId, true}
|
||||
})));
|
||||
|
||||
var addTask = new AddEntry(this, db, App.Kp2a, newEntry, autoOpenGroup, new ActionOnFinish(this, (success, message, activity) => (activity as ConfigureChildDatabasesActivity)?.Update()));
|
||||
|
||||
ProgressTask pt = new ProgressTask(App.Kp2a, this, addTask);
|
||||
pt.Run();
|
||||
}
|
||||
|
||||
private string TryMakeRelativePath(Database db, IOConnectionInfo ioc)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fsDb = App.Kp2a.GetFileStorage(db.Ioc);
|
||||
var dbParent = fsDb.GetParentPath(db.Ioc).Path + "/";
|
||||
if (ioc.Path.StartsWith(dbParent))
|
||||
{
|
||||
return "{DB_DIR}{ENV_DIRSEP}" + ioc.Path.Substring(dbParent.Length);
|
||||
}
|
||||
|
||||
}
|
||||
catch (NoFileStorageFoundException)
|
||||
{
|
||||
}
|
||||
return ioc.Path;
|
||||
|
||||
}
|
||||
|
||||
private void StartFileSelect(bool noForwardToPassword = false)
|
||||
{
|
||||
Intent intent = new Intent(this, typeof(FileSelectActivity));
|
||||
intent.PutExtra(FileSelectActivity.NoForwardToPasswordActivity, noForwardToPassword);
|
||||
intent.PutExtra("MakeCurrent", false);
|
||||
StartActivityForResult(intent, ReqCodeOpenNewDb);
|
||||
}
|
||||
|
||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
base.OnActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == ReqCodeOpenNewDb)
|
||||
{
|
||||
switch ((int)resultCode)
|
||||
{
|
||||
case (int)Result.Ok:
|
||||
|
||||
string iocString = data?.GetStringExtra("ioc");
|
||||
IOConnectionInfo ioc = IOConnectionInfo.UnserializeFromString(iocString);
|
||||
var db = App.Kp2a.TryGetDatabase(ioc);
|
||||
if (db != null)
|
||||
AddAutoOpenEntryForDatabase(db);
|
||||
|
||||
break;
|
||||
case PasswordActivity.ResultSelectOtherFile:
|
||||
StartFileSelect(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public const int ReqCodeOpenNewDb = 1;
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
468
src/keepass2android-app/CreateDatabaseActivity.cs
Normal file
@@ -0,0 +1,468 @@
|
||||
using System;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Text;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Java.IO;
|
||||
using KeePassLib.Keys;
|
||||
using KeePassLib.Serialization;
|
||||
using keepass2android.Io;
|
||||
using keepass2android;
|
||||
using Environment = Android.OS.Environment;
|
||||
using IOException = Java.IO.IOException;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name",
|
||||
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden,
|
||||
Theme = "@style/Kp2aTheme_BlueActionBar")]
|
||||
public class CreateDatabaseActivity : LifecycleAwareActivity
|
||||
{
|
||||
private IOConnectionInfo _ioc;
|
||||
private string _keyfileFilename;
|
||||
private bool _restoringInstanceState;
|
||||
private bool _showPassword;
|
||||
|
||||
private readonly ActivityDesign _design;
|
||||
private AppTask _appTask;
|
||||
private AppTask AppTask
|
||||
{
|
||||
get { return _appTask; }
|
||||
set
|
||||
{
|
||||
_appTask = value;
|
||||
Kp2aLog.LogTask(value, MyDebugName);
|
||||
}
|
||||
}
|
||||
|
||||
public CreateDatabaseActivity()
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
|
||||
private const int RequestCodeKeyFile = 0;
|
||||
private const int RequestCodeDbFilename = 1;
|
||||
private const string KeyfilefilenameBundleKey = "KeyfileFilename";
|
||||
|
||||
protected override void OnSaveInstanceState(Bundle outState)
|
||||
{
|
||||
base.OnSaveInstanceState(outState);
|
||||
outState.PutString(Util.KeyFilename, _ioc.Path);
|
||||
outState.PutString(Util.KeyServerusername, _ioc.UserName);
|
||||
outState.PutString(Util.KeyServerpassword, _ioc.Password);
|
||||
outState.PutInt(Util.KeyServercredmode, (int)_ioc.CredSaveMode);
|
||||
|
||||
if (_keyfileFilename != null)
|
||||
outState.PutString(KeyfilefilenameBundleKey, _keyfileFilename);
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
_design.ApplyTheme();
|
||||
base.OnCreate(bundle);
|
||||
|
||||
|
||||
SupportActionBar.SetDisplayHomeAsUpEnabled(true);
|
||||
SupportActionBar.SetHomeButtonEnabled(true);
|
||||
|
||||
SetContentView(Resource.Layout.create_database);
|
||||
AppTask = AppTask.GetTaskInOnCreate(bundle, Intent);
|
||||
|
||||
SetDefaultIoc();
|
||||
|
||||
FindViewById(Resource.Id.keyfile_filename).Visibility = ViewStates.Gone;
|
||||
|
||||
|
||||
var keyfileCheckbox = FindViewById<CheckBox>(Resource.Id.use_keyfile);
|
||||
|
||||
if (bundle != null)
|
||||
{
|
||||
_keyfileFilename = bundle.GetString(KeyfilefilenameBundleKey, null);
|
||||
if (_keyfileFilename != null)
|
||||
{
|
||||
FindViewById<TextView>(Resource.Id.keyfile_filename).Text = FileSelectHelper.ConvertFilenameToIocPath(_keyfileFilename);
|
||||
FindViewById(Resource.Id.keyfile_filename).Visibility = ViewStates.Visible;
|
||||
keyfileCheckbox.Checked = true;
|
||||
}
|
||||
|
||||
if (bundle.GetString(Util.KeyFilename, null) != null)
|
||||
{
|
||||
_ioc = new IOConnectionInfo
|
||||
{
|
||||
Path = bundle.GetString(Util.KeyFilename),
|
||||
UserName = bundle.GetString(Util.KeyServerusername),
|
||||
Password = bundle.GetString(Util.KeyServerpassword),
|
||||
CredSaveMode = (IOCredSaveMode) bundle.GetInt(Util.KeyServercredmode),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
UpdateIocView();
|
||||
|
||||
keyfileCheckbox.CheckedChange += (sender, args) =>
|
||||
{
|
||||
if (keyfileCheckbox.Checked)
|
||||
{
|
||||
if (_restoringInstanceState)
|
||||
return;
|
||||
|
||||
Util.ShowBrowseDialog(this, RequestCodeKeyFile, false, true);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
FindViewById(Resource.Id.keyfile_filename).Visibility = ViewStates.Gone;
|
||||
_keyfileFilename = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FindViewById(Resource.Id.btn_change_location).Click += (sender, args) =>
|
||||
{
|
||||
Intent intent = new Intent(this, typeof(FileStorageSelectionActivity));
|
||||
StartActivityForResult(intent, RequestCodeDbFilename);
|
||||
};
|
||||
|
||||
Button generatePassword = (Button)FindViewById(Resource.Id.generate_button);
|
||||
generatePassword.Click += (sender, e) =>
|
||||
{
|
||||
GeneratePasswordActivity.LaunchWithoutLockCheck(this);
|
||||
};
|
||||
|
||||
FindViewById(Resource.Id.btn_create).Click += (sender, evt) =>
|
||||
{
|
||||
CreateDatabase(Intent.GetBooleanExtra("MakeCurrent",true));
|
||||
};
|
||||
|
||||
Button btnTogglePassword = (Button)FindViewById(Resource.Id.toggle_password);
|
||||
btnTogglePassword.Click += (sender, e) =>
|
||||
{
|
||||
_showPassword = !_showPassword;
|
||||
MakePasswordMaskedOrVisible();
|
||||
};
|
||||
|
||||
|
||||
Util.SetNoPersonalizedLearning(FindViewById(Resource.Id.root));
|
||||
|
||||
}
|
||||
|
||||
readonly PasswordFont _passwordFont = new PasswordFont();
|
||||
|
||||
private void MakePasswordMaskedOrVisible()
|
||||
{
|
||||
EditText password = (EditText)FindViewById(Resource.Id.entry_password);
|
||||
TextView confpassword = (TextView)FindViewById(Resource.Id.entry_confpassword);
|
||||
int selStart = password.SelectionStart, selEnd = password.SelectionEnd;
|
||||
if (_showPassword)
|
||||
{
|
||||
password.InputType = InputTypes.ClassText | InputTypes.TextVariationVisiblePassword;
|
||||
_passwordFont.ApplyTo(password);
|
||||
confpassword.Visibility = ViewStates.Gone;
|
||||
}
|
||||
else
|
||||
{
|
||||
password.InputType = InputTypes.ClassText | InputTypes.TextVariationPassword;
|
||||
confpassword.Visibility = ViewStates.Visible;
|
||||
}
|
||||
password.SetSelection(selStart, selEnd);
|
||||
}
|
||||
|
||||
private void CreateDatabase(bool makeCurrent)
|
||||
{
|
||||
var keyfileCheckbox = FindViewById<CheckBox>(Resource.Id.use_keyfile);
|
||||
string password;
|
||||
if (!TryGetPassword(out password)) return;
|
||||
|
||||
|
||||
// Verify that a password or keyfile is set
|
||||
if (password.Length == 0 && !keyfileCheckbox.Checked)
|
||||
{
|
||||
Toast.MakeText(this, Resource.String.error_nopass, ToastLength.Long).Show();
|
||||
return;
|
||||
}
|
||||
|
||||
//create the key
|
||||
CompositeKey newKey = new CompositeKey();
|
||||
if (String.IsNullOrEmpty(password) == false)
|
||||
{
|
||||
newKey.AddUserKey(new KcpPassword(password));
|
||||
}
|
||||
if (String.IsNullOrEmpty(_keyfileFilename) == false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ioc = IOConnectionInfo.FromPath(_keyfileFilename);
|
||||
using (var stream = App.Kp2a.GetFileStorage(ioc).OpenFileForRead(ioc))
|
||||
{
|
||||
byte[] keyfileData = Util.StreamToMemoryStream(stream).ToArray();
|
||||
newKey.AddUserKey(new KcpKeyFile(keyfileData, ioc, true));
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Toast.MakeText(this, Resource.String.error_adding_keyfile, ToastLength.Long).Show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new database
|
||||
CreateDb create = new CreateDb(App.Kp2a, this, _ioc, new LaunchGroupActivity(_ioc, this), false, newKey, makeCurrent);
|
||||
ProgressTask createTask = new ProgressTask(
|
||||
App.Kp2a,
|
||||
this, create);
|
||||
createTask.Run();
|
||||
}
|
||||
|
||||
private bool TryGetPassword(out string pass)
|
||||
{
|
||||
TextView passView = (TextView)FindViewById(Resource.Id.entry_password);
|
||||
pass = passView.Text;
|
||||
|
||||
if (_showPassword)
|
||||
return true;
|
||||
|
||||
TextView passConfView = (TextView) FindViewById(Resource.Id.entry_confpassword);
|
||||
String confpass = passConfView.Text;
|
||||
|
||||
// Verify that passwords match
|
||||
if (! pass.Equals(confpass))
|
||||
{
|
||||
// Passwords do not match
|
||||
Toast.MakeText(this, Resource.String.error_pass_match, ToastLength.Long).Show();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
_design.ReapplyTheme();
|
||||
}
|
||||
|
||||
protected override void OnRestoreInstanceState(Bundle savedInstanceState)
|
||||
{
|
||||
_restoringInstanceState = true;
|
||||
base.OnRestoreInstanceState(savedInstanceState);
|
||||
_restoringInstanceState = false;
|
||||
}
|
||||
|
||||
|
||||
private void UpdateIocView()
|
||||
{
|
||||
string displayPath = App.Kp2a.GetFileStorage(_ioc).GetDisplayName(_ioc);
|
||||
int protocolSeparatorPos = displayPath.IndexOf("://", StringComparison.Ordinal);
|
||||
string protocolId = protocolSeparatorPos < 0 ?
|
||||
"file" : displayPath.Substring(0, protocolSeparatorPos);
|
||||
Drawable drawable = App.Kp2a.GetStorageIcon(protocolId);
|
||||
FindViewById<ImageView>(Resource.Id.filestorage_logo).SetImageDrawable(drawable);
|
||||
|
||||
String title = App.Kp2a.GetStorageDisplayName(protocolId);
|
||||
FindViewById<TextView>(Resource.Id.filestorage_label).Text = title;
|
||||
|
||||
FindViewById<TextView>(Resource.Id.label_filename).Text = protocolSeparatorPos < 0 ?
|
||||
displayPath :
|
||||
displayPath.Substring(protocolSeparatorPos + 3);
|
||||
|
||||
}
|
||||
|
||||
private void SetDefaultIoc()
|
||||
{
|
||||
Java.IO.File directory = GetExternalFilesDir(null);
|
||||
if (directory == null)
|
||||
directory = FilesDir;
|
||||
|
||||
string strDir = directory.CanonicalPath;
|
||||
if (!strDir.EndsWith(Java.IO.File.Separator))
|
||||
strDir += Java.IO.File.Separator;
|
||||
|
||||
string filename = strDir + "keepass.kdbx";
|
||||
filename = FileSelectHelper.ConvertFilenameToIocPath(filename);
|
||||
int count = 2;
|
||||
while (new Java.IO.File(filename).Exists())
|
||||
{
|
||||
filename = FileSelectHelper.ConvertFilenameToIocPath(strDir + "keepass" + count + ".kdbx");
|
||||
count++;
|
||||
}
|
||||
|
||||
_ioc = new IOConnectionInfo
|
||||
{
|
||||
Path = filename
|
||||
};
|
||||
}
|
||||
|
||||
private static string SdDir
|
||||
{
|
||||
get
|
||||
{
|
||||
string sdDir = Environment.ExternalStorageDirectory.AbsolutePath;
|
||||
if (!sdDir.EndsWith("/"))
|
||||
sdDir += "/";
|
||||
if (!sdDir.StartsWith("file://"))
|
||||
sdDir = "file://" + sdDir;
|
||||
return sdDir;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
base.OnActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (resultCode == KeePass.ResultOkPasswordGenerator)
|
||||
{
|
||||
String generatedPassword = data.GetStringExtra("keepass2android.password.generated_password");
|
||||
FindViewById<TextView>(Resource.Id.entry_password).Text = generatedPassword;
|
||||
FindViewById<TextView>(Resource.Id.entry_confpassword).Text = generatedPassword;
|
||||
}
|
||||
|
||||
FileSelectHelper fileSelectHelper = new FileSelectHelper(this, true, true, RequestCodeDbFilename)
|
||||
{
|
||||
DefaultExtension = "kdbx"
|
||||
};
|
||||
fileSelectHelper.OnOpen += (sender, info) =>
|
||||
{
|
||||
_ioc = info;
|
||||
(sender as CreateDatabaseActivity ?? this).UpdateIocView();
|
||||
};
|
||||
|
||||
if (fileSelectHelper.HandleActivityResult(this, requestCode, resultCode, data))
|
||||
return;
|
||||
|
||||
|
||||
if (resultCode == Result.Ok)
|
||||
{
|
||||
if (requestCode == RequestCodeKeyFile)
|
||||
{
|
||||
if (data.Data.Scheme == "content")
|
||||
{
|
||||
if ((int)Build.VERSION.SdkInt >= 19)
|
||||
{
|
||||
//try to take persistable permissions
|
||||
try
|
||||
{
|
||||
Kp2aLog.Log("TakePersistableUriPermission");
|
||||
var takeFlags = data.Flags
|
||||
& (ActivityFlags.GrantReadUriPermission
|
||||
| ActivityFlags.GrantWriteUriPermission);
|
||||
this.ContentResolver.TakePersistableUriPermission(data.Data, takeFlags);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.Log(e.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
string filename = Util.IntentToFilename(data, this);
|
||||
if (filename == null)
|
||||
filename = data.DataString;
|
||||
|
||||
|
||||
_keyfileFilename = FileSelectHelper.ConvertFilenameToIocPath(filename);
|
||||
FindViewById<TextView>(Resource.Id.keyfile_filename).Text = _keyfileFilename;
|
||||
FindViewById(Resource.Id.keyfile_filename).Visibility = ViewStates.Visible;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if (resultCode == (Result)FileStorageResults.FileUsagePrepared)
|
||||
{
|
||||
_ioc = new IOConnectionInfo();
|
||||
Util.SetIoConnectionFromIntent(_ioc, data);
|
||||
UpdateIocView();
|
||||
}
|
||||
if (resultCode == (Result)FileStorageResults.FileChooserPrepared)
|
||||
{
|
||||
IOConnectionInfo ioc = new IOConnectionInfo();
|
||||
Util.SetIoConnectionFromIntent(ioc, data);
|
||||
|
||||
new FileSelectHelper(this, true, true, RequestCodeDbFilename) { DefaultExtension = "kdbx" }
|
||||
.StartFileChooser(ioc.Path);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void AfterQueryCredentials(IOConnectionInfo ioc)
|
||||
{
|
||||
_ioc = ioc;
|
||||
UpdateIocView();
|
||||
}
|
||||
|
||||
private class LaunchGroupActivity : FileOnFinish
|
||||
{
|
||||
readonly CreateDatabaseActivity _activity;
|
||||
private readonly IOConnectionInfo _ioc;
|
||||
|
||||
public LaunchGroupActivity(IOConnectionInfo ioc, CreateDatabaseActivity activity)
|
||||
: base(activity, null)
|
||||
{
|
||||
_activity = activity;
|
||||
_ioc = ioc;
|
||||
}
|
||||
|
||||
public override void Run()
|
||||
{
|
||||
if (Success)
|
||||
{
|
||||
// Update the ongoing notification
|
||||
App.Kp2a.UpdateOngoingNotification();
|
||||
|
||||
if (PreferenceManager.GetDefaultSharedPreferences(_activity).GetBoolean(_activity.GetString(Resource.String.RememberRecentFiles_key), _activity.Resources.GetBoolean(Resource.Boolean.RememberRecentFiles_default)))
|
||||
{
|
||||
// Add to recent files
|
||||
FileDbHelper dbHelper = App.Kp2a.FileDbHelper;
|
||||
|
||||
dbHelper.CreateFile(_ioc, Filename, true);
|
||||
}
|
||||
|
||||
Intent data = new Intent();
|
||||
data.PutExtra("ioc", IOConnectionInfo.SerializeToString(_ioc));
|
||||
|
||||
_activity.SetResult(Result.Ok, data);
|
||||
|
||||
|
||||
_activity.Finish();
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
DisplayMessage(_activity);
|
||||
try
|
||||
{
|
||||
App.Kp2a.GetFileStorage(_ioc).Delete(_ioc);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//not nice, but not a catastrophic failure if we can't delete the file:
|
||||
Kp2aLog.Log("couldn't delete file after failure! " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override bool OnOptionsItemSelected(IMenuItem item)
|
||||
{
|
||||
switch (item.ItemId)
|
||||
{
|
||||
case Android.Resource.Id.Home:
|
||||
OnBackPressed();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
36
src/keepass2android-app/CreateNewFilename.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using Android.App;
|
||||
using KeePassLib.Serialization;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
class CreateNewFilename : RunnableOnFinish
|
||||
{
|
||||
private readonly string _filename;
|
||||
|
||||
public CreateNewFilename(Activity activity, OnFinish finish, string filename)
|
||||
: base(activity,finish)
|
||||
{
|
||||
_filename = filename;
|
||||
}
|
||||
|
||||
public override void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
int lastIndexOfSlash = _filename.LastIndexOf("/", StringComparison.Ordinal);
|
||||
string parent = _filename.Substring(0, lastIndexOfSlash);
|
||||
string newFilename = _filename.Substring(lastIndexOfSlash + 1);
|
||||
|
||||
string resultingFilename = App.Kp2a.GetFileStorage(new IOConnectionInfo { Path = parent }).CreateFilePath(parent, newFilename);
|
||||
|
||||
Finish(true, resultingFilename);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Finish(false, e.Message);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
107
src/keepass2android-app/DisableAutofillForQueryActivity.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.App.Assist;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Runtime;
|
||||
using Android.Service.Autofill;
|
||||
using Android.Util;
|
||||
using Android.Views;
|
||||
using Android.Views.Autofill;
|
||||
using Android.Widget;
|
||||
using keepass2android.services;
|
||||
using keepass2android.services.AutofillBase;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "DisableAutofillForQueryActivity")]
|
||||
public class DisableAutofillForQueryActivity : Activity
|
||||
{
|
||||
public IAutofillIntentBuilder IntentBuilder = new Kp2aAutofillIntentBuilder();
|
||||
|
||||
public const string ExtraIsDisable = "EXTRA_IS_DISABLE";
|
||||
|
||||
protected void RestartApp()
|
||||
{
|
||||
Intent intent = IntentBuilder.GetRestartAppIntent(this);
|
||||
StartActivity(intent);
|
||||
Finish();
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
|
||||
string requestedUrl = Intent.GetStringExtra(ChooseForAutofillActivityBase.ExtraQueryString);
|
||||
if (requestedUrl == null)
|
||||
{
|
||||
Toast.MakeText(this, "Cannot execute query for null.", ToastLength.Long).Show();
|
||||
RestartApp();
|
||||
return;
|
||||
}
|
||||
|
||||
var prefs = PreferenceManager.GetDefaultSharedPreferences(this);
|
||||
|
||||
bool isDisable = Intent.GetBooleanExtra(ExtraIsDisable, true);
|
||||
|
||||
var disabledValues = prefs.GetStringSet("AutoFillDisabledQueries", new HashSet<string>() { }).ToHashSet();
|
||||
if (isDisable)
|
||||
{
|
||||
disabledValues.Add(requestedUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
disabledValues.Remove(requestedUrl);
|
||||
}
|
||||
|
||||
|
||||
prefs.Edit().PutStringSet("AutoFillDisabledQueries", disabledValues).Commit();
|
||||
|
||||
bool isManual = Intent.GetBooleanExtra(ChooseForAutofillActivityBase.ExtraIsManualRequest, false);
|
||||
|
||||
Intent reply = new Intent();
|
||||
FillResponse.Builder builder = new FillResponse.Builder();
|
||||
AssistStructure structure = (AssistStructure)Intent.GetParcelableExtra(AutofillManager.ExtraAssistStructure);
|
||||
if (structure == null)
|
||||
{
|
||||
SetResult(Result.Canceled);
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
StructureParser parser = new StructureParser(this, structure);
|
||||
try
|
||||
{
|
||||
parser.ParseForFill(isManual);
|
||||
|
||||
}
|
||||
catch (Java.Lang.SecurityException e)
|
||||
{
|
||||
Log.Warn(CommonUtil.Tag, "Security exception handling request");
|
||||
SetResult(Result.Canceled);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
reply.PutExtra(AutofillManager.ExtraAuthenticationResult, (FillResponse)null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(e);
|
||||
throw;
|
||||
}
|
||||
|
||||
SetResult(Result.Ok, reply);
|
||||
|
||||
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
}
|
246
src/keepass2android-app/DonateReminder.cs
Normal file
@@ -0,0 +1,246 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using keepass2android;
|
||||
using KeePassLib.Utility;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = AppNames.AppName, Theme = "@style/Kp2aTheme_ActionBar")]
|
||||
public class DonateReminder : Activity
|
||||
{
|
||||
class Reminder
|
||||
{
|
||||
public DateTime From, To;
|
||||
public int ResourceToShow;
|
||||
public string Key;
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
base.OnCreate(bundle);
|
||||
|
||||
foreach (Reminder r in GetReminders())
|
||||
{
|
||||
if ((DateTime.Now >= r.From)
|
||||
&& (DateTime.Now < r.To))
|
||||
{
|
||||
SetContentView(r.ResourceToShow);
|
||||
}
|
||||
}
|
||||
|
||||
FindViewById(Resource.Id.ok_donate).Click += (sender, args) => { Util.GotoDonateUrl(this);Finish(); };
|
||||
FindViewById(Resource.Id.no_donate).Click += (sender, args) =>
|
||||
{
|
||||
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
|
||||
|
||||
ISharedPreferencesEditor edit = prefs.Edit();
|
||||
edit.PutBoolean("DismissedDonateReminder", true);
|
||||
EditorCompat.Apply(edit);
|
||||
|
||||
Finish();
|
||||
};
|
||||
}
|
||||
static IEnumerable<Reminder> GetReminders()
|
||||
{
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2017, 09, 16),
|
||||
To = new DateTime(2017, 09, 25),
|
||||
Key = "DonationOktoberfest2017b"//b because year was incorrectly set to 2015 in 0.9.8b
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2017, 09, 25),
|
||||
To = new DateTime(2017, 10, 04),
|
||||
Key = "DonationOktoberfest2017b-2"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2018, 09, 22),
|
||||
To = new DateTime(2018, 09, 30),
|
||||
Key = "DonationOktoberfest2018b"//b because year was incorrectly set to 2015 in 0.9.8b
|
||||
,ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2018, 09, 30),
|
||||
To = new DateTime(2018, 10, 08),
|
||||
Key = "DonationOktoberfest2018b-2"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2019, 09, 21),
|
||||
To = new DateTime(2019, 09, 30),
|
||||
Key = "DonationOktoberfest2019"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2019, 09, 30),
|
||||
To = new DateTime(2019, 10, 07),
|
||||
Key = "DonationOktoberfest2019-2"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2020, 09, 19),
|
||||
To = new DateTime(2020, 09, 26),
|
||||
Key = "DonationOktoberfest2020"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2020, 09, 26),
|
||||
To = new DateTime(2020, 10, 05),
|
||||
Key = "DonationOktoberfest2020-2"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2021, 09, 18),
|
||||
To = new DateTime(2021, 09, 26),
|
||||
Key = "DonationOktoberfest2021"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2021, 09, 26),
|
||||
To = new DateTime(2021, 10, 04),
|
||||
Key = "DonationOktoberfest2021-2"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2022, 09, 17),
|
||||
To = new DateTime(2022, 09, 25),
|
||||
Key = "DonationOktoberfest2022"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2022, 09, 25),
|
||||
To = new DateTime(2022, 10, 04),
|
||||
Key = "DonationOktoberfest2022-2"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2023, 09, 16),
|
||||
To = new DateTime(2023, 09, 25),
|
||||
Key = "DonationOktoberfest2023"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2023, 09, 25),
|
||||
To = new DateTime(2023, 10, 04),
|
||||
Key = "DonationOktoberfest2023-2"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2024, 09, 21),
|
||||
To = new DateTime(2024, 09, 28),
|
||||
Key = "DonationOktoberfest2024"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(2024, 09, 28),
|
||||
To = new DateTime(2024, 10, 08),
|
||||
Key = "DonationOktoberfest2024-2"
|
||||
,
|
||||
ResourceToShow = Resource.Layout.donate
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
int thisYear = DateTime.Now.Year;
|
||||
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(thisYear, 05, 10),
|
||||
To = new DateTime(thisYear, 05, 11),
|
||||
Key = "DonationBirthday" + thisYear,
|
||||
ResourceToShow = Resource.Layout.donate_bday
|
||||
};
|
||||
yield return new Reminder
|
||||
{
|
||||
From = new DateTime(thisYear, 05, 11),
|
||||
To = new DateTime(thisYear, 05, 18),
|
||||
Key = "DonationBirthday" + thisYear,
|
||||
ResourceToShow = Resource.Layout.donate_bdaymissed
|
||||
};
|
||||
}
|
||||
|
||||
public static void ShowDonateReminderIfAppropriate(Activity context)
|
||||
{
|
||||
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(context);
|
||||
if (prefs.GetBoolean(context.GetString(Resource.String.NoDonationReminder_key), false))
|
||||
return;
|
||||
|
||||
long usageCount = prefs.GetLong(context.GetString(Resource.String.UsageCount_key), 0);
|
||||
|
||||
if (usageCount <= 5)
|
||||
return;
|
||||
|
||||
foreach (Reminder r in GetReminders())
|
||||
{
|
||||
if ((DateTime.Now >= r.From )
|
||||
&& (DateTime.Now < r.To))
|
||||
{
|
||||
if (prefs.GetBoolean(r.Key, false) == false)
|
||||
{
|
||||
ISharedPreferencesEditor edit = prefs.Edit();
|
||||
edit.PutBoolean(r.Key, true);
|
||||
EditorCompat.Apply(edit);
|
||||
|
||||
context.StartActivity(new Intent(context, typeof(DonateReminder)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
1606
src/keepass2android-app/EntryActivity.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Android.Content;
|
||||
using Android.Graphics.Drawables;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Reperesents the popup menu item in EntryActivity to copy a string to clipboard
|
||||
/// </summary>
|
||||
class CopyToClipboardPopupMenuIcon : IPopupMenuItem
|
||||
{
|
||||
private readonly Context _context;
|
||||
private readonly IStringView _stringView;
|
||||
private readonly bool _isProtected;
|
||||
|
||||
public CopyToClipboardPopupMenuIcon(Context context, IStringView stringView, bool isProtected)
|
||||
{
|
||||
_context = context;
|
||||
_stringView = stringView;
|
||||
_isProtected = isProtected;
|
||||
}
|
||||
|
||||
public Drawable Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
return _context.Resources.GetDrawable(Resource.Drawable.baseline_content_copy_24);
|
||||
}
|
||||
}
|
||||
public string Text
|
||||
{
|
||||
get { return _context.Resources.GetString(Resource.String.copy_to_clipboard); }
|
||||
}
|
||||
|
||||
|
||||
public void HandleClick()
|
||||
{
|
||||
CopyToClipboardService.CopyValueToClipboardWithTimeout(_context, _stringView.Text, _isProtected);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
internal class ExtraStringView : IStringView
|
||||
{
|
||||
private readonly View _container;
|
||||
private readonly TextView _valueView;
|
||||
private readonly TextView _visibleValueView;
|
||||
private readonly TextView _keyView;
|
||||
|
||||
public ExtraStringView(LinearLayout container, TextView valueView, TextView visibleValueView, TextView keyView)
|
||||
{
|
||||
_container = container;
|
||||
_valueView = valueView;
|
||||
_visibleValueView = visibleValueView;
|
||||
_keyView = keyView;
|
||||
}
|
||||
|
||||
public View View
|
||||
{
|
||||
get { return _container; }
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
get { return _valueView.Text; }
|
||||
set
|
||||
{
|
||||
if (String.IsNullOrEmpty(value))
|
||||
{
|
||||
_container.Visibility = ViewStates.Gone;
|
||||
}
|
||||
else
|
||||
{
|
||||
_container.Visibility = ViewStates.Visible;
|
||||
_valueView.Text = value;
|
||||
if (_visibleValueView != null)
|
||||
_visibleValueView.Text = value;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
using Android.Graphics.Drawables;
|
||||
using keepass2android;
|
||||
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Reperesents the popup menu item in EntryActivity to go to the URL in the field
|
||||
/// </summary>
|
||||
class GotoUrlMenuItem : IPopupMenuItem
|
||||
{
|
||||
public string UrlFieldKey { get; }
|
||||
private readonly EntryActivity _ctx;
|
||||
|
||||
public GotoUrlMenuItem(EntryActivity ctx, string urlFieldKey)
|
||||
{
|
||||
UrlFieldKey = urlFieldKey;
|
||||
_ctx = ctx;
|
||||
}
|
||||
|
||||
public Drawable Icon
|
||||
{
|
||||
get { return _ctx.Resources.GetDrawable(Resource.Drawable.baseline_upload_24); }
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
get { return _ctx.Resources.GetString(Resource.String.menu_url); }
|
||||
}
|
||||
|
||||
public void HandleClick()
|
||||
{
|
||||
_ctx.GotoUrl(UrlFieldKey);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using Android.Graphics.Drawables;
|
||||
using KeePassLib;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for popup menu items in EntryActivity
|
||||
/// </summary>
|
||||
internal interface IPopupMenuItem
|
||||
{
|
||||
Drawable Icon { get; }
|
||||
String Text { get; }
|
||||
|
||||
void HandleClick();
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace keepass2android
|
||||
{
|
||||
internal interface IStringView
|
||||
{
|
||||
string Text { set; get; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
using Android.Graphics.Drawables;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the popup menu item in EntryActivity to open the associated attachment
|
||||
/// </summary>
|
||||
internal class OpenBinaryPopupItem : IPopupMenuItem
|
||||
{
|
||||
private readonly string _key;
|
||||
private readonly EntryActivity _entryActivity;
|
||||
|
||||
public OpenBinaryPopupItem(string key, EntryActivity entryActivity)
|
||||
{
|
||||
_key = key;
|
||||
_entryActivity = entryActivity;
|
||||
}
|
||||
|
||||
public Drawable Icon
|
||||
{
|
||||
get { return _entryActivity.Resources.GetDrawable(Resource.Drawable.baseline_share_24); }
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
get { return _entryActivity.Resources.GetString(Resource.String.SaveAttachmentDialog_open); }
|
||||
}
|
||||
|
||||
public void HandleClick()
|
||||
{
|
||||
Android.Net.Uri newUri = _entryActivity.WriteBinaryToFile(_key, true);
|
||||
if (newUri != null)
|
||||
{
|
||||
_entryActivity.OpenBinaryFile(newUri);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
using Android.Content;
|
||||
using Android.Graphics.Drawables;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
class PluginMenuOption
|
||||
{
|
||||
public string DisplayText { get; set; }
|
||||
|
||||
public Drawable Icon { get; set; }
|
||||
|
||||
public Intent Intent { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
using Android.Content;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.OS;
|
||||
using Keepass2android.Pluginsdk;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a popup menu item in EntryActivity which was added by a plugin. The click will therefore broadcast to the plugin.
|
||||
/// </summary>
|
||||
class PluginPopupMenuItem : IPopupMenuItem
|
||||
{
|
||||
private readonly EntryActivity _activity;
|
||||
private readonly string _pluginPackage;
|
||||
private readonly string _fieldId;
|
||||
private readonly string _popupItemId;
|
||||
private readonly string _displayText;
|
||||
private readonly int _iconId;
|
||||
private readonly Bundle _bundleExtra;
|
||||
|
||||
public PluginPopupMenuItem(EntryActivity activity, string pluginPackage, string fieldId, string popupItemId, string displayText, int iconId, Bundle bundleExtra)
|
||||
{
|
||||
_activity = activity;
|
||||
_pluginPackage = pluginPackage;
|
||||
_fieldId = fieldId;
|
||||
_popupItemId = popupItemId;
|
||||
_displayText = displayText;
|
||||
_iconId = iconId;
|
||||
_bundleExtra = bundleExtra;
|
||||
}
|
||||
|
||||
public Drawable Icon
|
||||
{
|
||||
get { return _activity.PackageManager.GetResourcesForApplication(_pluginPackage).GetDrawable(_iconId); }
|
||||
}
|
||||
public string Text
|
||||
{
|
||||
get { return _displayText; }
|
||||
}
|
||||
|
||||
public string PopupItemId
|
||||
{
|
||||
get { return _popupItemId; }
|
||||
}
|
||||
|
||||
public void HandleClick()
|
||||
{
|
||||
Intent i = new Intent(Strings.ActionEntryActionSelected);
|
||||
i.SetPackage(_pluginPackage);
|
||||
i.PutExtra(Strings.ExtraActionData, _bundleExtra);
|
||||
i.PutExtra(Strings.ExtraFieldId, _fieldId);
|
||||
i.PutExtra(Strings.ExtraSender, _activity.PackageName);
|
||||
|
||||
_activity.AddEntryToIntent(i);
|
||||
|
||||
_activity.SendBroadcast(i);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Android.App;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
internal class StandardStringView : IStringView
|
||||
{
|
||||
private readonly List<int> _viewIds;
|
||||
private readonly int _containerViewId;
|
||||
private readonly Activity _activity;
|
||||
|
||||
public StandardStringView(List<int> viewIds, int containerViewId, Activity activity)
|
||||
{
|
||||
_viewIds = viewIds;
|
||||
_containerViewId = containerViewId;
|
||||
_activity = activity;
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
set
|
||||
{
|
||||
View container = _activity.FindViewById(_containerViewId);
|
||||
foreach (int viewId in _viewIds)
|
||||
{
|
||||
TextView tv = (TextView) _activity.FindViewById(viewId);
|
||||
if (String.IsNullOrEmpty(value))
|
||||
{
|
||||
container.Visibility = tv.Visibility = ViewStates.Gone;
|
||||
}
|
||||
else
|
||||
{
|
||||
container.Visibility = tv.Visibility = ViewStates.Visible;
|
||||
tv.Text = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
get
|
||||
{
|
||||
TextView tv = (TextView) _activity.FindViewById(_viewIds.First());
|
||||
return tv.Text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.Widget;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Reperesents the popup menu item in EntryActivity to toggle visibility of all protected strings (e.g. Password)
|
||||
/// </summary>
|
||||
class ToggleVisibilityPopupMenuItem : IPopupMenuItem
|
||||
{
|
||||
private readonly EntryActivity _activity;
|
||||
private readonly TextView _valueView;
|
||||
|
||||
|
||||
public ToggleVisibilityPopupMenuItem(EntryActivity activity, TextView valueView)
|
||||
{
|
||||
_activity = activity;
|
||||
_valueView = valueView;
|
||||
}
|
||||
|
||||
public Drawable Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
//return new TextDrawable("\uF06E", _activity);
|
||||
return _activity.Resources.GetDrawable(Resource.Drawable.baseline_visibility_24);
|
||||
|
||||
}
|
||||
}
|
||||
public string Text
|
||||
{
|
||||
get
|
||||
{
|
||||
return _activity.Resources.GetString(
|
||||
_activity.GetVisibilityForProtectedView(_valueView) ?
|
||||
Resource.String.menu_hide_password
|
||||
: Resource.String.show_password);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void HandleClick()
|
||||
{
|
||||
_activity.ToggleVisibility(_valueView);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.Res;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android.EntryActivityClasses
|
||||
{
|
||||
internal class ViewImagePopupItem:IPopupMenuItem
|
||||
{
|
||||
private readonly string _key;
|
||||
private readonly EntryActivity _entryActivity;
|
||||
|
||||
public ViewImagePopupItem(string key, EntryActivity entryActivity)
|
||||
{
|
||||
_key = key;
|
||||
_entryActivity = entryActivity;
|
||||
}
|
||||
public Drawable Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
return _entryActivity.Resources.GetDrawable(Resource.Drawable.baseline_image_24);
|
||||
}
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
get
|
||||
{
|
||||
return _entryActivity.Resources.GetString(Resource.String.ShowAttachedImage);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleClick()
|
||||
{
|
||||
_entryActivity.ShowAttachedImage(_key);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
using Android.Graphics.Drawables;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the popup menu item in EntryActivity to store the binary attachment on SD card
|
||||
/// </summary>
|
||||
internal class WriteBinaryToFilePopupItem : IPopupMenuItem
|
||||
{
|
||||
private readonly string _key;
|
||||
private readonly EntryActivity _activity;
|
||||
|
||||
public WriteBinaryToFilePopupItem(string key, EntryActivity activity)
|
||||
{
|
||||
_key = key;
|
||||
_activity = activity;
|
||||
}
|
||||
|
||||
public Drawable Icon
|
||||
{
|
||||
get { return _activity.Resources.GetDrawable(Resource.Drawable.baseline_save_24); }
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
get { return _activity.Resources.GetString(Resource.String.SaveAttachmentDialog_save); }
|
||||
}
|
||||
|
||||
public void HandleClick()
|
||||
{
|
||||
_activity.WriteBinaryToFile(_key, false);
|
||||
}
|
||||
}
|
||||
}
|
1597
src/keepass2android-app/EntryEditActivity.cs
Normal file
88
src/keepass2android-app/EntryEditActivityState.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Collections.Generic;
|
||||
using KeePassLib;
|
||||
using KeePassLib.Security;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public abstract class EditModeBase
|
||||
{
|
||||
|
||||
|
||||
public virtual bool IsVisible(string fieldKey)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual IEnumerable<string> SortExtraFieldKeys(IEnumerable<string> keys)
|
||||
{
|
||||
return keys;
|
||||
}
|
||||
|
||||
protected bool? manualShowAddAttachments = null;
|
||||
|
||||
public virtual bool ShowAddAttachments
|
||||
{
|
||||
get
|
||||
{
|
||||
if (manualShowAddAttachments != null) return (bool)manualShowAddAttachments;
|
||||
return true; }
|
||||
set { manualShowAddAttachments = value; }
|
||||
}
|
||||
|
||||
|
||||
protected bool? manualShowAddExtras = null;
|
||||
|
||||
public virtual bool ShowAddExtras
|
||||
{
|
||||
get
|
||||
{
|
||||
if (manualShowAddExtras != null) return (bool) manualShowAddExtras;
|
||||
return true;
|
||||
}
|
||||
set { manualShowAddExtras = value; }
|
||||
}
|
||||
|
||||
public virtual string GetTitle(string key)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
public virtual string GetFieldType(string key)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
public virtual void InitializeEntry(PwEntry entry)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void PrepareForSaving(PwEntry entry)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the state of the EntrryEditActivity. This is required to be able to keep a partially modified entry in memory
|
||||
/// through the App variable. Serializing this state (especially the Entry/EntryInDatabase) can be a performance problem
|
||||
/// when there are big attachements.
|
||||
/// </summary>
|
||||
internal class EntryEditActivityState
|
||||
{
|
||||
internal PwEntry Entry, EntryInDatabase;
|
||||
internal bool ShowPassword = false;
|
||||
internal bool IsNew;
|
||||
internal PwIcon SelectedIconId;
|
||||
internal PwUuid SelectedCustomIconId = PwUuid.Zero;
|
||||
internal bool SelectedIcon = false;
|
||||
|
||||
internal PwGroup ParentGroup;
|
||||
|
||||
internal bool EntryModified;
|
||||
|
||||
public EditModeBase EditMode { get; set; }
|
||||
|
||||
//the key of the extra field to which the last triggered file selection process belongs
|
||||
public string LastTriggeredFileSelectionProcessKey;
|
||||
}
|
||||
}
|
||||
|
151
src/keepass2android-app/ExportDatabaseActivity.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Util;
|
||||
using Android.Widget;
|
||||
using Google.Android.Material.Dialog;
|
||||
using KeePass.DataExchange;
|
||||
using KeePass.DataExchange.Formats;
|
||||
using KeePassLib.Interfaces;
|
||||
using KeePassLib.Serialization;
|
||||
using keepass2android.Io;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public class ExportDbProcessManager: FileSaveProcessManager
|
||||
{
|
||||
private readonly FileFormatProvider _ffp;
|
||||
|
||||
public ExportDbProcessManager(int requestCode, Activity activity, FileFormatProvider ffp) : base(requestCode, activity)
|
||||
{
|
||||
_ffp = ffp;
|
||||
}
|
||||
|
||||
protected override void SaveFile(IOConnectionInfo ioc)
|
||||
{
|
||||
var exportDb = new ExportDatabaseActivity.ExportDb(_activity, App.Kp2a, new ActionOnFinish(_activity, (success, message, activity) =>
|
||||
{
|
||||
if (!success)
|
||||
Toast.MakeText(activity, message, ToastLength.Long).Show();
|
||||
else
|
||||
Toast.MakeText(activity, _activity.GetString(Resource.String.export_database_successful), ToastLength.Long).Show();
|
||||
activity.Finish();
|
||||
}
|
||||
), _ffp, ioc);
|
||||
ProgressTask pt = new ProgressTask(App.Kp2a, _activity, exportDb);
|
||||
pt.Run();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Activity(Label = "@string/app_name",
|
||||
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden,
|
||||
Theme = "@style/Kp2aTheme_ActionBar", Exported = true)]
|
||||
[IntentFilter(new[] {"kp2a.action.ExportDatabaseActivity"}, Categories = new[] {Intent.CategoryDefault})]
|
||||
public class ExportDatabaseActivity : LockCloseActivity
|
||||
{
|
||||
FileFormatProvider[] _ffp = new FileFormatProvider[]
|
||||
{
|
||||
new KeePassKdb2x(),
|
||||
new KeePassXml2x(),
|
||||
new KeePassCsv1x()
|
||||
};
|
||||
|
||||
private int _fileFormatIndex;
|
||||
|
||||
private ExportDbProcessManager _exportDbProcessManager;
|
||||
|
||||
protected override void OnCreate(Android.OS.Bundle savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
|
||||
builder.SetTitle(Resource.String.export_fileformats_title);
|
||||
builder.SetSingleChoiceItems(Resource.Array.export_fileformat_options, _fileFormatIndex,
|
||||
delegate(object sender, DialogClickEventArgs args) { _fileFormatIndex = args.Which; });
|
||||
builder.SetPositiveButton(Android.Resource.String.Ok, delegate
|
||||
{
|
||||
_exportDbProcessManager = new ExportDbProcessManager(0, this, _ffp[_fileFormatIndex]);
|
||||
_exportDbProcessManager.StartProcess();
|
||||
});
|
||||
builder.SetNegativeButton(Resource.String.cancel, delegate {
|
||||
Finish();
|
||||
});
|
||||
builder.Show();
|
||||
}
|
||||
|
||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
base.OnActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (_exportDbProcessManager?.OnActivityResult(requestCode, resultCode, data) == true)
|
||||
return;
|
||||
|
||||
Finish();
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected int RequestCodeDbFilename
|
||||
{
|
||||
get { return 0; }
|
||||
}
|
||||
|
||||
public class ExportDb : RunnableOnFinish
|
||||
{
|
||||
private readonly IKp2aApp _app;
|
||||
private readonly FileFormatProvider _fileFormat;
|
||||
private IOConnectionInfo _targetIoc;
|
||||
|
||||
public ExportDb(Activity activity, IKp2aApp app, OnFinish onFinish, FileFormatProvider fileFormat, IOConnectionInfo targetIoc) : base(activity, onFinish)
|
||||
{
|
||||
_app = app;
|
||||
this._fileFormat = fileFormat;
|
||||
_targetIoc = targetIoc;
|
||||
}
|
||||
|
||||
public override void Run()
|
||||
{
|
||||
StatusLogger.UpdateMessage(UiStringKey.exporting_database);
|
||||
var pd = _app.CurrentDb.KpDatabase;
|
||||
PwExportInfo pwInfo = new PwExportInfo(pd.RootGroup, pd, true);
|
||||
|
||||
try
|
||||
{
|
||||
var fileStorage = _app.GetFileStorage(_targetIoc);
|
||||
if (fileStorage is IOfflineSwitchable)
|
||||
{
|
||||
((IOfflineSwitchable) fileStorage).IsOffline = false;
|
||||
}
|
||||
using (var writeTransaction = fileStorage.OpenWriteTransaction(_targetIoc, _app.GetBooleanPreference(PreferenceKey.UseFileTransactions)))
|
||||
{
|
||||
Stream sOut = writeTransaction.OpenFile();
|
||||
_fileFormat.Export(pwInfo, sOut, new NullStatusLogger());
|
||||
|
||||
if (sOut != null) sOut.Close();
|
||||
|
||||
writeTransaction.CommitWrite();
|
||||
|
||||
}
|
||||
if (fileStorage is IOfflineSwitchable)
|
||||
{
|
||||
((IOfflineSwitchable)fileStorage).IsOffline = App.Kp2a.OfflineMode;
|
||||
}
|
||||
|
||||
Finish(true);
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Finish(false, ex.Message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
155
src/keepass2android-app/FileSaveProcessManager.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Widget;
|
||||
using keepass2android.Io;
|
||||
using KeePassLib.Serialization;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public abstract class FileSaveProcessManager
|
||||
{
|
||||
|
||||
protected readonly int _requestCode;
|
||||
protected readonly Activity _activity;
|
||||
|
||||
public FileSaveProcessManager(int requestCode, Activity activity)
|
||||
{
|
||||
_requestCode = requestCode;
|
||||
_activity = activity;
|
||||
}
|
||||
|
||||
public bool OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
|
||||
|
||||
if (requestCode == _requestCode)
|
||||
{
|
||||
if (resultCode == KeePass.ExitFileStorageSelectionOk)
|
||||
{
|
||||
string protocolId = data.GetStringExtra("protocolId");
|
||||
if (protocolId == "content")
|
||||
{
|
||||
Util.ShowBrowseDialog(_activity, _requestCode, true, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
FileSelectHelper fileSelectHelper = new FileSelectHelper(_activity, true, true, _requestCode);
|
||||
fileSelectHelper.OnOpen += (sender, ioc) =>
|
||||
{
|
||||
SaveFile(ioc);
|
||||
};
|
||||
App.Kp2a.GetFileStorage(protocolId).StartSelectFile(
|
||||
new FileStorageSetupInitiatorActivity(_activity, (i, result, arg3) => OnActivityResult(i, result, arg3), s => fileSelectHelper.PerformManualFileSelect(s)),
|
||||
true,
|
||||
_requestCode,
|
||||
protocolId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (resultCode == (Result)FileStorageResults.FileUsagePrepared)
|
||||
{
|
||||
var ioc = new IOConnectionInfo();
|
||||
Util.SetIoConnectionFromIntent(ioc, data);
|
||||
SaveFile(ioc);
|
||||
return true;
|
||||
}
|
||||
if (resultCode == (Result)FileStorageResults.FileChooserPrepared)
|
||||
{
|
||||
IOConnectionInfo ioc = new IOConnectionInfo();
|
||||
Util.SetIoConnectionFromIntent(ioc, data);
|
||||
new FileSelectHelper(_activity, true, true, _requestCode).StartFileChooser(ioc.Path);
|
||||
return true;
|
||||
}
|
||||
if (resultCode == Result.Ok)
|
||||
{
|
||||
if (requestCode == _requestCode)
|
||||
{
|
||||
|
||||
if (data.Data.Scheme == "content")
|
||||
{
|
||||
if ((int)Android.OS.Build.VERSION.SdkInt >= 19)
|
||||
{
|
||||
//try to take persistable permissions
|
||||
try
|
||||
{
|
||||
Kp2aLog.Log("TakePersistableUriPermission");
|
||||
var takeFlags = data.Flags
|
||||
& (ActivityFlags.GrantReadUriPermission
|
||||
| ActivityFlags.GrantWriteUriPermission);
|
||||
_activity.ContentResolver.TakePersistableUriPermission(data.Data, takeFlags);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.Log(e.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
string filename = Util.IntentToFilename(data, _activity);
|
||||
if (filename == null)
|
||||
filename = data.DataString;
|
||||
|
||||
bool fileExists = data.GetBooleanExtra("group.pals.android.lib.ui.filechooser.FileChooserActivity.result_file_exists", true);
|
||||
|
||||
if (fileExists)
|
||||
{
|
||||
SaveFile(new IOConnectionInfo { Path = FileSelectHelper.ConvertFilenameToIocPath(filename) });
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
var task = new CreateNewFilename(_activity, new ActionOnFinish(_activity, (success, messageOrFilename, activity) =>
|
||||
{
|
||||
if (!success)
|
||||
{
|
||||
Toast.MakeText(activity, messageOrFilename, ToastLength.Long).Show();
|
||||
return;
|
||||
}
|
||||
SaveFile(new IOConnectionInfo { Path = FileSelectHelper.ConvertFilenameToIocPath(messageOrFilename) });
|
||||
|
||||
|
||||
}), filename);
|
||||
|
||||
new ProgressTask(App.Kp2a, _activity, task).Run();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected virtual void Clear()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected abstract void SaveFile(IOConnectionInfo ioc);
|
||||
|
||||
public void StartProcess()
|
||||
{
|
||||
Intent intent = new Intent(_activity, typeof(FileStorageSelectionActivity));
|
||||
//intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, true);
|
||||
_activity.StartActivityForResult(intent, _requestCode);
|
||||
}
|
||||
|
||||
public virtual void OnSaveInstanceState(Bundle outState)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
816
src/keepass2android-app/FileSelectHelper.cs
Normal file
@@ -0,0 +1,816 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
#if !NoNet
|
||||
using FluentFTP;
|
||||
#endif
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Google.Android.Material.Dialog;
|
||||
using Java.IO;
|
||||
using keepass2android.Io;
|
||||
using keepass2android;
|
||||
#if !EXCLUDE_JAVAFILESTORAGE
|
||||
using Keepass2android.Javafilestorage;
|
||||
#endif
|
||||
using KeePassLib.Serialization;
|
||||
using KeePassLib.Utility;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public class FileSelectHelper
|
||||
{
|
||||
public static string ConvertFilenameToIocPath(string filename)
|
||||
{
|
||||
if ((filename != null) && (filename.StartsWith("file://")))
|
||||
{
|
||||
filename = filename.Substring(7);
|
||||
filename = Java.Net.URLDecoder.Decode(filename);
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
private readonly Activity _activity;
|
||||
private readonly bool _isForSave;
|
||||
private readonly int _requestCode;
|
||||
private readonly string _schemeSeparator = "://";
|
||||
private bool _tryGetPermanentAccess;
|
||||
|
||||
private const int SftpModeSpinnerPasswd = 0;
|
||||
private const int SftpModeSpinnerPubKey = 1;
|
||||
private const int SftpModeSpinnerCustomKey = 2;
|
||||
|
||||
private const int SftpKeySpinnerCreateNewIdx = 0;
|
||||
|
||||
public string DefaultExtension { get; set; }
|
||||
|
||||
public FileSelectHelper(Activity activity, bool isForSave, bool tryGetPermanentAccess, int requestCode)
|
||||
{
|
||||
_activity = activity;
|
||||
_isForSave = isForSave;
|
||||
_requestCode = requestCode;
|
||||
_tryGetPermanentAccess = tryGetPermanentAccess;
|
||||
}
|
||||
|
||||
private void ShowSftpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath)
|
||||
{
|
||||
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
|
||||
View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.sftpcredentials, null);
|
||||
var ctx = activity.ApplicationContext;
|
||||
var fileStorage = new Keepass2android.Javafilestorage.SftpStorage(ctx);
|
||||
|
||||
LinearLayout addNewBtn = dlgContents.FindViewById<LinearLayout>(Resource.Id.sftp_add_key_group);
|
||||
Button deleteBtn = dlgContents.FindViewById<Button>(Resource.Id.sftp_delete_key_button);
|
||||
EditText keyNameTxt = dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_name);
|
||||
EditText keyContentTxt = dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_content);
|
||||
|
||||
var keySpinner = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_key_names);
|
||||
var keyNamesAdapter = new ArrayAdapter(ctx, Android.Resource.Layout.SimpleSpinnerDropDownItem, new List<string>());
|
||||
UpdatePrivateKeyNames(keyNamesAdapter, fileStorage, ctx);
|
||||
keyNamesAdapter.SetDropDownViewResource(Android.Resource.Layout.SimpleSpinnerDropDownItem);
|
||||
keySpinner.Adapter = keyNamesAdapter;
|
||||
keySpinner.SetSelection(SftpKeySpinnerCreateNewIdx);
|
||||
|
||||
keySpinner.ItemSelected += (sender, args) =>
|
||||
{
|
||||
if (keySpinner.SelectedItemPosition == SftpKeySpinnerCreateNewIdx)
|
||||
{
|
||||
keyNameTxt.Text = "";
|
||||
keyContentTxt.Text = "";
|
||||
addNewBtn.Visibility = ViewStates.Visible;
|
||||
deleteBtn.Visibility = ViewStates.Gone;
|
||||
}
|
||||
else
|
||||
{
|
||||
addNewBtn.Visibility = ViewStates.Gone;
|
||||
deleteBtn.Visibility = ViewStates.Visible;
|
||||
}
|
||||
};
|
||||
|
||||
var authModeSpinner = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_auth_mode_spinner);
|
||||
dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button).Click += (sender, args) =>
|
||||
{
|
||||
string pub_filename = fileStorage.CreateKeyPair();
|
||||
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.SetAction(Intent.ActionSend);
|
||||
sendIntent.PutExtra(Intent.ExtraText, System.IO.File.ReadAllText(pub_filename));
|
||||
|
||||
sendIntent.PutExtra(Intent.ExtraSubject, "Keepass2Android sftp public key");
|
||||
sendIntent.SetType("text/plain");
|
||||
activity.StartActivity(Intent.CreateChooser(sendIntent, "Send public key to..."));
|
||||
};
|
||||
|
||||
dlgContents.FindViewById<Button>(Resource.Id.sftp_save_key_button).Click += (sender, args) =>
|
||||
{
|
||||
string keyName = keyNameTxt.Text;
|
||||
string keyContent = keyContentTxt.Text;
|
||||
|
||||
string toastMsg = null;
|
||||
if (!string.IsNullOrEmpty(keyName) && !string.IsNullOrEmpty(keyContent))
|
||||
{
|
||||
try
|
||||
{
|
||||
fileStorage.SavePrivateKeyContent(keyName, keyContent);
|
||||
keyNameTxt.Text = "";
|
||||
keyContentTxt.Text = "";
|
||||
toastMsg = ctx.GetString(Resource.String.private_key_saved);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
toastMsg = ctx.GetString(Resource.String.private_key_save_failed,
|
||||
new Java.Lang.Object[] { e.Message });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
toastMsg = ctx.GetString(Resource.String.private_key_info);
|
||||
}
|
||||
|
||||
if (toastMsg!= null) {
|
||||
Toast.MakeText(_activity, toastMsg, ToastLength.Long).Show();
|
||||
}
|
||||
|
||||
UpdatePrivateKeyNames(keyNamesAdapter, fileStorage, ctx);
|
||||
keySpinner.SetSelection(ResolveKeySpinnerSelection(keyNamesAdapter, keyName));
|
||||
};
|
||||
|
||||
dlgContents.FindViewById<Button>(Resource.Id.sftp_delete_key_button).Click += (sender, args) =>
|
||||
{
|
||||
int selectedKey = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_key_names).SelectedItemPosition;
|
||||
string keyName = ResolveSelectedKeyName(keyNamesAdapter, selectedKey);
|
||||
if (!string.IsNullOrEmpty(keyName))
|
||||
{
|
||||
bool deleted = fileStorage.DeleteCustomKey(keyName);
|
||||
|
||||
int msgId = deleted ? Resource.String.private_key_delete : Resource.String.private_key_delete_failed;
|
||||
string msg = ctx.GetString(msgId, new Java.Lang.Object[] { keyName });
|
||||
Toast.MakeText(_activity, msg, ToastLength.Long).Show();
|
||||
|
||||
UpdatePrivateKeyNames(keyNamesAdapter, fileStorage, ctx);
|
||||
keySpinner.SetSelection(SftpKeySpinnerCreateNewIdx);
|
||||
}
|
||||
};
|
||||
|
||||
authModeSpinner.ItemSelected += (sender, args) =>
|
||||
{
|
||||
var passwordBox = dlgContents.FindViewById<EditText>(Resource.Id.sftp_password);
|
||||
var publicKeyButton = dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button);
|
||||
var keyfileGroup = dlgContents.FindViewById<LinearLayout>(Resource.Id.sftp_keyfile_group);
|
||||
|
||||
switch (authModeSpinner.SelectedItemPosition)
|
||||
{
|
||||
case SftpModeSpinnerPasswd:
|
||||
passwordBox.Visibility = ViewStates.Visible;
|
||||
publicKeyButton.Visibility = ViewStates.Gone;
|
||||
keyfileGroup.Visibility = ViewStates.Gone;
|
||||
break;
|
||||
case SftpModeSpinnerPubKey:
|
||||
passwordBox.Visibility = ViewStates.Gone;
|
||||
publicKeyButton.Visibility = ViewStates.Visible;
|
||||
keyfileGroup.Visibility = ViewStates.Gone;
|
||||
break;
|
||||
case SftpModeSpinnerCustomKey:
|
||||
passwordBox.Visibility = ViewStates.Gone;
|
||||
publicKeyButton.Visibility = ViewStates.Gone;
|
||||
keyfileGroup.Visibility = ViewStates.Visible;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (!defaultPath.EndsWith(_schemeSeparator))
|
||||
{
|
||||
SftpStorage.ConnectionInfo ci = fileStorage.SplitStringToConnectionInfo(defaultPath);
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_host).Text = ci.Host;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text = ci.Port.ToString();
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text = ci.Username;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text = ci.Password;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_name).Text = ci.KeyName;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_passphrase).Text = ci.KeyPassphrase;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text = ci.LocalPath;
|
||||
if (ci.ConnectTimeoutSec != SftpStorage.UnsetSftpConnectTimeout)
|
||||
{
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_connect_timeout).Text = ci.ConnectTimeoutSec.ToString();
|
||||
}
|
||||
|
||||
if (ci.ConfigOpts.Contains(SftpStorage.SshCfgKex))
|
||||
{
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_kex).Text = ci.ConfigOpts[SftpStorage.SshCfgKex].ToString();
|
||||
}
|
||||
if (ci.ConfigOpts.Contains(SftpStorage.SshCfgServerHostKey))
|
||||
{
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_shk).Text = ci.ConfigOpts[SftpStorage.SshCfgServerHostKey].ToString();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ci.Password))
|
||||
{
|
||||
authModeSpinner.SetSelection(SftpModeSpinnerPasswd);
|
||||
} else if (!string.IsNullOrEmpty(ci.KeyName))
|
||||
{
|
||||
authModeSpinner.SetSelection(SftpModeSpinnerCustomKey);
|
||||
keySpinner.SetSelection(ResolveKeySpinnerSelection(keyNamesAdapter, ci.KeyName));
|
||||
} else
|
||||
{
|
||||
authModeSpinner.SetSelection(SftpModeSpinnerPubKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
builder.SetView(dlgContents);
|
||||
builder.SetPositiveButton(Android.Resource.String.Ok, (sender, args) => {
|
||||
int idx = 0;
|
||||
string host = dlgContents.FindViewById<EditText>(Resource.Id.sftp_host).Text;
|
||||
string portText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text;
|
||||
int port = SftpStorage.DefaultSftpPort;
|
||||
if (!string.IsNullOrEmpty(portText))
|
||||
int.TryParse(portText, out port);
|
||||
string user = dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text;
|
||||
|
||||
string password = null;
|
||||
string keyName = null, keyPassphrase = null;
|
||||
switch (dlgContents.FindViewById<Spinner>(Resource.Id.sftp_auth_mode_spinner).SelectedItemPosition)
|
||||
{
|
||||
case SftpModeSpinnerPasswd:
|
||||
password = dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text;
|
||||
break;
|
||||
case SftpModeSpinnerPubKey:
|
||||
break;
|
||||
case SftpModeSpinnerCustomKey:
|
||||
keyName = ResolveSelectedKeyName(keyNamesAdapter, keySpinner.SelectedItemPosition);
|
||||
keyPassphrase = dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_passphrase).Text;
|
||||
break;
|
||||
}
|
||||
|
||||
string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text;
|
||||
if (string.IsNullOrEmpty(initialPath))
|
||||
initialPath = "/";
|
||||
string connectTimeoutText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_connect_timeout).Text;
|
||||
int connectTimeout = SftpStorage.UnsetSftpConnectTimeout;
|
||||
if (!string.IsNullOrEmpty(connectTimeoutText))
|
||||
{
|
||||
int.TryParse(connectTimeoutText, out connectTimeout);
|
||||
}
|
||||
string kexAlgorithms = dlgContents.FindViewById<EditText>(Resource.Id.sftp_kex).Text;
|
||||
string shkAlgorithms = dlgContents.FindViewById<EditText>(Resource.Id.sftp_shk).Text;
|
||||
|
||||
string sftpPath = fileStorage.BuildFullPath(
|
||||
host, port, initialPath, user, password, connectTimeout, keyName, keyPassphrase,
|
||||
kexAlgorithms, shkAlgorithms);
|
||||
|
||||
onStartBrowse(sftpPath);
|
||||
});
|
||||
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
|
||||
|
||||
builder.SetNegativeButton(Android.Resource.String.Cancel, evtH);
|
||||
builder.SetTitle(activity.GetString(Resource.String.enter_sftp_login_title));
|
||||
Dialog dialog = builder.Create();
|
||||
|
||||
dialog.Show();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
|
||||
private void UpdatePrivateKeyNames(ArrayAdapter dataView, SftpStorage storage, Context ctx)
|
||||
{
|
||||
dataView.Clear();
|
||||
dataView.Add(ctx.GetString(Resource.String.private_key_create_new));
|
||||
foreach (string keyName in storage.GetCustomKeyNames())
|
||||
dataView.Add(keyName);
|
||||
}
|
||||
|
||||
private int ResolveKeySpinnerSelection(ArrayAdapter dataView, string keyName)
|
||||
{
|
||||
int idx = -1;
|
||||
for (int i = 0; i < dataView.Count; i++)
|
||||
{
|
||||
string itemName = dataView.GetItem(i).ToString();
|
||||
if (string.Equals(keyName, itemName)) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return idx < 0 ? SftpKeySpinnerCreateNewIdx : idx;
|
||||
}
|
||||
|
||||
private string ResolveSelectedKeyName(ArrayAdapter dataView, int selectedItem)
|
||||
{
|
||||
if (selectedItem != SftpKeySpinnerCreateNewIdx && selectedItem > 0 && selectedItem < dataView.Count)
|
||||
return dataView.GetItem(selectedItem).ToString();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
#endif
|
||||
|
||||
private void ShowHttpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath)
|
||||
{
|
||||
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
|
||||
View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.httpcredentials, null);
|
||||
if (!defaultPath.EndsWith(_schemeSeparator))
|
||||
{
|
||||
var webdavStorage = new Keepass2android.Javafilestorage.WebDavStorage(App.Kp2a.CertificateErrorHandler);
|
||||
var connInfo = webdavStorage.SplitStringToConnectionInfo(defaultPath);
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.http_url).Text = connInfo.Url;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.http_user).Text = connInfo.Username;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.http_password).Text = connInfo.Password;
|
||||
|
||||
|
||||
}
|
||||
builder.SetView(dlgContents);
|
||||
builder.SetPositiveButton(Android.Resource.String.Ok,
|
||||
(sender, args) =>
|
||||
{
|
||||
string host = dlgContents.FindViewById<EditText>(Resource.Id.http_url).Text;
|
||||
|
||||
string user = dlgContents.FindViewById<EditText>(Resource.Id.http_user).Text;
|
||||
string password = dlgContents.FindViewById<EditText>(Resource.Id.http_password).Text;
|
||||
|
||||
string scheme = defaultPath.Substring(0, defaultPath.IndexOf(_schemeSeparator, StringComparison.Ordinal));
|
||||
if (host.Contains(_schemeSeparator) == false)
|
||||
host = scheme + _schemeSeparator + host;
|
||||
string httpPath = new Keepass2android.Javafilestorage.WebDavStorage(null).BuildFullPath(host, user,
|
||||
password);
|
||||
onStartBrowse(httpPath);
|
||||
});
|
||||
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
|
||||
|
||||
builder.SetNegativeButton(Android.Resource.String.Cancel, evtH);
|
||||
builder.SetTitle(activity.GetString(Resource.String.enter_http_login_title));
|
||||
Dialog dialog = builder.Create();
|
||||
|
||||
dialog.Show();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath)
|
||||
{
|
||||
#if !NoNet
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
|
||||
View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.ftpcredentials, null);
|
||||
if (!defaultPath.EndsWith(_schemeSeparator))
|
||||
{
|
||||
var connection = NetFtpFileStorage.ConnectionSettings.FromIoc(IOConnectionInfo.FromPath(defaultPath));
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.ftp_user).Text = connection.Username;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.ftp_password).Text = connection.Password;
|
||||
dlgContents.FindViewById<Spinner>(Resource.Id.ftp_encryption).SetSelection((int)connection.EncryptionMode);
|
||||
|
||||
var uri = NetFtpFileStorage.IocToUri(IOConnectionInfo.FromPath(defaultPath));
|
||||
string pathAndQuery = uri.PathAndQuery;
|
||||
|
||||
var host = uri.Host;
|
||||
var localPath = WebUtility.UrlDecode(pathAndQuery);
|
||||
|
||||
|
||||
if (!uri.IsDefaultPort)
|
||||
{
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.ftp_port).Text = uri.Port.ToString();
|
||||
}
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.ftp_host).Text = host;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.ftp_initial_dir).Text = localPath;
|
||||
|
||||
|
||||
}
|
||||
builder.SetView(dlgContents);
|
||||
builder.SetPositiveButton(Android.Resource.String.Ok,
|
||||
(sender, args) =>
|
||||
{
|
||||
string host = dlgContents.FindViewById<EditText>(Resource.Id.ftp_host).Text;
|
||||
string portText = dlgContents.FindViewById<EditText>(Resource.Id.ftp_port).Text;
|
||||
FtpEncryptionMode encryption =
|
||||
(FtpEncryptionMode) dlgContents.FindViewById<Spinner>(Resource.Id.ftp_encryption).SelectedItemPosition;
|
||||
int port = NetFtpFileStorage.GetDefaultPort(encryption);
|
||||
if (!string.IsNullOrEmpty(portText))
|
||||
int.TryParse(portText, out port);
|
||||
string user = dlgContents.FindViewById<EditText>(Resource.Id.ftp_user).Text;
|
||||
string password = dlgContents.FindViewById<EditText>(Resource.Id.ftp_password).Text;
|
||||
string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.ftp_initial_dir).Text;
|
||||
string ftpPath = new NetFtpFileStorage(_activity, App.Kp2a, () => false)
|
||||
.BuildFullPath(host, port, initialPath, user, password, encryption);
|
||||
onStartBrowse(ftpPath);
|
||||
});
|
||||
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
|
||||
|
||||
builder.SetNegativeButton(Android.Resource.String.Cancel, evtH);
|
||||
builder.SetTitle(activity.GetString(Resource.String.enter_ftp_login_title));
|
||||
Dialog dialog = builder.Create();
|
||||
|
||||
dialog.Show();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
private void ShowMegaDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath)
|
||||
{
|
||||
#if !NoNet
|
||||
var settings = MegaFileStorage.GetAccountSettings(activity);
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
|
||||
View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.megacredentials, null);
|
||||
if (!defaultPath.EndsWith(_schemeSeparator))
|
||||
{
|
||||
string user = "";
|
||||
string password = "";
|
||||
if (!defaultPath.EndsWith(_schemeSeparator))
|
||||
{
|
||||
user = MegaFileStorage.GetAccount(defaultPath);
|
||||
if (!settings.PasswordByUsername.TryGetValue(user, out password))
|
||||
password = "";
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.mega_user).Enabled = false;
|
||||
|
||||
}
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.mega_user).Text = user;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.mega_password).Text = password;
|
||||
|
||||
}
|
||||
|
||||
var userView = ((AutoCompleteTextView)dlgContents.FindViewById(Resource.Id.mega_user));
|
||||
userView.Adapter = new ArrayAdapter(activity, Android.Resource.Layout.SimpleListItem1, Android.Resource.Id.Text1, settings.PasswordByUsername.Keys.ToArray());
|
||||
|
||||
userView.TextChanged += (sender, args) =>
|
||||
{
|
||||
if (userView.Text != null && settings.PasswordByUsername.TryGetValue(userView.Text, out string pwd))
|
||||
{
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.mega_password).Text = pwd;
|
||||
}
|
||||
};
|
||||
builder.SetCancelable(false);
|
||||
builder.SetView(dlgContents);
|
||||
builder.SetPositiveButton(Android.Resource.String.Ok,
|
||||
(sender, args) =>
|
||||
{
|
||||
string user = dlgContents.FindViewById<EditText>(Resource.Id.mega_user).Text;
|
||||
string password = dlgContents.FindViewById<EditText>(Resource.Id.mega_password).Text;
|
||||
//store the credentials in the mega credentials store:
|
||||
|
||||
|
||||
settings.PasswordByUsername[user] = password;
|
||||
|
||||
MegaFileStorage.UpdateAccountSettings(settings, activity);
|
||||
|
||||
onStartBrowse(MegaFileStorage.ProtocolId + "://" + user);
|
||||
});
|
||||
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
|
||||
|
||||
builder.SetNegativeButton(Android.Resource.String.Cancel, evtH);
|
||||
builder.SetTitle(activity.GetString(Resource.String.enter_mega_login_title));
|
||||
Dialog dialog = builder.Create();
|
||||
|
||||
dialog.Show();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void PerformManualFileSelect(string defaultPath)
|
||||
{
|
||||
if (defaultPath.StartsWith("sftp://"))
|
||||
ShowSftpDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath);
|
||||
else if ((defaultPath.StartsWith("ftp://")) || (defaultPath.StartsWith("ftps://")))
|
||||
ShowFtpDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath);
|
||||
else if ((defaultPath.StartsWith("http://")) || (defaultPath.StartsWith("https://")))
|
||||
ShowHttpDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath);
|
||||
else if (defaultPath.StartsWith("owncloud://"))
|
||||
ShowOwncloudDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath, "owncloud");
|
||||
else if (defaultPath.StartsWith("nextcloud://"))
|
||||
ShowOwncloudDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath, "nextcloud");
|
||||
else if (defaultPath.StartsWith("mega://"))
|
||||
ShowMegaDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath);
|
||||
else
|
||||
{
|
||||
Func<string, Dialog, bool> onOpen = OnOpenButton;
|
||||
Util.ShowFilenameDialog(_activity,
|
||||
!_isForSave ? onOpen : null,
|
||||
_isForSave ? onOpen : null,
|
||||
ReturnCancel,
|
||||
false,
|
||||
defaultPath,
|
||||
_activity.GetString(Resource.String.enter_filename_details_url),
|
||||
_requestCode)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowOwncloudDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath, string subtype)
|
||||
{
|
||||
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
|
||||
View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.owncloudcredentials, null);
|
||||
builder.SetView(dlgContents);
|
||||
|
||||
builder.SetPositiveButton(Android.Resource.String.Ok,
|
||||
(sender, args) =>
|
||||
{
|
||||
string host = dlgContents.FindViewById<EditText>(Resource.Id.owncloud_url).Text;
|
||||
|
||||
string user = dlgContents.FindViewById<EditText>(Resource.Id.http_user).Text;
|
||||
string password = dlgContents.FindViewById<EditText>(Resource.Id.http_password).Text;
|
||||
|
||||
string scheme = defaultPath.Substring(0,defaultPath.IndexOf(_schemeSeparator, StringComparison.Ordinal));
|
||||
if (host.Contains(_schemeSeparator) == false)
|
||||
host = scheme + _schemeSeparator + host;
|
||||
string httpPath = new Keepass2android.Javafilestorage.WebDavStorage(null).BuildFullPath(WebDavFileStorage.Owncloud2Webdav(host, subtype == "owncloud" ? WebDavFileStorage.owncloudPrefix : WebDavFileStorage.nextcloudPrefix), user,
|
||||
password);
|
||||
onStartBrowse(httpPath);
|
||||
});
|
||||
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
|
||||
|
||||
builder.SetNegativeButton(Android.Resource.String.Cancel, evtH);
|
||||
builder.SetTitle(activity.GetString(subtype == "owncloud" ? Resource.String.enter_owncloud_login_title : Resource.String.enter_nextcloud_login_title));
|
||||
Dialog dialog = builder.Create();
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.owncloud_url).SetHint(subtype == "owncloud" ? Resource.String.hint_owncloud_url : Resource.String.hint_nextcloud_url);
|
||||
dialog.Show();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
private bool ReturnFileOrStartFileChooser(string filename)
|
||||
{
|
||||
string filenameWithoutProt = filename;
|
||||
if (filenameWithoutProt.Contains(_schemeSeparator))
|
||||
{
|
||||
filenameWithoutProt =
|
||||
filenameWithoutProt.Substring(filenameWithoutProt.IndexOf(_schemeSeparator, StringComparison.Ordinal) + 3);
|
||||
}
|
||||
|
||||
int lastSlashPos = filenameWithoutProt.LastIndexOf('/');
|
||||
int lastDotPos = filenameWithoutProt.LastIndexOf('.');
|
||||
if ((lastSlashPos < 0 ) //no slash, probably only a server address (my.server.com)
|
||||
|| (lastSlashPos >= lastDotPos)) //no dot after last slash or == in case neither / nor .
|
||||
{
|
||||
//looks like a folder.
|
||||
return StartFileChooser(filename);
|
||||
}
|
||||
//looks like a file
|
||||
IocSelected(null, IOConnectionInfo.FromPath(filename));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ReturnCancel()
|
||||
{
|
||||
if (OnCancel != null)
|
||||
OnCancel(this, null);
|
||||
}
|
||||
|
||||
|
||||
protected void ShowFilenameWarning(string fileName, Action onUserWantsToContinue, Action onUserWantsToCorrect)
|
||||
{
|
||||
new MaterialAlertDialogBuilder(_activity)
|
||||
.SetPositiveButton(Resource.String.Continue, delegate { onUserWantsToContinue(); })
|
||||
.SetMessage(Resource.String.NoFilenameWarning)
|
||||
.SetCancelable(false)
|
||||
.SetNegativeButton(Android.Resource.String.Cancel, delegate { onUserWantsToCorrect(); })
|
||||
.Create()
|
||||
.Show();
|
||||
|
||||
}
|
||||
private bool OnOpenButton(string filename, Dialog dialog)
|
||||
{
|
||||
|
||||
IOConnectionInfo ioc = new IOConnectionInfo
|
||||
{
|
||||
Path = filename
|
||||
};
|
||||
|
||||
// Make sure file name exists
|
||||
if (filename.Length == 0)
|
||||
{
|
||||
Toast.MakeText(_activity,
|
||||
Resource.String.error_filename_required,
|
||||
ToastLength.Long).Show();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
int lastSlashPos = filename.LastIndexOf('/');
|
||||
int lastDotPos = filename.LastIndexOf('.');
|
||||
if (lastSlashPos >= lastDotPos) //no dot after last slash or == in case neither / nor .
|
||||
{
|
||||
ShowFilenameWarning(filename, () => { IocSelected(null, ioc); dialog.Dismiss(); }, () => { /* don't do anything, leave dialog open, let user try again*/ });
|
||||
//signal that the dialog should be kept open
|
||||
return false;
|
||||
}
|
||||
|
||||
IFileStorage fileStorage;
|
||||
try
|
||||
{
|
||||
fileStorage = App.Kp2a.GetFileStorage(ioc);
|
||||
}
|
||||
catch (NoFileStorageFoundException)
|
||||
{
|
||||
Toast.MakeText(_activity,
|
||||
"Unexpected scheme in "+filename,
|
||||
ToastLength.Long).Show();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_isForSave && ioc.IsLocalFile())
|
||||
{
|
||||
// Try to create the file
|
||||
Java.IO.File file = new Java.IO.File(filename);
|
||||
try
|
||||
{
|
||||
Java.IO.File parent = file.ParentFile;
|
||||
|
||||
if (parent == null || (parent.Exists() && !parent.IsDirectory))
|
||||
{
|
||||
Toast.MakeText(_activity,
|
||||
Resource.String.error_invalid_path,
|
||||
ToastLength.Long).Show();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parent.Exists())
|
||||
{
|
||||
// Create parent dircetory
|
||||
if (!parent.Mkdirs())
|
||||
{
|
||||
Toast.MakeText(_activity,
|
||||
Resource.String.error_could_not_create_parent,
|
||||
ToastLength.Long).Show();
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
System.IO.File.Create(filename).Dispose();
|
||||
|
||||
}
|
||||
catch (Java.IO.IOException ex)
|
||||
{
|
||||
Toast.MakeText(
|
||||
_activity,
|
||||
_activity.GetText(Resource.String.error_file_not_create) + " "
|
||||
+ ex.LocalizedMessage,
|
||||
ToastLength.Long).Show();
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
if (fileStorage.RequiresCredentials(ioc))
|
||||
{
|
||||
Util.QueryCredentials(ioc, iocResult => IocSelected(null, iocResult), _activity);
|
||||
}
|
||||
else
|
||||
{
|
||||
IocSelected(null, ioc);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void IocSelected(Activity activity, IOConnectionInfo ioc)
|
||||
{
|
||||
if (OnOpen != null)
|
||||
OnOpen(activity, ioc);
|
||||
}
|
||||
|
||||
public bool StartFileChooser(string defaultPath)
|
||||
{
|
||||
#if !EXCLUDE_FILECHOOSER
|
||||
string fileProviderAuthority = FileChooserFileProvider.TheAuthority;
|
||||
if (defaultPath.StartsWith("file://"))
|
||||
{
|
||||
fileProviderAuthority = _activity.PackageName + ".android-filechooser.localfile";
|
||||
}
|
||||
Intent i = Keepass2android.Kp2afilechooser.Kp2aFileChooserBridge.GetLaunchFileChooserIntent(_activity, fileProviderAuthority,
|
||||
defaultPath);
|
||||
|
||||
|
||||
if (_isForSave)
|
||||
{
|
||||
i.PutExtra("group.pals.android.lib.ui.filechooser.FileChooserActivity.save_dialog", true);
|
||||
string ext;
|
||||
if (!string.IsNullOrEmpty(DefaultExtension))
|
||||
{
|
||||
ext = DefaultExtension;
|
||||
}
|
||||
else
|
||||
{
|
||||
ext = UrlUtil.GetExtension(defaultPath);
|
||||
}
|
||||
if ((ext != String.Empty) && (ext.Contains("?") == false))
|
||||
i.PutExtra("group.pals.android.lib.ui.filechooser.FileChooserActivity.default_file_ext", ext);
|
||||
}
|
||||
_activity.StartActivityForResult(i, _requestCode);
|
||||
|
||||
#else
|
||||
Toast.MakeText(LocaleManager.LocalizedAppContext, "File chooser is excluded!", ToastLength.Long).Show();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
public event EventHandler OnCancel;
|
||||
|
||||
public event EventHandler<IOConnectionInfo> OnOpen;
|
||||
|
||||
public static bool CanEditIoc(IOConnectionInfo ioc)
|
||||
{
|
||||
return ioc.Path.StartsWith("http")
|
||||
|| ioc.Path.StartsWith("ftp")
|
||||
|| ioc.Path.StartsWith("sftp")
|
||||
|| ioc.Path.StartsWith("mega");
|
||||
|
||||
}
|
||||
|
||||
public bool HandleActivityResult(Activity activity, int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
if (requestCode != _requestCode)
|
||||
return false;
|
||||
|
||||
if (resultCode == KeePass.ExitFileStorageSelectionOk)
|
||||
{
|
||||
string protocolId = data.GetStringExtra("protocolId");
|
||||
if (protocolId == "content")
|
||||
{
|
||||
Util.ShowBrowseDialog(activity, _requestCode, _isForSave, _tryGetPermanentAccess);
|
||||
}
|
||||
else
|
||||
{
|
||||
App.Kp2a.GetFileStorage(protocolId).StartSelectFile(
|
||||
new FileStorageSetupInitiatorActivity(activity,(i, result, arg3) =>HandleActivityResult(activity, i, result, arg3),s => PerformManualFileSelect(s)),
|
||||
_isForSave,
|
||||
_requestCode,
|
||||
protocolId);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultCode == Result.Ok)
|
||||
{
|
||||
|
||||
if (data.Data.Scheme == "content")
|
||||
{
|
||||
if ((int)Build.VERSION.SdkInt >= 19)
|
||||
{
|
||||
//try to take persistable permissions
|
||||
try
|
||||
{
|
||||
Kp2aLog.Log("TakePersistableUriPermission");
|
||||
var takeFlags = data.Flags
|
||||
& (ActivityFlags.GrantReadUriPermission
|
||||
| ActivityFlags.GrantWriteUriPermission);
|
||||
activity.ContentResolver.TakePersistableUriPermission(data.Data, takeFlags);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.Log(e.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
string filename = Util.IntentToFilename(data, activity);
|
||||
if (filename == null)
|
||||
filename = data.DataString;
|
||||
|
||||
bool fileExists = data.GetBooleanExtra("group.pals.android.lib.ui.filechooser.FileChooserActivity.result_file_exists", true);
|
||||
|
||||
if (fileExists)
|
||||
{
|
||||
var ioc = new IOConnectionInfo { Path = ConvertFilenameToIocPath(filename) };
|
||||
IocSelected(activity,ioc);
|
||||
}
|
||||
else
|
||||
{
|
||||
var task = new CreateNewFilename(activity, new ActionOnFinish(activity, (success, messageOrFilename, newActivity) =>
|
||||
{
|
||||
if (!success)
|
||||
{
|
||||
Toast.MakeText(newActivity, messageOrFilename, ToastLength.Long).Show();
|
||||
return;
|
||||
}
|
||||
var ioc = new IOConnectionInfo { Path = ConvertFilenameToIocPath(messageOrFilename) };
|
||||
IocSelected(newActivity, ioc);
|
||||
|
||||
}), filename);
|
||||
|
||||
new ProgressTask(App.Kp2a, activity, task).Run();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (resultCode == (Result)FileStorageResults.FileUsagePrepared)
|
||||
{
|
||||
var ioc = new IOConnectionInfo();
|
||||
Util.SetIoConnectionFromIntent(ioc, data);
|
||||
IocSelected(null, ioc);
|
||||
}
|
||||
if (resultCode == (Result)FileStorageResults.FileChooserPrepared )
|
||||
{
|
||||
IOConnectionInfo ioc = new IOConnectionInfo();
|
||||
Util.SetIoConnectionFromIntent(ioc, data);
|
||||
StartFileChooser(ioc.Path);
|
||||
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
276
src/keepass2android-app/FileStorageSelectionActivity.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Android;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Content.Res;
|
||||
using Android.Graphics;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.OS;
|
||||
using Android.Text;
|
||||
using Android.Util;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Google.Android.Material.Dialog;
|
||||
using keepass2android.Io;
|
||||
using keepass2android.view;
|
||||
using AlertDialog = Android.App.AlertDialog;
|
||||
using Object = Java.Lang.Object;
|
||||
using Resource = keepass2android.Resource;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden, Theme = "@style/Kp2aTheme_BlueNoActionBar")]
|
||||
public class FileStorageSelectionActivity : AndroidX.AppCompat.App.AppCompatActivity
|
||||
{
|
||||
private readonly ActivityDesign _design;
|
||||
|
||||
private FileStorageAdapter _fileStorageAdapter;
|
||||
private const int RequestExternalStoragePermission = 1;
|
||||
|
||||
public FileStorageSelectionActivity()
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
public const string AllowThirdPartyAppGet = "AllowThirdPartyAppGet";
|
||||
public const string AllowThirdPartyAppSend = "AllowThirdPartyAppSend";
|
||||
|
||||
class FileStorageAdapter: BaseAdapter
|
||||
{
|
||||
|
||||
private readonly FileStorageSelectionActivity _context;
|
||||
|
||||
private readonly List<string> _displayedProtocolIds = new List<string>();
|
||||
|
||||
public FileStorageAdapter(FileStorageSelectionActivity context)
|
||||
{
|
||||
_context = context;
|
||||
//show all supported protocols:
|
||||
foreach (IFileStorage fs in App.Kp2a.FileStorages)
|
||||
_displayedProtocolIds.AddRange(fs.SupportedProtocols);
|
||||
|
||||
//this is there for legacy reasons, new protocol is onedrive
|
||||
_displayedProtocolIds.Remove("skydrive");
|
||||
|
||||
//onedrive was replaced by onedrive2 in a later implementation, but we still have the previous implementation to open existing connections (without the need to re-authenticate etc.)
|
||||
_displayedProtocolIds.Remove("onedrive");
|
||||
|
||||
|
||||
|
||||
//special handling for local files:
|
||||
if (!Util.IsKitKatOrLater)
|
||||
{
|
||||
//put file:// to the top
|
||||
_displayedProtocolIds.Remove("file");
|
||||
_displayedProtocolIds.Insert(0, "file");
|
||||
|
||||
//remove "content" (covered by androidget)
|
||||
//On KitKat, content is handled by AndroidContentStorage taking advantage
|
||||
//of persistable permissions and ACTION_OPEN/CREATE_DOCUMENT
|
||||
_displayedProtocolIds.Remove("content");
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
_displayedProtocolIds.Remove("file");
|
||||
}
|
||||
|
||||
|
||||
//starting with Android 11, we don't show the Third party app option. Due to restricted permissions,
|
||||
//this no longer works.
|
||||
if ((int)Build.VERSION.SdkInt < 30)
|
||||
if (context.Intent.GetBooleanExtra(AllowThirdPartyAppGet, false))
|
||||
_displayedProtocolIds.Add("androidget");
|
||||
if (context.Intent.GetBooleanExtra(AllowThirdPartyAppSend, false))
|
||||
_displayedProtocolIds.Add("androidsend");
|
||||
#if NoNet
|
||||
//don't display "get regular version", is classified as deceptive ad by Google. Haha.
|
||||
//_displayedProtocolIds.Add("kp2a");
|
||||
#endif
|
||||
_displayedProtocolIds = _displayedProtocolIds.GroupBy(p => App.Kp2a.GetStorageMainTypeDisplayName(p))
|
||||
.Select(g => string.Join(",", g)).ToList();
|
||||
|
||||
}
|
||||
|
||||
public override Object GetItem(int position)
|
||||
{
|
||||
return _displayedProtocolIds[position];
|
||||
}
|
||||
|
||||
public override long GetItemId(int position)
|
||||
{
|
||||
return position;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static float convertDpToPixel(float dp, Context context)
|
||||
{
|
||||
return Util.convertDpToPixel(dp, context);
|
||||
}
|
||||
|
||||
|
||||
public override View GetView(int position, View convertView, ViewGroup parent)
|
||||
{
|
||||
|
||||
Button btn;
|
||||
|
||||
if (convertView == null)
|
||||
{ // if it's not recycled, initialize some attributes
|
||||
|
||||
btn = new Button(_context);
|
||||
btn.LayoutParameters = new GridView.LayoutParams((int)convertDpToPixel(90, _context), (int)convertDpToPixel(110, _context));
|
||||
btn.SetBackgroundResource(Resource.Drawable.storagetype_button_bg);
|
||||
btn.SetPadding((int)convertDpToPixel(4, _context),
|
||||
(int)convertDpToPixel(20, _context),
|
||||
(int)convertDpToPixel(4, _context),
|
||||
(int)convertDpToPixel(4, _context));
|
||||
btn.SetTextSize(ComplexUnitType.Sp, 11);
|
||||
btn.SetTextColor(new Color(115, 115, 115));
|
||||
btn.SetSingleLine(false);
|
||||
btn.Gravity = GravityFlags.Center;
|
||||
btn.Click += (sender, args) => _context.OnItemSelected( (string) ((Button)sender).Tag);
|
||||
}
|
||||
else
|
||||
{
|
||||
btn = (Button)convertView;
|
||||
}
|
||||
|
||||
var protocolId = _displayedProtocolIds[position];
|
||||
btn.Tag = protocolId;
|
||||
|
||||
string firstProtocolInList = protocolId.Split(",").First();
|
||||
|
||||
|
||||
Drawable drawable = App.Kp2a.GetStorageIcon(firstProtocolInList);
|
||||
|
||||
String title =
|
||||
protocolId == "kp2a" ? App.Kp2a.GetResourceString("get_regular_version")
|
||||
:
|
||||
App.Kp2a.GetStorageMainTypeDisplayName(firstProtocolInList);
|
||||
var str = new SpannableString(title);
|
||||
|
||||
btn.TextFormatted = str;
|
||||
//var drawable = ContextCompat.GetDrawable(context, Resource.Drawable.Icon);
|
||||
btn.SetCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
public override int Count
|
||||
{
|
||||
get { return _displayedProtocolIds.Count; }
|
||||
}
|
||||
}
|
||||
|
||||
private void OnItemSelected(string protocolId)
|
||||
{
|
||||
if (protocolId == "kp2a")
|
||||
{
|
||||
//send user to market page of regular edition to get more protocols
|
||||
Util.GotoUrl(this, GetString(Resource.String.MarketURL) + "keepass2android.keepass2android");
|
||||
return;
|
||||
}
|
||||
|
||||
if (protocolId.Contains(","))
|
||||
{
|
||||
//bring up a selection dialog to select the variant of the file storage
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
|
||||
|
||||
builder.SetItems(protocolId.Split(",").Select(singleProtocolId => App.Kp2a.GetStorageDisplayName(singleProtocolId)).ToArray(),
|
||||
delegate(object sender, DialogClickEventArgs args)
|
||||
{
|
||||
string[] singleProtocolIds = protocolId.Split(",");
|
||||
OnItemSelected(singleProtocolIds[args.Which]);
|
||||
});
|
||||
builder.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
var field = typeof(Resource.String).GetField("filestoragehelp_" + protocolId);
|
||||
if (field == null)
|
||||
{
|
||||
//no help available
|
||||
ReturnProtocol(protocolId);
|
||||
}
|
||||
else
|
||||
{
|
||||
//set help:
|
||||
string help = GetString((int)field.GetValue(null));
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.SetTitle(GetString(Resource.String.app_name))
|
||||
.SetMessage(help)
|
||||
.SetPositiveButton(Android.Resource.String.Ok, (sender, args) => ReturnProtocol(protocolId))
|
||||
.Create()
|
||||
.Show();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void ReturnProtocol(string protocolId)
|
||||
{
|
||||
if ((protocolId == "androidget") && ((int) Build.VERSION.SdkInt >= 23) &&
|
||||
( CheckSelfPermission(Manifest.Permission.WriteExternalStorage) != Permission.Granted))
|
||||
{
|
||||
RequestPermissions(new string[]{Manifest.Permission.ReadExternalStorage, Manifest.Permission.WriteExternalStorage},RequestExternalStoragePermission);
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent();
|
||||
intent.PutExtra("protocolId", protocolId);
|
||||
SetResult(KeePass.ExitFileStorageSelectionOk, intent);
|
||||
Finish();
|
||||
}
|
||||
|
||||
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
|
||||
{
|
||||
if ((requestCode == RequestExternalStoragePermission) && (grantResults[0] == Permission.Granted))
|
||||
{
|
||||
ReturnProtocol("androidget");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
_design.ApplyTheme();
|
||||
base.OnCreate(bundle);
|
||||
|
||||
|
||||
SetContentView(Resource.Layout.filestorage_selection);
|
||||
|
||||
var toolbar = FindViewById<AndroidX.AppCompat.Widget.Toolbar>(Resource.Id.mytoolbar);
|
||||
|
||||
SetSupportActionBar(toolbar);
|
||||
|
||||
SupportActionBar.Title = RemoveTrailingColon(GetString(Resource.String.select_storage_type));
|
||||
|
||||
SupportActionBar.SetDisplayHomeAsUpEnabled(true);
|
||||
SupportActionBar.SetDisplayShowHomeEnabled(true);
|
||||
toolbar.NavigationClick += (sender, args) => OnBackPressed();
|
||||
|
||||
_fileStorageAdapter = new FileStorageAdapter(this);
|
||||
var gridView = FindViewById<GridView>(Resource.Id.gridview);
|
||||
gridView.ItemClick +=
|
||||
(sender, args) => OnItemSelected((string)_fileStorageAdapter.GetItem(args.Position));
|
||||
gridView.Adapter = _fileStorageAdapter;
|
||||
|
||||
}
|
||||
|
||||
private string RemoveTrailingColon(string str)
|
||||
{
|
||||
if (str.EndsWith(":"))
|
||||
return str.Substring(0, str.Length - 1);
|
||||
return str;
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
_design.ReapplyTheme();
|
||||
}
|
||||
}
|
||||
}
|
344
src/keepass2android-app/FingerprintSetupActivity.cs
Normal file
@@ -0,0 +1,344 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Hardware.Fingerprints;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Google.Android.Material.Dialog;
|
||||
using Java.Lang;
|
||||
using keepass2android;
|
||||
using KeePassLib.Keys;
|
||||
using KeePassLib.Utility;
|
||||
using Enum = System.Enum;
|
||||
using Exception = System.Exception;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name",
|
||||
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden,
|
||||
Theme = "@style/Kp2aTheme_ActionBar", MainLauncher = false, Exported = true)]
|
||||
[IntentFilter(new[] { "kp2a.action.FingerprintSetupActivity" }, Categories = new[] { Intent.CategoryDefault })]
|
||||
public class BiometricSetupActivity : LockCloseActivity, IBiometricAuthCallback
|
||||
{
|
||||
private readonly ActivityDesign _activityDesign;
|
||||
|
||||
public BiometricSetupActivity(IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
_activityDesign = new ActivityDesign(this);
|
||||
}
|
||||
public BiometricSetupActivity()
|
||||
{
|
||||
_activityDesign = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private FingerprintUnlockMode _unlockMode = FingerprintUnlockMode.Disabled;
|
||||
private FingerprintUnlockMode _desiredUnlockMode;
|
||||
private BiometricEncryption _enc;
|
||||
private RadioButton[] _radioButtons;
|
||||
public override bool OnOptionsItemSelected(IMenuItem item)
|
||||
{
|
||||
switch (item.ItemId)
|
||||
{
|
||||
|
||||
case Android.Resource.Id.Home:
|
||||
Finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
_activityDesign.ApplyTheme();
|
||||
base.OnCreate(savedInstanceState);
|
||||
SetContentView(Resource.Layout.fingerprint_setup);
|
||||
|
||||
Enum.TryParse(
|
||||
PreferenceManager.GetDefaultSharedPreferences(this).GetString(App.Kp2a.CurrentDb.CurrentFingerprintModePrefKey, ""),
|
||||
out _unlockMode);
|
||||
|
||||
_fpIcon = FindViewById<ImageView>(Resource.Id.fingerprint_icon);
|
||||
_fpTextView = FindViewById<TextView>(Resource.Id.fingerprint_status);
|
||||
|
||||
|
||||
SupportActionBar.SetDisplayHomeAsUpEnabled(true);
|
||||
SupportActionBar.SetHomeButtonEnabled(true);
|
||||
|
||||
int[] radioButtonIds =
|
||||
{
|
||||
Resource.Id.radio_fingerprint_quickunlock, Resource.Id.radio_fingerprint_unlock,
|
||||
Resource.Id.radio_fingerprint_disabled
|
||||
};
|
||||
_radioButtons = radioButtonIds.Select(FindViewById<RadioButton>).ToArray();
|
||||
_radioButtons[0].Tag = FingerprintUnlockMode.QuickUnlock.ToString();
|
||||
_radioButtons[1].Tag = FingerprintUnlockMode.FullUnlock.ToString();
|
||||
_radioButtons[2].Tag = FingerprintUnlockMode.Disabled.ToString();
|
||||
foreach (RadioButton r in _radioButtons)
|
||||
{
|
||||
r.CheckedChange += (sender, args) =>
|
||||
{
|
||||
var rbSender = ((RadioButton) sender);
|
||||
if (!rbSender.Checked) return;
|
||||
foreach (RadioButton rOther in _radioButtons)
|
||||
{
|
||||
if (rOther == sender) continue;
|
||||
rOther.Checked = false;
|
||||
}
|
||||
FingerprintUnlockMode newMode;
|
||||
Enum.TryParse(rbSender.Tag.ToString(), out newMode);
|
||||
ChangeUnlockMode(_unlockMode, newMode);
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
CheckCurrentRadioButton();
|
||||
|
||||
int errorId = Resource.String.fingerprint_os_error;
|
||||
SetError(errorId);
|
||||
|
||||
FindViewById(Resource.Id.cancel_button).Click += (sender, args) =>
|
||||
{
|
||||
_enc.StopListening();
|
||||
_unlockMode = FingerprintUnlockMode.Disabled; //cancelling a FingerprintEncryption means a new key has been created but not been authenticated to encrypt something. We can't keep the previous state.
|
||||
StoreUnlockMode();
|
||||
FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible;
|
||||
FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone;
|
||||
_enc = null;
|
||||
CheckCurrentRadioButton();
|
||||
};
|
||||
|
||||
FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Gone;
|
||||
FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone;
|
||||
|
||||
FindViewById<CheckBox>(Resource.Id.close_database_after_failed).Checked =
|
||||
Util.GetCloseDatabaseAfterFailedBiometricQuickUnlock(this);
|
||||
|
||||
FindViewById<CheckBox>(Resource.Id.close_database_after_failed).CheckedChange += (sender, args) =>
|
||||
{
|
||||
PreferenceManager.GetDefaultSharedPreferences(this)
|
||||
.Edit()
|
||||
.PutBoolean(GetString(Resource.String.CloseDatabaseAfterFailedBiometricQuickUnlock_key), args.IsChecked)
|
||||
.Commit();
|
||||
};
|
||||
|
||||
|
||||
UpdateCloseDatabaseAfterFailedBiometricQuickUnlockVisibility();
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void UpdateCloseDatabaseAfterFailedBiometricQuickUnlockVisibility()
|
||||
{
|
||||
FindViewById(Resource.Id.close_database_after_failed).Visibility = _unlockMode == FingerprintUnlockMode.QuickUnlock ? ViewStates.Visible : ViewStates.Gone;
|
||||
}
|
||||
|
||||
string CurrentPreferenceKey
|
||||
{
|
||||
get { return App.Kp2a.CurrentDb.CurrentFingerprintPrefKey; }
|
||||
}
|
||||
|
||||
private void StoreUnlockMode()
|
||||
{
|
||||
ISharedPreferencesEditor edit = PreferenceManager.GetDefaultSharedPreferences(this).Edit();
|
||||
if (_unlockMode == FingerprintUnlockMode.Disabled)
|
||||
{
|
||||
edit.PutString(CurrentPreferenceKey, "");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_unlockMode == FingerprintUnlockMode.FullUnlock)
|
||||
{
|
||||
var userKey = App.Kp2a.CurrentDb.KpDatabase.MasterKey.GetUserKey<KcpPassword>();
|
||||
_enc.StoreEncrypted(userKey != null ? userKey.Password.ReadString() : "", CurrentPreferenceKey, edit);
|
||||
}
|
||||
else
|
||||
_enc.StoreEncrypted("QuickUnlock" /*some dummy data*/, CurrentPreferenceKey, edit);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.SetTitle(GetString(Resource.String.ErrorOcurred))
|
||||
.SetMessage(GetString(Resource.String.FingerprintSetupFailed))
|
||||
.SetCancelable(false)
|
||||
.SetPositiveButton(Android.Resource.String.Ok, (sender, args) => { })
|
||||
.Show();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
edit.PutString(App.Kp2a.CurrentDb.CurrentFingerprintModePrefKey, _unlockMode.ToString());
|
||||
edit.Commit();
|
||||
}
|
||||
|
||||
private void CheckCurrentRadioButton()
|
||||
{
|
||||
|
||||
foreach (RadioButton r in _radioButtons)
|
||||
{
|
||||
FingerprintUnlockMode um;
|
||||
Enum.TryParse(r.Tag.ToString(), out um);
|
||||
if (um == _unlockMode)
|
||||
r.Checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetError(int errorId)
|
||||
{
|
||||
var tv = FindViewById<TextView>(Resource.Id.tvFatalError);
|
||||
tv.Text = GetString(Resource.String.fingerprint_fatal) + " " + GetString(errorId);
|
||||
tv.Visibility = ViewStates.Visible;
|
||||
}
|
||||
|
||||
|
||||
private void ShowRadioButtons()
|
||||
{
|
||||
FindViewById<TextView>(Resource.Id.tvFatalError).Visibility = ViewStates.Gone;
|
||||
FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible;
|
||||
FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone;
|
||||
}
|
||||
|
||||
private void HideRadioButtons()
|
||||
{
|
||||
FindViewById<TextView>(Resource.Id.tvFatalError).Visibility = ViewStates.Gone;
|
||||
FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Gone;
|
||||
FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone;
|
||||
}
|
||||
|
||||
|
||||
private void ChangeUnlockMode(FingerprintUnlockMode oldMode, FingerprintUnlockMode newMode)
|
||||
{
|
||||
if (oldMode == newMode)
|
||||
return;
|
||||
|
||||
|
||||
if (newMode == FingerprintUnlockMode.Disabled)
|
||||
{
|
||||
_unlockMode = newMode;
|
||||
UpdateCloseDatabaseAfterFailedBiometricQuickUnlockVisibility();
|
||||
|
||||
StoreUnlockMode();
|
||||
return;
|
||||
}
|
||||
|
||||
_desiredUnlockMode = newMode;
|
||||
FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Gone;
|
||||
UpdateCloseDatabaseAfterFailedBiometricQuickUnlockVisibility();
|
||||
|
||||
FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Visible;
|
||||
try
|
||||
{
|
||||
_enc = new BiometricEncryption(new BiometricModule(this), CurrentPreferenceKey);
|
||||
if (!_enc.Init())
|
||||
throw new Exception("Failed to initialize cipher");
|
||||
ResetErrorTextRunnable();
|
||||
|
||||
_enc.StartListening(new BiometricAuthCallbackAdapter(this, this));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CheckCurrentRadioButton();
|
||||
Toast.MakeText(this, e.ToString(), ToastLength.Long).Show();
|
||||
FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible;
|
||||
FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
static readonly long ERROR_TIMEOUT_MILLIS = 1600;
|
||||
static readonly long SUCCESS_DELAY_MILLIS = 1300;
|
||||
private ImageView _fpIcon;
|
||||
private TextView _fpTextView;
|
||||
|
||||
public void OnBiometricAuthSucceeded()
|
||||
{
|
||||
_unlockMode = _desiredUnlockMode;
|
||||
|
||||
_fpTextView.RemoveCallbacks(ResetErrorTextRunnable);
|
||||
_fpIcon.SetImageResource(Resource.Drawable.ic_fingerprint_success);
|
||||
_fpTextView.SetTextColor(_fpTextView.Resources.GetColor(Resource.Color.md_theme_secondary, null));
|
||||
_fpTextView.Text = _fpTextView.Resources.GetString(Resource.String.fingerprint_success);
|
||||
_fpIcon.PostDelayed(() =>
|
||||
{
|
||||
FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible;
|
||||
FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone;
|
||||
|
||||
StoreUnlockMode();
|
||||
UpdateCloseDatabaseAfterFailedBiometricQuickUnlockVisibility();
|
||||
|
||||
|
||||
}, SUCCESS_DELAY_MILLIS);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void OnBiometricError(string error)
|
||||
{
|
||||
_fpIcon.SetImageResource(Resource.Drawable.ic_fingerprint_error);
|
||||
_fpTextView.Text = error;
|
||||
_fpTextView.SetTextColor(
|
||||
_fpTextView.Resources.GetColor(Resource.Color.md_theme_error, null));
|
||||
_fpTextView.RemoveCallbacks(ResetErrorTextRunnable);
|
||||
_fpTextView.PostDelayed(ResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
|
||||
}
|
||||
|
||||
public void OnBiometricAttemptFailed(string message)
|
||||
{
|
||||
//ignore
|
||||
}
|
||||
|
||||
void ResetErrorTextRunnable()
|
||||
{
|
||||
_fpTextView.SetTextColor(
|
||||
_fpTextView.Resources.GetColor(Resource.Color.md_theme_secondary, null));
|
||||
_fpTextView.Text = "";
|
||||
_fpIcon.SetImageResource(Resource.Drawable.baseline_fingerprint_24);
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
|
||||
BiometricModule fpModule = new BiometricModule(this);
|
||||
HideRadioButtons();
|
||||
if (!fpModule.IsHardwareAvailable)
|
||||
{
|
||||
SetError(Resource.String.fingerprint_hardware_error);
|
||||
UpdateCloseDatabaseAfterFailedBiometricQuickUnlockVisibility();
|
||||
return;
|
||||
}
|
||||
if (!fpModule.IsAvailable)
|
||||
{
|
||||
SetError(Resource.String.fingerprint_no_enrolled);
|
||||
return;
|
||||
}
|
||||
ShowRadioButtons();
|
||||
UpdateCloseDatabaseAfterFailedBiometricQuickUnlockVisibility();
|
||||
|
||||
}
|
||||
|
||||
protected override void OnPause()
|
||||
{
|
||||
base.OnPause();
|
||||
if (_enc != null)
|
||||
_enc.StopListening();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
9
src/keepass2android-app/FingerprintUnlockMode.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace keepass2android
|
||||
{
|
||||
public enum FingerprintUnlockMode
|
||||
{
|
||||
Disabled = 0,
|
||||
QuickUnlock = 1,
|
||||
FullUnlock = 2,
|
||||
}
|
||||
}
|
89
src/keepass2android-app/FixedDrawerLayout.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using Android.Content;
|
||||
using Android.Graphics;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Util;
|
||||
using AndroidX.DrawerLayout.Widget;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public class FixedDrawerLayout : DrawerLayout
|
||||
{
|
||||
private bool _fitsSystemWindows;
|
||||
|
||||
protected FixedDrawerLayout(IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
}
|
||||
|
||||
public FixedDrawerLayout(Context context, IAttributeSet attrs, int defStyle)
|
||||
: base(context, attrs, defStyle)
|
||||
{
|
||||
}
|
||||
|
||||
public FixedDrawerLayout(Context context, IAttributeSet attrs)
|
||||
: base(context, attrs)
|
||||
{
|
||||
}
|
||||
|
||||
public FixedDrawerLayout(Context context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
||||
private int[] mInsets = new int[4];
|
||||
|
||||
protected override bool FitSystemWindows(Rect insets)
|
||||
{
|
||||
if (Util.IsKitKatOrLater)
|
||||
{
|
||||
// Intentionally do not modify the bottom inset. For some reason,
|
||||
// if the bottom inset is modified, window resizing stops working.
|
||||
// TODO: Figure out why.
|
||||
|
||||
mInsets[0] = insets.Left;
|
||||
mInsets[1] = insets.Top;
|
||||
mInsets[2] = insets.Right;
|
||||
|
||||
insets.Left = 0;
|
||||
insets.Top = 0;
|
||||
insets.Right = 0;
|
||||
}
|
||||
|
||||
return base.FitSystemWindows(insets);
|
||||
|
||||
}
|
||||
public int[] GetInsets()
|
||||
{
|
||||
return mInsets;
|
||||
}
|
||||
|
||||
public struct MeasureArgs
|
||||
{
|
||||
public int ActualHeight;
|
||||
public int ProposedHeight;
|
||||
|
||||
}
|
||||
|
||||
public event EventHandler<MeasureArgs> MeasureEvent;
|
||||
|
||||
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
MeasureArgs args;
|
||||
|
||||
args.ProposedHeight = MeasureSpec.GetSize(heightMeasureSpec);
|
||||
args.ActualHeight = Height;
|
||||
|
||||
|
||||
OnMeasureEvent(args);
|
||||
base.OnMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
protected virtual void OnMeasureEvent(MeasureArgs args)
|
||||
{
|
||||
var handler = MeasureEvent;
|
||||
if (handler != null) handler(this, args);
|
||||
}
|
||||
}
|
||||
}
|
624
src/keepass2android-app/GeneratePasswordActivity.cs
Normal file
@@ -0,0 +1,624 @@
|
||||
/*
|
||||
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.Globalization;
|
||||
using System.Linq;
|
||||
using Android.App;
|
||||
using Android.App.Admin;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Graphics;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Google.Android.Material.Dialog;
|
||||
using Java.Util;
|
||||
using keepass2android;
|
||||
using KeePassLib.Cryptography;
|
||||
using Newtonsoft.Json;
|
||||
using OtpKeyProv;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name", Theme = "@style/Kp2aTheme_ActionBar", WindowSoftInputMode = SoftInput.StateHidden, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden)]
|
||||
public class GeneratePasswordActivity :
|
||||
#if DEBUG
|
||||
LifecycleAwareActivity
|
||||
#else
|
||||
LockCloseActivity
|
||||
#endif
|
||||
|
||||
{
|
||||
private readonly int[] _buttonLengthButtonIds = new[] {Resource.Id.btn_length6,
|
||||
Resource.Id.btn_length8,
|
||||
Resource.Id.btn_length12,
|
||||
Resource.Id.btn_length16,
|
||||
Resource.Id.btn_length24,
|
||||
Resource.Id.btn_length32};
|
||||
|
||||
private readonly int[] _checkboxIds = new[] {Resource.Id.cb_uppercase,
|
||||
Resource.Id.cb_lowercase,
|
||||
Resource.Id.cb_digits,
|
||||
Resource.Id.cb_minus,
|
||||
Resource.Id.cb_underline,
|
||||
Resource.Id.cb_space,
|
||||
Resource.Id.cb_specials,
|
||||
Resource.Id.cb_specials_extended,
|
||||
Resource.Id.cb_brackets,
|
||||
Resource.Id.cb_at_least_one_from_each_group,
|
||||
Resource.Id.cb_exclude_lookalike
|
||||
};
|
||||
|
||||
PasswordFont _passwordFont = new PasswordFont();
|
||||
|
||||
|
||||
private ActivityDesign _design;
|
||||
public GeneratePasswordActivity()
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
public static void Launch(Activity act) {
|
||||
Intent i = new Intent(act, typeof(GeneratePasswordActivity));
|
||||
|
||||
act.StartActivityForResult(i, 0);
|
||||
}
|
||||
|
||||
public static void LaunchWithoutLockCheck(Activity act)
|
||||
{
|
||||
Intent i = new Intent(act, typeof(GeneratePasswordActivity));
|
||||
|
||||
#if DEBUG
|
||||
#else
|
||||
i.PutExtra(NoLockCheck, true);
|
||||
#endif
|
||||
|
||||
act.StartActivityForResult(i, 0);
|
||||
}
|
||||
|
||||
private PasswordProfiles _profiles;
|
||||
|
||||
private bool _updateDisabled = false;
|
||||
|
||||
class PasswordProfiles
|
||||
{
|
||||
public List<KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>> Profiles { get; set; }
|
||||
|
||||
public PasswordGenerator.CombinedKeyOptions LastUsedSettings { get; set; }
|
||||
|
||||
public int? TryFindLastUsedProfileIndex()
|
||||
{
|
||||
for (int i=0;i<Profiles.Count;i++)
|
||||
{
|
||||
var kvp = Profiles[i];
|
||||
if (kvp.Value.Equals(LastUsedSettings))
|
||||
return i;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Add(string key, PasswordGenerator.CombinedKeyOptions options)
|
||||
{
|
||||
for (var index = 0; index < Profiles.Count; index++)
|
||||
{
|
||||
var kvp = Profiles[index];
|
||||
if (kvp.Key == key)
|
||||
{
|
||||
Profiles[index] = new KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>(key, options);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Profiles.Add(new KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>(key, options));
|
||||
}
|
||||
|
||||
public void Remove(in int profileIndex)
|
||||
{
|
||||
Profiles.RemoveAt(profileIndex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState) {
|
||||
_design.ApplyTheme();
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
SetContentView(Resource.Layout.generate_password);
|
||||
SetResult(KeePass.ExitNormal);
|
||||
|
||||
|
||||
var prefs = GetPreferences(FileCreationMode.Private);
|
||||
|
||||
|
||||
|
||||
string jsonOptions = prefs.GetString("password_generator_profiles", null);
|
||||
if (jsonOptions != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_profiles = JsonConvert.DeserializeObject<PasswordProfiles>(jsonOptions);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PasswordGenerator.CombinedKeyOptions options = new PasswordGenerator.CombinedKeyOptions()
|
||||
{
|
||||
PasswordGenerationOptions = new PasswordGenerator.PasswordGenerationOptions()
|
||||
{
|
||||
Length = prefs.GetInt("length", 12),
|
||||
UpperCase = prefs.GetBoolean("cb_uppercase", true),
|
||||
LowerCase = prefs.GetBoolean("cb_lowercase", true),
|
||||
Digits = prefs.GetBoolean("cb_digits", true),
|
||||
Minus = prefs.GetBoolean("cb_minus", false),
|
||||
Underline = prefs.GetBoolean("cb_underline", false),
|
||||
Space = prefs.GetBoolean("cb_space", false),
|
||||
Specials = prefs.GetBoolean("cb_specials", false),
|
||||
SpecialsExtended = false,
|
||||
Brackets = prefs.GetBoolean("cb_brackets", false)
|
||||
}
|
||||
};
|
||||
_profiles = new PasswordProfiles()
|
||||
{
|
||||
LastUsedSettings = options,
|
||||
Profiles = GetDefaultProfiles()
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
_profiles ??= new PasswordProfiles();
|
||||
_profiles.LastUsedSettings ??= new PasswordGenerator.CombinedKeyOptions()
|
||||
{
|
||||
PasswordGenerationOptions = new PasswordGenerator.PasswordGenerationOptions()
|
||||
{Length = 7, UpperCase = true, LowerCase = true, Digits = true}
|
||||
};
|
||||
_profiles.Profiles ??= new List<KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>>();
|
||||
|
||||
_updateDisabled = true;
|
||||
PopulateFieldsFromOptions(_profiles.LastUsedSettings);
|
||||
_updateDisabled = false;
|
||||
|
||||
|
||||
var profileSpinner = UpdateProfileSpinner();
|
||||
|
||||
profileSpinner.ItemSelected += (sender, args) =>
|
||||
{
|
||||
if (profileSpinner.SelectedItemPosition > 0)
|
||||
{
|
||||
_profiles.LastUsedSettings = _profiles.Profiles[profileSpinner.SelectedItemPosition - 1].Value;
|
||||
_updateDisabled = true;
|
||||
PopulateFieldsFromOptions(_profiles.LastUsedSettings);
|
||||
_updateDisabled = false;
|
||||
UpdatePassword();
|
||||
}
|
||||
};
|
||||
|
||||
foreach (int id in _buttonLengthButtonIds) {
|
||||
Button button = (Button) FindViewById(id);
|
||||
button.Click += (sender, e) =>
|
||||
{
|
||||
Button b = (Button) sender;
|
||||
|
||||
EditText editText = (EditText) FindViewById(Resource.Id.length);
|
||||
editText.Text = b.Text;
|
||||
};
|
||||
}
|
||||
|
||||
FindViewById<EditText>(Resource.Id.length).TextChanged += (sender, args) => UpdatePassword();
|
||||
FindViewById<EditText>(Resource.Id.wordcount).TextChanged += (sender, args) => UpdatePassword();
|
||||
FindViewById<EditText>(Resource.Id.wordseparator).TextChanged += (sender, args) => UpdatePassword();
|
||||
|
||||
foreach (int id in _checkboxIds)
|
||||
{
|
||||
FindViewById<CheckBox>(id).CheckedChange += (sender, args) => UpdatePassword();
|
||||
}
|
||||
|
||||
var mode_spinner = FindViewById<Spinner>(Resource.Id.spinner_password_generator_mode);
|
||||
mode_spinner.ItemSelected += (sender, args) =>
|
||||
{
|
||||
FindViewById(Resource.Id.passphraseOptions).Visibility =
|
||||
mode_spinner.SelectedItemPosition == 0 ? ViewStates.Gone : ViewStates.Visible;
|
||||
FindViewById(Resource.Id.passwordOptions).Visibility =
|
||||
mode_spinner.SelectedItemPosition == 1 ? ViewStates.Gone : ViewStates.Visible;
|
||||
|
||||
UpdatePassword();
|
||||
};
|
||||
|
||||
FindViewById<Spinner>(Resource.Id.spinner_password_generator_case_mode).ItemSelected += (sender, args) =>
|
||||
{
|
||||
UpdatePassword();
|
||||
};
|
||||
|
||||
Button genPassButton = (Button) FindViewById(Resource.Id.generate_password_button);
|
||||
genPassButton.Click += (sender, e) => { UpdatePassword(); };
|
||||
|
||||
|
||||
|
||||
View acceptButton = FindViewById(Resource.Id.accept_button);
|
||||
acceptButton.Click += (sender, e) => {
|
||||
EditText password = (EditText) FindViewById(Resource.Id.password_edit);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.PutExtra("keepass2android.password.generated_password", password.Text);
|
||||
|
||||
SetResult(KeePass.ResultOkPasswordGenerator, intent);
|
||||
|
||||
Finish();
|
||||
};
|
||||
|
||||
|
||||
View cancelButton = FindViewById(Resource.Id.cancel_button);
|
||||
cancelButton.Click += (sender, e) =>
|
||||
{
|
||||
SetResult(Result.Canceled);
|
||||
|
||||
Finish();
|
||||
};
|
||||
|
||||
FindViewById(Resource.Id.btn_password_generator_profile_save)
|
||||
.Click += (sender, args) =>
|
||||
{
|
||||
var editText = new EditText(this);
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.SetMessage(Resource.String.save_password_generation_profile_text)
|
||||
.SetView(editText)
|
||||
.SetPositiveButton(Android.Resource.String.Ok, (o, eventArgs) =>
|
||||
{
|
||||
_profiles.Add(editText.Text, GetOptions());
|
||||
UpdateProfileSpinner();
|
||||
})
|
||||
.Show();
|
||||
};
|
||||
|
||||
FindViewById(Resource.Id.btn_password_generator_profile_delete)
|
||||
.Click += (sender, args) =>
|
||||
{
|
||||
if (profileSpinner.SelectedItemPosition > 0)
|
||||
{
|
||||
_profiles.Remove(profileSpinner.SelectedItemPosition-1);
|
||||
UpdateProfileSpinner();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
EditText txtPasswordToSet = (EditText) FindViewById(Resource.Id.password_edit);
|
||||
|
||||
_passwordFont.ApplyTo(txtPasswordToSet);
|
||||
|
||||
UpdatePassword();
|
||||
|
||||
SupportActionBar.SetDisplayHomeAsUpEnabled(true);
|
||||
SupportActionBar.SetHomeButtonEnabled(true);
|
||||
|
||||
}
|
||||
|
||||
private Spinner UpdateProfileSpinner()
|
||||
{
|
||||
string[] profileNames = new List<string> {GetString(Resource.String.custom_settings)}
|
||||
.Concat(_profiles.Profiles.Select(p => p.Key))
|
||||
.ToArray();
|
||||
ArrayAdapter<String> profileArrayAdapter = new ArrayAdapter<String>(this,
|
||||
Android.Resource.Layout.SimpleSpinnerDropDownItem,
|
||||
profileNames);
|
||||
var profileSpinner = FindViewById<Spinner>(Resource.Id.spinner_password_generator_profile);
|
||||
profileSpinner.Adapter = profileArrayAdapter;
|
||||
|
||||
UpdateProfileSpinnerSelection();
|
||||
return profileSpinner;
|
||||
}
|
||||
|
||||
private static List<KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>> GetDefaultProfiles()
|
||||
{
|
||||
return new List<KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>>()
|
||||
{
|
||||
new KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>(
|
||||
"Simple12", new PasswordGenerator.CombinedKeyOptions()
|
||||
{
|
||||
PasswordGenerationOptions
|
||||
= new PasswordGenerator.PasswordGenerationOptions()
|
||||
{
|
||||
Length = 12, AtLeastOneFromEachGroup = true, ExcludeLookAlike = true,
|
||||
Digits = true, LowerCase = true, UpperCase = true
|
||||
}
|
||||
|
||||
}
|
||||
),
|
||||
new KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>(
|
||||
"Special12", new PasswordGenerator.CombinedKeyOptions()
|
||||
{
|
||||
PasswordGenerationOptions
|
||||
= new PasswordGenerator.PasswordGenerationOptions()
|
||||
{
|
||||
Length = 12, AtLeastOneFromEachGroup = true, ExcludeLookAlike = true,
|
||||
Digits = true, LowerCase = true, UpperCase = true,Specials = true,Brackets = true
|
||||
}
|
||||
|
||||
}
|
||||
),
|
||||
new KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>(
|
||||
"Password64", new PasswordGenerator.CombinedKeyOptions()
|
||||
{
|
||||
PasswordGenerationOptions
|
||||
= new PasswordGenerator.PasswordGenerationOptions()
|
||||
{
|
||||
Length = 64, AtLeastOneFromEachGroup = true,
|
||||
Digits = true, LowerCase = true, UpperCase = true, ExcludeLookAlike = false,Specials = true,Brackets = true, Minus = true, Space = true, SpecialsExtended = true,Underline = true
|
||||
}
|
||||
|
||||
}
|
||||
),
|
||||
new KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>(
|
||||
|
||||
"Passphrase7", new PasswordGenerator.CombinedKeyOptions()
|
||||
{
|
||||
PassphraseGenerationOptions
|
||||
= new PasswordGenerator.PassphraseGenerationOptions()
|
||||
{
|
||||
WordCount = 7,
|
||||
CaseMode = PasswordGenerator.PassphraseGenerationOptions.PassphraseCaseMode.Lowercase,
|
||||
Separator = " "
|
||||
}
|
||||
}
|
||||
),
|
||||
new KeyValuePair<string, PasswordGenerator.CombinedKeyOptions>(
|
||||
"Passphrase5Plus", new PasswordGenerator.CombinedKeyOptions()
|
||||
{
|
||||
PassphraseGenerationOptions
|
||||
= new PasswordGenerator.PassphraseGenerationOptions()
|
||||
{
|
||||
WordCount = 5,
|
||||
CaseMode = PasswordGenerator.PassphraseGenerationOptions.PassphraseCaseMode.PascalCase,
|
||||
Separator = " "
|
||||
},
|
||||
PasswordGenerationOptions = new PasswordGenerator.PasswordGenerationOptions()
|
||||
{
|
||||
Length = 2,
|
||||
AtLeastOneFromEachGroup = true,
|
||||
ExcludeLookAlike = true,
|
||||
Digits = true,
|
||||
Specials = true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
private void PopulateFieldsFromOptions(PasswordGenerator.CombinedKeyOptions combinedOptions)
|
||||
{
|
||||
PasswordGenerator.PasswordGenerationOptions passwordOptions = combinedOptions.PasswordGenerationOptions;
|
||||
if (passwordOptions != null)
|
||||
{
|
||||
((CheckBox)FindViewById(Resource.Id.cb_uppercase)).Checked = passwordOptions.UpperCase;
|
||||
((CheckBox)FindViewById(Resource.Id.cb_lowercase)).Checked = passwordOptions.LowerCase;
|
||||
((CheckBox)FindViewById(Resource.Id.cb_digits)).Checked = passwordOptions.Digits;
|
||||
((CheckBox)FindViewById(Resource.Id.cb_minus)).Checked = passwordOptions.Minus;
|
||||
((CheckBox)FindViewById(Resource.Id.cb_underline)).Checked = passwordOptions.Underline;
|
||||
((CheckBox)FindViewById(Resource.Id.cb_space)).Checked = passwordOptions.Space;
|
||||
((CheckBox)FindViewById(Resource.Id.cb_specials)).Checked = passwordOptions.Specials;
|
||||
((CheckBox)FindViewById(Resource.Id.cb_specials_extended)).Checked = passwordOptions.SpecialsExtended;
|
||||
((CheckBox)FindViewById(Resource.Id.cb_brackets)).Checked = passwordOptions.Brackets;
|
||||
((CheckBox)FindViewById(Resource.Id.cb_exclude_lookalike)).Checked = passwordOptions.ExcludeLookAlike;
|
||||
((CheckBox)FindViewById(Resource.Id.cb_at_least_one_from_each_group)).Checked = passwordOptions.AtLeastOneFromEachGroup;
|
||||
|
||||
((EditText)FindViewById(Resource.Id.length)).Text = passwordOptions.Length.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
FindViewById(Resource.Id.passwordOptions).Visibility = ViewStates.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
FindViewById(Resource.Id.passwordOptions).Visibility = ViewStates.Gone;
|
||||
}
|
||||
|
||||
var passphraseOptions = combinedOptions.PassphraseGenerationOptions;
|
||||
|
||||
if (passphraseOptions != null)
|
||||
{
|
||||
FindViewById<EditText>(Resource.Id.wordcount).Text = passphraseOptions.WordCount.ToString(CultureInfo.InvariantCulture);
|
||||
FindViewById<EditText>(Resource.Id.wordseparator).Text = passphraseOptions.Separator;
|
||||
FindViewById<Spinner>(Resource.Id.spinner_password_generator_case_mode)
|
||||
.SetSelection((int)passphraseOptions.CaseMode);
|
||||
|
||||
FindViewById(Resource.Id.passphraseOptions).Visibility = ViewStates.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
FindViewById(Resource.Id.passphraseOptions).Visibility = ViewStates.Gone;
|
||||
}
|
||||
|
||||
|
||||
int mode;
|
||||
if (combinedOptions.PasswordGenerationOptions != null &&
|
||||
combinedOptions.PassphraseGenerationOptions != null)
|
||||
mode = 2;
|
||||
else if (combinedOptions.PasswordGenerationOptions == null &&
|
||||
combinedOptions.PassphraseGenerationOptions != null)
|
||||
mode = 1;
|
||||
else mode = 0;
|
||||
|
||||
FindViewById<Spinner>(Resource.Id.spinner_password_generator_mode)
|
||||
.SetSelection(mode);
|
||||
|
||||
}
|
||||
|
||||
private void UpdatePassword()
|
||||
{
|
||||
if (_updateDisabled)
|
||||
return;
|
||||
|
||||
String password = "";
|
||||
uint passwordBits = 0;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
password = GeneratePassword();
|
||||
passwordBits = QualityEstimation.EstimatePasswordBits(password.ToCharArray());
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
EditText txtPassword = (EditText)FindViewById(Resource.Id.password_edit);
|
||||
txtPassword.Text = password;
|
||||
|
||||
var progressBar = FindViewById<ProgressBar>(Resource.Id.pb_password_strength);
|
||||
|
||||
progressBar.Progress = (int)passwordBits;
|
||||
progressBar.Max = 128;
|
||||
|
||||
Color color = new Color(196, 63, 49);
|
||||
if (passwordBits > 40)
|
||||
{
|
||||
color = new Color(219, 152, 55);
|
||||
}
|
||||
|
||||
if (passwordBits > 64)
|
||||
{
|
||||
color = new Color(96, 138, 38);
|
||||
}
|
||||
|
||||
if (passwordBits > 100)
|
||||
{
|
||||
color = new Color(31, 128, 31);
|
||||
}
|
||||
|
||||
progressBar.ProgressDrawable.SetColorFilter(new PorterDuffColorFilter(color,
|
||||
PorterDuff.Mode.SrcIn));
|
||||
|
||||
FindViewById<TextView>(Resource.Id.tv_password_strength).Text = " " + passwordBits + " bits";
|
||||
|
||||
|
||||
|
||||
UpdateProfileSpinnerSelection();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void UpdateProfileSpinnerSelection()
|
||||
{
|
||||
int? lastUsedIndex = _profiles.TryFindLastUsedProfileIndex();
|
||||
FindViewById<Spinner>(Resource.Id.spinner_password_generator_profile)
|
||||
.SetSelection(lastUsedIndex != null ? lastUsedIndex.Value + 1 : 0);
|
||||
}
|
||||
|
||||
public String GeneratePassword() {
|
||||
String password = "";
|
||||
|
||||
try
|
||||
{
|
||||
PasswordGenerator generator = new PasswordGenerator(this);
|
||||
|
||||
var options = GetOptions();
|
||||
|
||||
try
|
||||
{
|
||||
password = generator.GeneratePassword(options);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ArgumentException(GetString(Resource.String.error_pass_gen_type));
|
||||
}
|
||||
|
||||
_profiles.LastUsedSettings = options;
|
||||
|
||||
SaveProfiles();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Toast.MakeText(this, e.Message, ToastLength.Long).Show();
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
private void SaveProfiles()
|
||||
{
|
||||
var prefs = GetPreferences(FileCreationMode.Private);
|
||||
prefs.Edit()
|
||||
.PutString("password_generator_profiles", JsonConvert.SerializeObject(_profiles))
|
||||
.Commit();
|
||||
}
|
||||
|
||||
private PasswordGenerator.CombinedKeyOptions GetOptions()
|
||||
{
|
||||
PasswordGenerator.CombinedKeyOptions options = new PasswordGenerator.CombinedKeyOptions();
|
||||
if (FindViewById(Resource.Id.passphraseOptions).Visibility == ViewStates.Visible)
|
||||
{
|
||||
int wordCount;
|
||||
if (!int.TryParse(((EditText)FindViewById(Resource.Id.wordcount)).Text, out wordCount))
|
||||
{
|
||||
throw new Exception(GetString(Resource.String.error_wrong_length));
|
||||
}
|
||||
|
||||
options.PassphraseGenerationOptions =
|
||||
new PasswordGenerator.PassphraseGenerationOptions()
|
||||
{
|
||||
WordCount = wordCount,
|
||||
Separator = FindViewById<EditText>(Resource.Id.wordseparator).Text,
|
||||
CaseMode = (PasswordGenerator.PassphraseGenerationOptions.PassphraseCaseMode)FindViewById<Spinner>(Resource.Id.spinner_password_generator_case_mode).SelectedItemPosition
|
||||
};
|
||||
}
|
||||
|
||||
if (FindViewById(Resource.Id.passwordOptions).Visibility == ViewStates.Visible)
|
||||
{
|
||||
int length;
|
||||
if (!int.TryParse(((EditText) FindViewById(Resource.Id.length)).Text, out length))
|
||||
{
|
||||
throw new Exception(GetString(Resource.String.error_wrong_length));
|
||||
}
|
||||
|
||||
options.PasswordGenerationOptions =
|
||||
new PasswordGenerator.PasswordGenerationOptions()
|
||||
{
|
||||
Length = length,
|
||||
UpperCase = ((CheckBox) FindViewById(Resource.Id.cb_uppercase)).Checked,
|
||||
LowerCase = ((CheckBox) FindViewById(Resource.Id.cb_lowercase)).Checked,
|
||||
Digits = ((CheckBox) FindViewById(Resource.Id.cb_digits)).Checked,
|
||||
Minus = ((CheckBox) FindViewById(Resource.Id.cb_minus)).Checked,
|
||||
Underline = ((CheckBox) FindViewById(Resource.Id.cb_underline)).Checked,
|
||||
Space = ((CheckBox) FindViewById(Resource.Id.cb_space)).Checked,
|
||||
Specials = ((CheckBox) FindViewById(Resource.Id.cb_specials)).Checked,
|
||||
SpecialsExtended = ((CheckBox) FindViewById(Resource.Id.cb_specials_extended)).Checked,
|
||||
Brackets = ((CheckBox) FindViewById(Resource.Id.cb_brackets)).Checked,
|
||||
ExcludeLookAlike = ((CheckBox) FindViewById(Resource.Id.cb_exclude_lookalike)).Checked,
|
||||
AtLeastOneFromEachGroup = ((CheckBox) FindViewById(Resource.Id.cb_at_least_one_from_each_group))
|
||||
.Checked
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
public override bool OnOptionsItemSelected(IMenuItem item)
|
||||
{
|
||||
switch (item.ItemId)
|
||||
{
|
||||
case Android.Resource.Id.Home:
|
||||
OnBackPressed();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
317
src/keepass2android-app/GroupActivity.cs
Normal file
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
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.OS;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using KeePassLib;
|
||||
using Android.Util;
|
||||
using KeePassLib.Utility;
|
||||
using keepass2android.view;
|
||||
using Android.Content.PM;
|
||||
using Android.Graphics;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.Preferences;
|
||||
using Android.Runtime;
|
||||
using Google.Android.Material.Dialog;
|
||||
using keepass2android.Io;
|
||||
using keepass2android;
|
||||
using KeePassLib.Security;
|
||||
using AlertDialog = Android.App.AlertDialog;
|
||||
using Object = Java.Lang.Object;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden, Theme = "@style/Kp2aTheme_ActionBar", Exported = true)]
|
||||
[MetaData("android.app.default_searchable", Value = "keepass2android.search.SearchResults")]
|
||||
#if NoNet
|
||||
[MetaData("android.app.searchable", Resource = "@xml/searchable_offline")]
|
||||
#else
|
||||
#if DEBUG
|
||||
[MetaData("android.app.searchable", Resource = "@xml/searchable_debug")]
|
||||
#else
|
||||
[MetaData("android.app.searchable", Resource = "@xml/searchable")]
|
||||
#endif
|
||||
#endif
|
||||
[IntentFilter(new string[]{"android.intent.action.SEARCH"})]
|
||||
[MetaData("android.app.searchable",Resource=AppNames.Searchable)]
|
||||
public class GroupActivity : GroupBaseActivity {
|
||||
|
||||
public const int Uninit = -1;
|
||||
|
||||
|
||||
private const String Tag = "Group Activity:";
|
||||
private const string Askaddtemplates = "AskAddTemplates";
|
||||
|
||||
public static void Launch(Activity act, AppTask appTask, ActivityLaunchMode launchMode) {
|
||||
Launch(act, null, appTask, launchMode);
|
||||
}
|
||||
|
||||
public static void Launch (Activity act, PwGroup g, AppTask appTask, ActivityLaunchMode launchMode)
|
||||
{
|
||||
Intent i = new Intent(act, typeof(GroupActivity));
|
||||
|
||||
if ( g != null ) {
|
||||
i.PutExtra(KeyEntry, g.Uuid.ToHexString());
|
||||
}
|
||||
appTask.ToIntent(i);
|
||||
|
||||
launchMode.Launch(act, i);
|
||||
}
|
||||
|
||||
protected PwUuid RetrieveGroupId(Intent i)
|
||||
{
|
||||
String uuid = i.GetStringExtra(KeyEntry);
|
||||
|
||||
if ( String.IsNullOrEmpty(uuid) ) {
|
||||
return null;
|
||||
}
|
||||
return new PwUuid(MemUtil.HexStringToByteArray(uuid));
|
||||
}
|
||||
|
||||
public override void SetupNormalButtons()
|
||||
{
|
||||
SetNormalButtonVisibility(AddGroupEnabled, AddEntryEnabled);
|
||||
}
|
||||
|
||||
protected override bool AddEntryEnabled
|
||||
{
|
||||
get { return App.Kp2a.CurrentDb.CanWrite && ((this.Group.ParentGroup != null) || App.Kp2a.CurrentDb.DatabaseFormat.CanHaveEntriesInRootGroup); }
|
||||
}
|
||||
|
||||
protected override bool AddGroupEnabled
|
||||
{
|
||||
get { return App.Kp2a.CurrentDb.CanWrite; }
|
||||
}
|
||||
|
||||
private class TemplateListAdapter : ArrayAdapter<PwEntry>
|
||||
{
|
||||
public TemplateListAdapter(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateListAdapter(Context context, int textViewResourceId) : base(context, textViewResourceId)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateListAdapter(Context context, int resource, int textViewResourceId) : base(context, resource, textViewResourceId)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateListAdapter(Context context, int textViewResourceId, PwEntry[] objects) : base(context, textViewResourceId, objects)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateListAdapter(Context context, int resource, int textViewResourceId, PwEntry[] objects) : base(context, resource, textViewResourceId, objects)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateListAdapter(Context context, int textViewResourceId, IList<PwEntry> objects) : base(context, textViewResourceId, objects)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateListAdapter(Context context, int resource, int textViewResourceId, IList<PwEntry> objects) : base(context, resource, textViewResourceId, objects)
|
||||
{
|
||||
}
|
||||
|
||||
public override View GetView(int position, View convertView, ViewGroup parent)
|
||||
{
|
||||
View v = base.GetView(position, convertView, parent);
|
||||
|
||||
TextView tv = (TextView)v.FindViewById(Android.Resource.Id.Text1);
|
||||
tv.SetPadding(tv.PaddingLeft,0,tv.PaddingRight,0);
|
||||
|
||||
PwEntry templateEntry = this.GetItem(position);
|
||||
int size = (int)(Util.convertDpToPixel(Util.convertDpToPixel(20, Context), Context));
|
||||
var bmp =
|
||||
Bitmap.CreateScaledBitmap(
|
||||
Util.DrawableToBitmap(App.Kp2a.CurrentDb .DrawableFactory.GetIconDrawable(Context, App.Kp2a.CurrentDb.KpDatabase, templateEntry.IconId, PwUuid.Zero, false)),
|
||||
size, size,
|
||||
true);
|
||||
|
||||
|
||||
Drawable icon = new BitmapDrawable(bmp);
|
||||
|
||||
if (
|
||||
PreferenceManager.GetDefaultSharedPreferences(Context)
|
||||
.GetString("IconSetKey", Context.PackageName) == Context.PackageName)
|
||||
{
|
||||
Android.Graphics.PorterDuff.Mode mMode = Android.Graphics.PorterDuff.Mode.SrcAtop;
|
||||
Color color = new Color(189, 189, 189);
|
||||
icon.SetColorFilter(color, mMode);
|
||||
}
|
||||
|
||||
//Put the image on the TextView
|
||||
tv.SetCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
|
||||
tv.Text = templateEntry.Strings.ReadSafe(PwDefs.TitleField);
|
||||
tv.SetTextSize(ComplexUnitType.Dip, 20);
|
||||
|
||||
tv.CompoundDrawablePadding = (int)Util.convertDpToPixel(8, Context);
|
||||
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
protected override void OnCreate (Bundle savedInstanceState)
|
||||
{
|
||||
base.OnCreate (savedInstanceState);
|
||||
|
||||
|
||||
if (IsFinishing) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetResult (KeePass.ExitNormal);
|
||||
|
||||
Log.Warn (Tag, "Creating group view");
|
||||
Intent intent = Intent;
|
||||
|
||||
PwUuid id = RetrieveGroupId (intent);
|
||||
|
||||
Database db = App.Kp2a.CurrentDb;
|
||||
if (db != null)
|
||||
{
|
||||
Group = id == null ? db.Root : db.GroupsById[id];
|
||||
}
|
||||
|
||||
Log.Warn (Tag, "Retrieved group");
|
||||
if (Group == null) {
|
||||
Log.Warn (Tag, "Group was null");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (AddGroupEnabled) {
|
||||
// Add Group button
|
||||
View addGroup = FindViewById (Resource.Id.fabAddNewGroup);
|
||||
addGroup.Click += (sender, e) => {
|
||||
GroupEditActivity.Launch (this, Group);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (AddEntryEnabled)
|
||||
{
|
||||
View addEntry = FindViewById (Resource.Id.fabAddNewEntry);
|
||||
addEntry.Click += (sender, e) =>
|
||||
{
|
||||
if (App.Kp2a.CurrentDb.DatabaseFormat.SupportsTemplates &&
|
||||
!AddTemplateEntries.ContainsAllTemplates(App.Kp2a.CurrentDb) &&
|
||||
PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(Askaddtemplates, true))
|
||||
{
|
||||
App.Kp2a.AskYesNoCancel(UiStringKey.AskAddTemplatesTitle, UiStringKey.AskAddTemplatesMessage,UiStringKey.yes, UiStringKey.no,
|
||||
(o, args) =>
|
||||
{
|
||||
//yes
|
||||
ProgressTask pt = new ProgressTask(App.Kp2a, this,
|
||||
new AddTemplateEntries(this, App.Kp2a, new ActionOnFinish(this,
|
||||
(success, message, activity) => ((GroupActivity)activity)?.StartAddEntry())));
|
||||
pt.Run();
|
||||
},
|
||||
(o, args) =>
|
||||
{
|
||||
var edit = PreferenceManager.GetDefaultSharedPreferences(this).Edit();
|
||||
edit.PutBoolean(Askaddtemplates, false);
|
||||
edit.Commit();
|
||||
//no
|
||||
StartAddEntry();
|
||||
},null, this);
|
||||
|
||||
}
|
||||
else
|
||||
StartAddEntry();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
SetGroupTitle();
|
||||
SetGroupIcon();
|
||||
|
||||
FragmentManager.FindFragmentById<GroupListFragment>(Resource.Id.list_fragment).ListAdapter = new PwGroupListAdapter(this, Group);
|
||||
Log.Warn(Tag, "Finished creating group");
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void StartAddEntry()
|
||||
{
|
||||
PwEntry defaultTemplate = new PwEntry(false, false);
|
||||
defaultTemplate.IconId = PwIcon.Key;
|
||||
defaultTemplate.Strings.Set(PwDefs.TitleField, new ProtectedString(false, GetString(Resource.String.DefaultTemplate)));
|
||||
List<PwEntry> templates = new List<PwEntry>() {defaultTemplate};
|
||||
if ((!PwUuid.Zero.Equals(App.Kp2a.CurrentDb.KpDatabase.EntryTemplatesGroup))
|
||||
&& (App.Kp2a.CurrentDb.KpDatabase.RootGroup.FindGroup(App.Kp2a.CurrentDb.KpDatabase.EntryTemplatesGroup, true) != null))
|
||||
{
|
||||
templates.AddRange(
|
||||
App.Kp2a.CurrentDb.GroupsById[App.Kp2a.CurrentDb.KpDatabase.EntryTemplatesGroup].Entries.OrderBy(
|
||||
entr => entr.Strings.ReadSafe(PwDefs.TitleField)));
|
||||
}
|
||||
if (templates.Count > 1)
|
||||
{
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.SetAdapter(new TemplateListAdapter(this, Android.Resource.Layout.SelectDialogItem,
|
||||
Android.Resource.Id.Text1, templates),
|
||||
(o, args) => { EntryEditActivity.Launch(this, Group, templates[args.Which].Uuid, AppTask); })
|
||||
.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
EntryEditActivity.Launch(this, Group, PwUuid.Zero, AppTask);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnCreateContextMenu(IContextMenu menu, View v,
|
||||
IContextMenuContextMenuInfo menuInfo) {
|
||||
|
||||
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||
ClickView cv = (ClickView) acmi.TargetView;
|
||||
cv.OnCreateMenu(menu, menuInfo);
|
||||
}
|
||||
|
||||
public override bool EntriesBelongToCurrentDatabaseOnly
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public override ElementAndDatabaseId FullGroupId
|
||||
{
|
||||
get { return new ElementAndDatabaseId(App.Kp2a.FindDatabaseForElement(Group), Group); }
|
||||
}
|
||||
|
||||
public override void OnBackPressed()
|
||||
{
|
||||
base.OnBackPressed();
|
||||
//if ((Group != null) && (Group.ParentGroup != null))
|
||||
//OverridePendingTransition(Resource.Animation.anim_enter_back, Resource.Animation.anim_leave_back);
|
||||
}
|
||||
|
||||
public override bool OnContextItemSelected(IMenuItem item) {
|
||||
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo)item.MenuInfo;
|
||||
ClickView cv = (ClickView) acmi.TargetView;
|
||||
|
||||
return cv.OnContextItemSelected(item);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
1692
src/keepass2android-app/GroupBaseActivity.cs
Normal file
173
src/keepass2android-app/GroupEditActivity.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
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 Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Widget;
|
||||
using keepass2android;
|
||||
using KeePassLib;
|
||||
using KeePassLib.Utility;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name", Theme= "@style/Kp2aTheme_Dialog")]
|
||||
public class GroupEditActivity : LifecycleAwareActivity
|
||||
{
|
||||
public const String KeyParent = "parent";
|
||||
public const String KeyName = "name";
|
||||
public const String KeyIconId = "icon_id";
|
||||
public const String KeyCustomIconId = "custom_icon_id";
|
||||
public const string KeyGroupUuid = "group_uuid";
|
||||
|
||||
private ActivityDesign _design;
|
||||
|
||||
private int _selectedIconId;
|
||||
private PwUuid _selectedCustomIconId = PwUuid.Zero;
|
||||
private PwGroup _groupToEdit;
|
||||
|
||||
public GroupEditActivity()
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
protected GroupEditActivity(IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public const int RequestCodeGroupEdit = 9713;
|
||||
|
||||
|
||||
public static void Launch(Activity act, PwGroup parentGroup)
|
||||
{
|
||||
Intent i = new Intent(act, typeof(GroupEditActivity));
|
||||
|
||||
PwGroup parent = parentGroup;
|
||||
i.PutExtra(KeyParent, parent.Uuid.ToHexString());
|
||||
|
||||
act.StartActivityForResult(i, RequestCodeGroupEdit);
|
||||
}
|
||||
|
||||
public static void Launch(Activity act, PwGroup parentGroup, PwGroup groupToEdit)
|
||||
{
|
||||
Intent i = new Intent(act, typeof(GroupEditActivity));
|
||||
|
||||
PwGroup parent = parentGroup;
|
||||
i.PutExtra(KeyParent, parent.Uuid.ToHexString());
|
||||
i.PutExtra(KeyGroupUuid, groupToEdit.Uuid.ToHexString());
|
||||
|
||||
act.StartActivityForResult(i, GroupEditActivity.RequestCodeGroupEdit);
|
||||
}
|
||||
|
||||
protected override void OnCreate (Bundle savedInstanceState)
|
||||
{
|
||||
_design.ApplyDialogTheme();
|
||||
|
||||
base.OnCreate (savedInstanceState);
|
||||
SetContentView (Resource.Layout.group_edit);
|
||||
|
||||
ImageButton iconButton = (ImageButton)FindViewById (Resource.Id.icon_button);
|
||||
iconButton.Click += (sender, e) =>
|
||||
{
|
||||
IconPickerActivity.Launch (this);
|
||||
};
|
||||
_selectedIconId = (int) PwIcon.FolderOpen;
|
||||
|
||||
iconButton.SetImageDrawable(App.Kp2a.CurrentDb.DrawableFactory.GetIconDrawable(this, App.Kp2a.CurrentDb.KpDatabase, (PwIcon)_selectedIconId, null, true));
|
||||
|
||||
Button okButton = (Button)FindViewById (Resource.Id.ok);
|
||||
okButton.Click += (sender, e) => {
|
||||
TextView nameField = (TextView)FindViewById (Resource.Id.group_name);
|
||||
String name = nameField.Text;
|
||||
|
||||
if (name.Length > 0) {
|
||||
Intent intent = new Intent ();
|
||||
|
||||
intent.PutExtra (KeyName, name);
|
||||
intent.PutExtra (KeyIconId, _selectedIconId);
|
||||
if (_selectedCustomIconId != null)
|
||||
intent.PutExtra(KeyCustomIconId, MemUtil.ByteArrayToHexString(_selectedCustomIconId.UuidBytes));
|
||||
if (_groupToEdit != null)
|
||||
intent.PutExtra(KeyGroupUuid, MemUtil.ByteArrayToHexString(_groupToEdit.Uuid.UuidBytes));
|
||||
|
||||
SetResult (Result.Ok, intent);
|
||||
|
||||
Finish ();
|
||||
} else {
|
||||
Toast.MakeText (this, Resource.String.error_no_name, ToastLength.Long).Show ();
|
||||
}
|
||||
};
|
||||
|
||||
if (Intent.HasExtra(KeyGroupUuid))
|
||||
{
|
||||
string groupUuid = Intent.Extras.GetString(KeyGroupUuid);
|
||||
_groupToEdit = App.Kp2a.CurrentDb.GroupsById[new PwUuid(MemUtil.HexStringToByteArray(groupUuid))];
|
||||
_selectedIconId = (int) _groupToEdit.IconId;
|
||||
_selectedCustomIconId = _groupToEdit.CustomIconUuid;
|
||||
TextView nameField = (TextView)FindViewById(Resource.Id.group_name);
|
||||
nameField.Text = _groupToEdit.Name;
|
||||
App.Kp2a.CurrentDb.DrawableFactory.AssignDrawableTo(iconButton, this, App.Kp2a.CurrentDb.KpDatabase, _groupToEdit.IconId, _groupToEdit.CustomIconUuid, false);
|
||||
SetTitle(Resource.String.edit_group_title);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetTitle(Resource.String.add_group_title);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Button cancel = (Button)FindViewById (Resource.Id.cancel);
|
||||
cancel.Click += (sender, e) => {
|
||||
Intent intent = new Intent ();
|
||||
SetResult (Result.Canceled, intent);
|
||||
|
||||
Finish ();
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
base.OnActivityResult(requestCode, resultCode, data);
|
||||
switch ((int)resultCode)
|
||||
{
|
||||
case EntryEditActivity.ResultOkIconPicker:
|
||||
_selectedIconId = data.Extras.GetInt(IconPickerActivity.KeyIconId, (int) PwIcon.Key);
|
||||
String customIconIdString = data.Extras.GetString(IconPickerActivity.KeyCustomIconId);
|
||||
if (!String.IsNullOrEmpty(customIconIdString))
|
||||
_selectedCustomIconId = new PwUuid(MemUtil.HexStringToByteArray(customIconIdString));
|
||||
else
|
||||
_selectedCustomIconId = PwUuid.Zero;
|
||||
|
||||
ImageButton currIconButton = (ImageButton) FindViewById(Resource.Id.icon_button);
|
||||
App.Kp2a.CurrentDb.DrawableFactory.AssignDrawableTo(currIconButton, this, App.Kp2a.CurrentDb.KpDatabase, (PwIcon) _selectedIconId, _selectedCustomIconId, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
//DON'T: _design.ReapplyTheme();
|
||||
// (This causes endless loop creating/recreating. More correct: ReapplyDialogTheme (which doesn't exist) Not required anyways...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
7
src/keepass2android-app/ILockCloseActivity.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace keepass2android
|
||||
{
|
||||
public interface ILockCloseActivity
|
||||
{
|
||||
void OnLockDatabase(bool lockedByTimeout);
|
||||
}
|
||||
}
|
247
src/keepass2android-app/IconPickerActivity.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
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.IO;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Graphics;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Java.Lang;
|
||||
using keepass2android;
|
||||
using KeePassLib;
|
||||
using KeePassLib.Utility;
|
||||
using FileNotFoundException = Java.IO.FileNotFoundException;
|
||||
using IOException = Java.IO.IOException;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name", Theme = "@style/Kp2aTheme_ActionBar", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden)]
|
||||
public class IconPickerActivity : LockCloseActivity
|
||||
{
|
||||
public const string KeyIconId = "icon_id";
|
||||
public const string KeyCustomIconId = "custom_icon_id";
|
||||
|
||||
public static void Launch(Activity act)
|
||||
{
|
||||
Intent i = new Intent(act, typeof(IconPickerActivity));
|
||||
act.StartActivityForResult(i, 0);
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
SetContentView(Resource.Layout.icon_picker);
|
||||
|
||||
GridView currIconGridView = (GridView)FindViewById(Resource.Id.IconGridView);
|
||||
currIconGridView.Adapter = new ImageAdapter(this, App.Kp2a.CurrentDb.KpDatabase);
|
||||
|
||||
currIconGridView.ItemClick += (sender, e) =>
|
||||
{
|
||||
|
||||
Intent intent = new Intent();
|
||||
|
||||
if (((ImageAdapter) currIconGridView.Adapter).IsCustomIcon(e.Position))
|
||||
{
|
||||
intent.PutExtra(KeyCustomIconId,
|
||||
MemUtil.ByteArrayToHexString(((ImageAdapter) currIconGridView.Adapter).GetCustomIcon(e.Position).Uuid.UuidBytes));
|
||||
}
|
||||
else
|
||||
{
|
||||
intent.PutExtra(KeyIconId, e.Position);
|
||||
}
|
||||
SetResult((Result)EntryEditActivity.ResultOkIconPicker, intent);
|
||||
|
||||
Finish();
|
||||
};
|
||||
}
|
||||
|
||||
private const int AddCustomIconId = 1;
|
||||
private const int RequestCodePickImage = 2;
|
||||
|
||||
public override bool OnCreateOptionsMenu(IMenu menu)
|
||||
{
|
||||
|
||||
base.OnCreateOptionsMenu(menu);
|
||||
|
||||
menu.Add(0, AddCustomIconId, 0, GetString(Resource.String.AddCustomIcon));
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
public override bool OnOptionsItemSelected(IMenuItem item)
|
||||
{
|
||||
if (item.ItemId == AddCustomIconId)
|
||||
{
|
||||
Intent intent = new Intent();
|
||||
intent.SetType("image/*");
|
||||
intent.SetAction(Intent.ActionGetContent);
|
||||
intent.AddCategory(Intent.CategoryOpenable);
|
||||
StartActivityForResult(intent, RequestCodePickImage);
|
||||
}
|
||||
|
||||
return base.OnOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
|
||||
base.OnActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == RequestCodePickImage && resultCode == Result.Ok)
|
||||
try
|
||||
{
|
||||
var stream = ContentResolver.OpenInputStream(data.Data);
|
||||
var bitmap = BitmapFactory.DecodeStream(stream);
|
||||
|
||||
stream.Close();
|
||||
|
||||
float maxSize = 128;
|
||||
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
{
|
||||
if ((bitmap.Width > maxSize) || (bitmap.Height > maxSize))
|
||||
{
|
||||
float scale = System.Math.Min(maxSize / bitmap.Width, maxSize / bitmap.Height);
|
||||
var scaleWidth = (int)(bitmap.Width * scale);
|
||||
var scaleHeight = (int)(bitmap.Height * scale);
|
||||
var scaledBitmap = Bitmap.CreateScaledBitmap(bitmap, scaleWidth, scaleHeight, true);
|
||||
Bitmap newRectBitmap = Bitmap.CreateBitmap((int)maxSize, (int)maxSize, Bitmap.Config.Argb8888);
|
||||
|
||||
Canvas c = new Canvas(newRectBitmap);
|
||||
c.DrawBitmap(scaledBitmap, (maxSize - scaledBitmap.Width)/2.0f, (maxSize - scaledBitmap.Height)/2.0f, null);
|
||||
bitmap = newRectBitmap;
|
||||
}
|
||||
;
|
||||
bitmap.Compress(Bitmap.CompressFormat.Png, 90, ms);
|
||||
PwCustomIcon pwci = new PwCustomIcon(new PwUuid(true), ms.ToArray());
|
||||
|
||||
App.Kp2a.CurrentDb.KpDatabase.CustomIcons.Add(pwci);
|
||||
|
||||
}
|
||||
var gridView = ((GridView)FindViewById(Resource.Id.IconGridView));
|
||||
((BaseAdapter)gridView.Adapter).NotifyDataSetInvalidated();
|
||||
gridView.SmoothScrollToPosition(((BaseAdapter)gridView.Adapter).Count-1);
|
||||
}
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
e.PrintStackTrace();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.PrintStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public class ImageAdapter : BaseAdapter
|
||||
{
|
||||
readonly IconPickerActivity _act;
|
||||
private readonly PwDatabase _db;
|
||||
|
||||
public ImageAdapter(IconPickerActivity act, PwDatabase db)
|
||||
{
|
||||
_act = act;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public override int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
return (int)PwIcon.Count + _db.CustomIcons.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public override Java.Lang.Object GetItem(int position)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public override long GetItemId(int position)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override View GetView(int position, View convertView, ViewGroup parent)
|
||||
{
|
||||
View currView;
|
||||
if(convertView == null)
|
||||
{
|
||||
LayoutInflater li = (LayoutInflater) _act.GetSystemService(LayoutInflaterService);
|
||||
currView = li.Inflate(Resource.Layout.icon, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
currView = convertView;
|
||||
}
|
||||
TextView tv = (TextView) currView.FindViewById(Resource.Id.icon_text);
|
||||
ImageView iv = (ImageView) currView.FindViewById(Resource.Id.icon_image);
|
||||
|
||||
if (position < (int)PwIcon.Count)
|
||||
{
|
||||
tv.Text = "" + position;
|
||||
var drawable = App.Kp2a.CurrentDb .DrawableFactory.GetIconDrawable(_act, App.Kp2a.CurrentDb.KpDatabase, (KeePassLib.PwIcon) position, null, false);
|
||||
drawable = new BitmapDrawable(Util.DrawableToBitmap(drawable));
|
||||
iv.SetImageDrawable(drawable);
|
||||
//App.Kp2a.GetDb().DrawableFactory.AssignDrawableTo(iv, _act, App.Kp2a.GetDb().KpDatabase, (KeePassLib.PwIcon) position, null, false);
|
||||
|
||||
if (
|
||||
PreferenceManager.GetDefaultSharedPreferences(currView.Context)
|
||||
.GetString("IconSetKey", currView.Context.PackageName) == currView.Context.PackageName)
|
||||
{
|
||||
Android.Graphics.PorterDuff.Mode mMode = Android.Graphics.PorterDuff.Mode.SrcAtop;
|
||||
Color color = new Color(189, 189, 189);
|
||||
iv.SetColorFilter(color, mMode);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
int pos = position - (int)PwIcon.Count;
|
||||
var icon = _db.CustomIcons[pos];
|
||||
tv.Text = pos.ToString();
|
||||
iv.SetColorFilter(null);
|
||||
iv.SetImageBitmap(icon.Image);
|
||||
|
||||
}
|
||||
|
||||
return currView;
|
||||
}
|
||||
|
||||
public bool IsCustomIcon(int position)
|
||||
{
|
||||
return position >= (int)PwIcon.Count;
|
||||
}
|
||||
|
||||
public PwCustomIcon GetCustomIcon(int position)
|
||||
{
|
||||
if (!IsCustomIcon(position))
|
||||
return null;
|
||||
return _db.CustomIcons[position - (int)PwIcon.Count];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
334
src/keepass2android-app/ImageViewActivity.cs
Normal file
@@ -0,0 +1,334 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Graphics;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Util;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using keepass2android;
|
||||
using KeePassLib;
|
||||
using KeePassLib.Utility;
|
||||
using Object = Java.Lang.Object;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
|
||||
public class ZoomableImageView : ImageView
|
||||
{
|
||||
|
||||
private class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener
|
||||
{
|
||||
|
||||
private ZoomableImageView parent;
|
||||
|
||||
public ScaleListener(ZoomableImageView parent)
|
||||
{
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public override bool OnScaleBegin(ScaleGestureDetector detector)
|
||||
{
|
||||
parent.mode = ZOOM;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public override bool OnScale(ScaleGestureDetector detector)
|
||||
{
|
||||
float scaleFactor = detector.ScaleFactor;
|
||||
float newScale = parent.saveScale * scaleFactor;
|
||||
if (newScale < parent.maxScale && newScale > parent.minScale)
|
||||
{
|
||||
parent.saveScale = newScale;
|
||||
float width = parent.Width;
|
||||
float height = parent.Height;
|
||||
parent.right = (parent.originalBitmapWidth * parent.saveScale) - width;
|
||||
parent.bottom = (parent.originalBitmapHeight * parent.saveScale) - height;
|
||||
|
||||
float scaledBitmapWidth = parent.originalBitmapWidth * parent.saveScale;
|
||||
float scaledBitmapHeight = parent.originalBitmapHeight * parent.saveScale;
|
||||
|
||||
if (scaledBitmapWidth <= width || scaledBitmapHeight <= height)
|
||||
{
|
||||
parent.matrix.PostScale(scaleFactor, scaleFactor, width / 2, height / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
parent.matrix.PostScale(scaleFactor, scaleFactor, detector.FocusX, detector.FocusY);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const int NONE = 0;
|
||||
const int DRAG = 1;
|
||||
const int ZOOM = 2;
|
||||
const int CLICK = 3;
|
||||
|
||||
private int mode = NONE;
|
||||
|
||||
private Matrix matrix = new Matrix();
|
||||
|
||||
private PointF last = new PointF();
|
||||
private PointF start = new PointF();
|
||||
private float minScale = 1.0f;
|
||||
private float maxScale = 100.0f;
|
||||
private float[] m;
|
||||
|
||||
private float redundantXSpace, redundantYSpace;
|
||||
private float saveScale = 1f;
|
||||
private float right, bottom, originalBitmapWidth, originalBitmapHeight;
|
||||
|
||||
private ScaleGestureDetector mScaleDetector;
|
||||
protected ZoomableImageView(IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
}
|
||||
public ZoomableImageView(Context context)
|
||||
: base(context)
|
||||
{
|
||||
|
||||
init(context);
|
||||
}
|
||||
|
||||
public ZoomableImageView(Context context, IAttributeSet attrs)
|
||||
: base(context, attrs)
|
||||
{
|
||||
init(context);
|
||||
}
|
||||
|
||||
public ZoomableImageView(Context context, IAttributeSet attrs, int defStyleAttr)
|
||||
: base(context, attrs, defStyleAttr)
|
||||
{
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context)
|
||||
{
|
||||
base.Clickable = true;
|
||||
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener(this));
|
||||
m = new float[9];
|
||||
ImageMatrix = matrix;
|
||||
SetScaleType(ScaleType.Matrix);
|
||||
}
|
||||
|
||||
|
||||
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
base.OnMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
int bmHeight = getBmHeight();
|
||||
int bmWidth = getBmWidth();
|
||||
|
||||
float width = MeasuredWidth;
|
||||
float height = MeasuredHeight;
|
||||
//Fit to screen.
|
||||
float scale = width > height ? height / bmHeight : width / bmWidth;
|
||||
|
||||
matrix.SetScale(scale, scale);
|
||||
saveScale = 1f;
|
||||
|
||||
originalBitmapWidth = scale * bmWidth;
|
||||
originalBitmapHeight = scale * bmHeight;
|
||||
|
||||
// Center the image
|
||||
redundantYSpace = (height - originalBitmapHeight);
|
||||
redundantXSpace = (width - originalBitmapWidth);
|
||||
|
||||
matrix.PostTranslate(redundantXSpace / 2, redundantYSpace / 2);
|
||||
|
||||
ImageMatrix = matrix;
|
||||
}
|
||||
|
||||
|
||||
public override bool OnTouchEvent(MotionEvent event_)
|
||||
{
|
||||
mScaleDetector.OnTouchEvent(event_);
|
||||
|
||||
matrix.GetValues(m);
|
||||
float x = m[Matrix.MtransX];
|
||||
float y = m[Matrix.MtransY];
|
||||
PointF curr = new PointF(event_.GetX(), event_.GetY());
|
||||
Log.Debug("TOUCH", event_.Action.ToString(), " mode=" + mode);
|
||||
switch (event_.Action)
|
||||
{
|
||||
//when one finger is touching
|
||||
//set the mode to DRAG
|
||||
case MotionEventActions.Down:
|
||||
last.Set(event_.GetX(), event_.GetY());
|
||||
start.Set(last);
|
||||
mode = DRAG;
|
||||
break;
|
||||
//when two fingers are touching
|
||||
//set the mode to ZOOM
|
||||
case MotionEventActions.Pointer2Down:
|
||||
case MotionEventActions.PointerDown:
|
||||
last.Set(event_.GetX(), event_.GetY());
|
||||
start.Set(last);
|
||||
mode = ZOOM;
|
||||
break;
|
||||
//when a finger moves
|
||||
//If mode is applicable move image
|
||||
case MotionEventActions.Move:
|
||||
//if the mode is ZOOM or
|
||||
//if the mode is DRAG and already zoomed
|
||||
if (mode == ZOOM || (mode == DRAG && saveScale > minScale))
|
||||
{
|
||||
float deltaX = curr.X - last.X;// x difference
|
||||
float deltaY = curr.Y - last.Y;// y difference
|
||||
float scaleWidth = (float)System.Math.Round(originalBitmapWidth * saveScale);// width after applying current scale
|
||||
float scaleHeight = (float)System.Math.Round(originalBitmapHeight * saveScale);// height after applying current scale
|
||||
|
||||
bool limitX = false;
|
||||
bool limitY = false;
|
||||
|
||||
//if scaleWidth is smaller than the views width
|
||||
//in other words if the image width fits in the view
|
||||
//limit left and right movement
|
||||
if (scaleWidth < Width && scaleHeight < Height)
|
||||
{
|
||||
// don't do anything
|
||||
}
|
||||
else if (scaleWidth < Width)
|
||||
{
|
||||
deltaX = 0;
|
||||
limitY = true;
|
||||
}
|
||||
//if scaleHeight is smaller than the views height
|
||||
//in other words if the image height fits in the view
|
||||
//limit up and down movement
|
||||
else if (scaleHeight < Height)
|
||||
{
|
||||
deltaY = 0;
|
||||
limitX = true;
|
||||
}
|
||||
//if the image doesnt fit in the width or height
|
||||
//limit both up and down and left and right
|
||||
else
|
||||
{
|
||||
limitX = true;
|
||||
limitY = true;
|
||||
}
|
||||
|
||||
if (limitY)
|
||||
{
|
||||
if (y + deltaY > 0)
|
||||
{
|
||||
deltaY = -y;
|
||||
}
|
||||
else if (y + deltaY < -bottom)
|
||||
{
|
||||
deltaY = -(y + bottom);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (limitX)
|
||||
{
|
||||
if (x + deltaX > 0)
|
||||
{
|
||||
deltaX = -x;
|
||||
}
|
||||
else if (x + deltaX < -right)
|
||||
{
|
||||
deltaX = -(x + right);
|
||||
}
|
||||
|
||||
}
|
||||
//move the image with the matrix
|
||||
matrix.PostTranslate(deltaX, deltaY);
|
||||
//set the last touch location to the current
|
||||
last.Set(curr.X, curr.Y);
|
||||
}
|
||||
break;
|
||||
//first finger is lifted
|
||||
case MotionEventActions.Up:
|
||||
mode = NONE;
|
||||
int xDiff = (int)System.Math.Abs(curr.X - start.X);
|
||||
int yDiff = (int)System.Math.Abs(curr.Y - start.Y);
|
||||
if (xDiff < CLICK && yDiff < CLICK)
|
||||
PerformClick();
|
||||
break;
|
||||
// second finger is lifted
|
||||
case MotionEventActions.Pointer2Up:
|
||||
case MotionEventActions.PointerUp:
|
||||
mode = NONE;
|
||||
break;
|
||||
}
|
||||
ImageMatrix = matrix;
|
||||
Invalidate();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setMaxZoom(float x)
|
||||
{
|
||||
maxScale = x;
|
||||
}
|
||||
|
||||
private int getBmWidth()
|
||||
{
|
||||
Drawable drawable = Drawable;
|
||||
if (drawable != null)
|
||||
{
|
||||
return drawable.IntrinsicWidth;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int getBmHeight()
|
||||
{
|
||||
Drawable drawable = Drawable;
|
||||
if (drawable != null)
|
||||
{
|
||||
return drawable.IntrinsicHeight;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
[Activity(Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden,
|
||||
Theme = "@style/Kp2aTheme_ActionBar")]
|
||||
public class ImageViewActivity : LockCloseActivity
|
||||
{
|
||||
private ActivityDesign _activityDesign;
|
||||
|
||||
public ImageViewActivity()
|
||||
{
|
||||
_activityDesign = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
_activityDesign.ReapplyTheme();
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
_activityDesign.ApplyTheme();
|
||||
base.OnCreate(savedInstanceState);
|
||||
SetContentView(Resource.Layout.ImageViewActivity);
|
||||
|
||||
ElementAndDatabaseId fullId = new ElementAndDatabaseId(Intent.GetStringExtra("EntryId"));
|
||||
|
||||
var uuid = new PwUuid(MemUtil.HexStringToByteArray(fullId.ElementIdString));
|
||||
string key = Intent.GetStringExtra("EntryKey");
|
||||
var binary = App.Kp2a.GetDatabase(fullId.DatabaseId).EntriesById[uuid].Binaries.Get(key);
|
||||
SupportActionBar.Title = key;
|
||||
byte[] pbdata = binary.ReadData();
|
||||
|
||||
var bmp = BitmapFactory.DecodeByteArray(pbdata,0,pbdata.Length);
|
||||
|
||||
FindViewById<ImageView>(Resource.Id.imageView).SetImageBitmap(bmp);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
466
src/keepass2android-app/KeeAutoExec.cs
Normal file
@@ -0,0 +1,466 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Android.App;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Android.Webkit;
|
||||
using Android.Widget;
|
||||
using Java.Nio.FileNio;
|
||||
using KeePass.DataExchange;
|
||||
using KeePass.Util.Spr;
|
||||
using keepass2android;
|
||||
using KeePassLib;
|
||||
using KeePassLib.Keys;
|
||||
using KeePassLib.Security;
|
||||
using KeePassLib.Serialization;
|
||||
using KeePassLib.Utility;
|
||||
using Debug = System.Diagnostics.Debug;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public sealed class AutoExecItem
|
||||
{
|
||||
private PwEntry m_pe;
|
||||
public PwEntry Entry
|
||||
{
|
||||
get { return m_pe; }
|
||||
}
|
||||
|
||||
private PwDatabase m_pdContext;
|
||||
public PwDatabase Database
|
||||
{
|
||||
get { return m_pdContext; }
|
||||
}
|
||||
|
||||
public bool Enabled = true;
|
||||
public bool Visible = true;
|
||||
|
||||
public long Priority = 0;
|
||||
|
||||
public string IfDevice = null;
|
||||
|
||||
public AutoExecItem(PwEntry pe, PwDatabase pdContext)
|
||||
{
|
||||
if (pe == null) throw new ArgumentNullException("pe");
|
||||
|
||||
m_pe = pe;
|
||||
m_pdContext = pdContext;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class KeeAutoExecExt
|
||||
{
|
||||
public const string _ifDevice = "IfDevice";
|
||||
|
||||
private static string _thisDevice = null;
|
||||
|
||||
public static string ThisDeviceId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_thisDevice != null)
|
||||
return _thisDevice;
|
||||
String android_id = Settings.Secure.GetString(LocaleManager.LocalizedAppContext.ContentResolver, Settings.Secure.AndroidId);
|
||||
|
||||
string deviceName = Build.Manufacturer+" "+Build.Model;
|
||||
_thisDevice = deviceName + " (" + android_id + ")";
|
||||
|
||||
_thisDevice = _thisDevice.Replace("!", "_");
|
||||
_thisDevice = _thisDevice.Replace(",", "_");
|
||||
_thisDevice = _thisDevice.Replace(";", "_");
|
||||
return _thisDevice;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static int PrioritySort(AutoExecItem x, AutoExecItem y)
|
||||
{
|
||||
if (x == null) { Debug.Assert(false); return ((y == null) ? 0 : -1); }
|
||||
if (y == null) { Debug.Assert(false); return 1; }
|
||||
|
||||
return x.Priority.CompareTo(y.Priority);
|
||||
}
|
||||
|
||||
private static void AddAutoExecEntries(List<PwEntry> l, PwGroup pg)
|
||||
{
|
||||
if (pg.Name.Equals("AutoOpen", StrUtil.CaseIgnoreCmp))
|
||||
l.AddRange(pg.GetEntries(true));
|
||||
else
|
||||
{
|
||||
foreach (PwGroup pgSub in pg.Groups)
|
||||
AddAutoExecEntries(l, pgSub);
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<string, bool> GetIfDevice(AutoExecItem item)
|
||||
{
|
||||
Dictionary<string, bool> result = new Dictionary<string, bool>();
|
||||
|
||||
string strList = item.IfDevice;
|
||||
|
||||
if (string.IsNullOrEmpty(strList))
|
||||
return result;
|
||||
|
||||
CsvOptions opt = new CsvOptions
|
||||
{
|
||||
BackslashIsEscape = false,
|
||||
TrimFields = true
|
||||
};
|
||||
|
||||
CsvStreamReaderEx csv = new CsvStreamReaderEx(strList, opt);
|
||||
string[] vFlt = csv.ReadLine();
|
||||
if (vFlt == null) { Debug.Assert(false); return result; }
|
||||
|
||||
foreach (string strFlt in vFlt)
|
||||
{
|
||||
if (string.IsNullOrEmpty(strFlt)) continue;
|
||||
|
||||
if (strFlt[0] == '!') // Exclusion
|
||||
{
|
||||
result[strFlt.Substring(1).TrimStart()] = false;
|
||||
}
|
||||
else // Inclusion
|
||||
{
|
||||
result[strFlt] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string BuildIfDevice(Dictionary<string, bool> devices)
|
||||
{
|
||||
CsvOptions opt = new CsvOptions
|
||||
{
|
||||
BackslashIsEscape = false,
|
||||
TrimFields = true
|
||||
};
|
||||
|
||||
bool hasEnabledDevices = devices.Any(kvp => kvp.Value);
|
||||
|
||||
string result = "";
|
||||
foreach (var deviceWithEnabled in devices)
|
||||
{
|
||||
if (result != "")
|
||||
{
|
||||
result += opt.FieldSeparator;
|
||||
}
|
||||
//if the list of devices has enabled devices, we do not need to include a negated expression
|
||||
if (hasEnabledDevices && !deviceWithEnabled.Value)
|
||||
continue;
|
||||
string deviceValue = (deviceWithEnabled.Value ? "" : "!") + deviceWithEnabled.Key;
|
||||
if (deviceValue.Contains(opt.FieldSeparator) || deviceValue.Contains("\\") ||
|
||||
deviceValue.Contains("\""))
|
||||
{
|
||||
//add escaping:
|
||||
deviceValue = deviceValue.Replace("\"", "\\\"");
|
||||
deviceValue = "\"" + deviceValue + "\"";
|
||||
}
|
||||
result += deviceValue;
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool IsDeviceEnabled(AutoExecItem a, string strDevice, out bool isExplicit)
|
||||
{
|
||||
isExplicit = false;
|
||||
var ifDevices = GetIfDevice(a);
|
||||
|
||||
if (!ifDevices.Any() || string.IsNullOrEmpty(strDevice))
|
||||
return true;
|
||||
|
||||
bool bHasIncl = false, bHasExcl = false;
|
||||
foreach (var kvp in ifDevices)
|
||||
{
|
||||
if (string.IsNullOrEmpty(kvp.Key)) continue;
|
||||
|
||||
if (strDevice.Equals(kvp.Key, StrUtil.CaseIgnoreCmp))
|
||||
{
|
||||
isExplicit = true;
|
||||
return kvp.Value;
|
||||
}
|
||||
|
||||
if (kvp.Value == false)
|
||||
{
|
||||
bHasExcl = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bHasIncl = true;
|
||||
}
|
||||
}
|
||||
|
||||
return (bHasExcl || !bHasIncl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void SetDeviceEnabled(AutoExecItem a, string strDevice, bool enabled=true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(strDevice))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var devices = GetIfDevice(a);
|
||||
|
||||
devices[strDevice] = enabled;
|
||||
|
||||
string result = BuildIfDevice(devices);
|
||||
a.Entry.Strings.Set(_ifDevice, new ProtectedString(false,result));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static List<AutoExecItem> GetAutoExecItems(PwDatabase pd)
|
||||
{
|
||||
List<AutoExecItem> l = new List<AutoExecItem>();
|
||||
if (pd == null) { Debug.Assert(false); return l; }
|
||||
if (!pd.IsOpen) return l;
|
||||
|
||||
PwGroup pgRoot = pd.RootGroup;
|
||||
if (pgRoot == null) { Debug.Assert(false); return l; }
|
||||
|
||||
List<PwEntry> lAutoEntries = new List<PwEntry>();
|
||||
AddAutoExecEntries(lAutoEntries, pgRoot);
|
||||
|
||||
long lPriStd = 0;
|
||||
foreach (PwEntry pe in lAutoEntries)
|
||||
{
|
||||
|
||||
if (pe.Strings.ReadSafe(PwDefs.UrlField).Length == 0) continue;
|
||||
|
||||
var a = MakeAutoExecItem(pd, pe, lPriStd);
|
||||
|
||||
l.Add(a);
|
||||
++lPriStd;
|
||||
}
|
||||
|
||||
l.Sort(KeeAutoExecExt.PrioritySort);
|
||||
return l;
|
||||
}
|
||||
|
||||
public static AutoExecItem MakeAutoExecItem(PwDatabase pd, PwEntry pe, long lPriStd)
|
||||
{
|
||||
string str = pe.Strings.ReadSafe(PwDefs.UrlField);
|
||||
AutoExecItem a = new AutoExecItem(pe, pd);
|
||||
|
||||
|
||||
SprContext ctx = new SprContext(pe, pd, SprCompileFlags.All);
|
||||
|
||||
if (pe.Expires && (pe.ExpiryTime <= DateTime.UtcNow))
|
||||
a.Enabled = false;
|
||||
|
||||
bool? ob = GetBoolEx(pe, "Enabled", ctx);
|
||||
if (ob.HasValue) a.Enabled = ob.Value;
|
||||
|
||||
ob = GetBoolEx(pe, "Visible", ctx);
|
||||
if (ob.HasValue) a.Visible = ob.Value;
|
||||
|
||||
long lItemPri = lPriStd;
|
||||
if (GetString(pe, "Priority", ctx, true, out str))
|
||||
long.TryParse(str, out lItemPri);
|
||||
a.Priority = lItemPri;
|
||||
|
||||
|
||||
if (GetString(pe, _ifDevice, ctx, true, out str))
|
||||
a.IfDevice = str;
|
||||
return a;
|
||||
}
|
||||
|
||||
public static bool AutoOpenEntry(Activity activity, AutoExecItem item, bool bManual,
|
||||
ActivityLaunchMode launchMode)
|
||||
{
|
||||
string str;
|
||||
PwEntry pe = item.Entry;
|
||||
SprContext ctxNoEsc = new SprContext(pe, item.Database, SprCompileFlags.All);
|
||||
IOConnectionInfo ioc;
|
||||
if (!TryGetDatabaseIoc(item, out ioc)) return false;
|
||||
|
||||
var ob = GetBoolEx(pe, "SkipIfNotExists", ctxNoEsc);
|
||||
if (!ob.HasValue) // Backw. compat.
|
||||
ob = GetBoolEx(pe, "Skip if not exists", ctxNoEsc);
|
||||
if (ob.HasValue && ob.Value)
|
||||
{
|
||||
if (!CheckFileExsts(ioc)) return false;
|
||||
}
|
||||
|
||||
CompositeKey ck = new CompositeKey();
|
||||
|
||||
if (GetString(pe, PwDefs.PasswordField, ctxNoEsc, false, out str))
|
||||
ck.AddUserKey(new KcpPassword(str));
|
||||
|
||||
if (GetString(pe, PwDefs.UserNameField, ctxNoEsc, false, out str))
|
||||
{
|
||||
string strAbs = str;
|
||||
IOConnectionInfo iocKey = IOConnectionInfo.FromPath(strAbs);
|
||||
if (iocKey.IsLocalFile() && !UrlUtil.IsAbsolutePath(strAbs))
|
||||
{
|
||||
//local relative paths not supported on Android
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
ob = GetBoolEx(pe, "SkipIfKeyFileNotExists", ctxNoEsc);
|
||||
if (ob.HasValue && ob.Value)
|
||||
{
|
||||
IOConnectionInfo iocKeyAbs = IOConnectionInfo.FromPath(strAbs);
|
||||
if (!CheckFileExsts(iocKeyAbs)) return false;
|
||||
}
|
||||
|
||||
try { ck.AddUserKey(new KcpKeyFile(strAbs)); }
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
Toast.MakeText(LocaleManager.LocalizedAppContext,Resource.String.error_adding_keyfile,ToastLength.Long).Show();
|
||||
return false;
|
||||
}
|
||||
catch (Exception) { throw; }
|
||||
}
|
||||
else // Try getting key file from attachments
|
||||
{
|
||||
ProtectedBinary pBin = pe.Binaries.Get("KeyFile.bin");
|
||||
if (pBin != null)
|
||||
ck.AddUserKey(new KcpKeyFile(IOConnectionInfo.FromPath(
|
||||
StrUtil.DataToDataUri(pBin.ReadData(), null))));
|
||||
}
|
||||
|
||||
GetString(pe, "Focus", ctxNoEsc, true, out str);
|
||||
bool bRestoreFocus = str.Equals("Restore", StrUtil.CaseIgnoreCmp);
|
||||
|
||||
PasswordActivity.Launch(activity,ioc,ck,launchMode, !
|
||||
bRestoreFocus);
|
||||
|
||||
App.Kp2a.RegisterChildDatabase(ioc);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CheckFileExsts(IOConnectionInfo ioc)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
var fileStorage = App.Kp2a.GetFileStorage(ioc);
|
||||
|
||||
//we're assuming that remote files always exist (if we have a file storage, i.e. we check after receiving a file storage)
|
||||
//The SkipIfNotExists switch only makes sense for local files, because remote files either exist for all devices or none
|
||||
//(Ok, there are exceptions like files available in a (W)LAN. But then we still have the device switch and caches.)
|
||||
//We cannot use OpenFileForRead on remote storages because this method is called from the main thread.
|
||||
if (!ioc.IsLocalFile())
|
||||
return true;
|
||||
|
||||
using (var stream = fileStorage.OpenFileForRead(ioc))
|
||||
{
|
||||
}
|
||||
}
|
||||
catch (NoFileStorageFoundException e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetDatabaseIoc(AutoExecItem a, out IOConnectionInfo ioc)
|
||||
{
|
||||
PwEntry pe = a.Entry;
|
||||
PwDatabase pdContext = a.Database;
|
||||
|
||||
SprContext ctxNoEsc = new SprContext(pe, pdContext, SprCompileFlags.All);
|
||||
SprContext ctxEsc = new SprContext(pe, pdContext, SprCompileFlags.All,
|
||||
false, true);
|
||||
|
||||
ioc = null;
|
||||
|
||||
string strDb;
|
||||
if (!GetString(pe, PwDefs.UrlField, ctxEsc, true, out strDb)) return false;
|
||||
|
||||
ioc = IOConnectionInfo.FromPath(strDb);
|
||||
//TODO
|
||||
/*if (ioc.IsLocalFile() && !UrlUtil.IsAbsolutePath(strDb))
|
||||
ioc = IOConnectionInfo.FromPath(UrlUtil.MakeAbsolutePath(
|
||||
WinUtil.GetExecutable(), strDb));*/
|
||||
if (ioc.Path.Length == 0) return false;
|
||||
|
||||
string strIocUserName;
|
||||
if (GetString(pe, "IocUserName", ctxNoEsc, true, out strIocUserName))
|
||||
ioc.UserName = strIocUserName;
|
||||
|
||||
string strIocPassword;
|
||||
if (GetString(pe, "IocPassword", ctxNoEsc, true, out strIocPassword))
|
||||
ioc.Password = strIocPassword;
|
||||
|
||||
if ((strIocUserName.Length != 0) && (strIocPassword.Length != 0))
|
||||
ioc.IsComplete = true;
|
||||
|
||||
string str;
|
||||
if (GetString(pe, "IocTimeout", ctxNoEsc, true, out str))
|
||||
{
|
||||
long l;
|
||||
if (long.TryParse(str, out l))
|
||||
ioc.Properties.SetLong(IocKnownProperties.Timeout, l);
|
||||
}
|
||||
|
||||
bool? ob = GetBoolEx(pe, "IocPreAuth", ctxNoEsc);
|
||||
if (ob.HasValue)
|
||||
ioc.Properties.SetBool(IocKnownProperties.PreAuth, ob.Value);
|
||||
|
||||
if (GetString(pe, "IocUserAgent", ctxNoEsc, true, out str))
|
||||
ioc.Properties.Set(IocKnownProperties.UserAgent, str);
|
||||
|
||||
ob = GetBoolEx(pe, "IocExpect100Continue", ctxNoEsc);
|
||||
if (ob.HasValue)
|
||||
ioc.Properties.SetBool(IocKnownProperties.Expect100Continue, ob.Value);
|
||||
|
||||
ob = GetBoolEx(pe, "IocPassive", ctxNoEsc);
|
||||
if (ob.HasValue)
|
||||
ioc.Properties.SetBool(IocKnownProperties.Passive, ob.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool GetString(PwEntry pe, string strName, SprContext ctx,
|
||||
bool bTrim, out string strValue)
|
||||
{
|
||||
if ((pe == null) || (strName == null))
|
||||
{
|
||||
Debug.Assert(false);
|
||||
strValue = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
string str = pe.Strings.ReadSafe(strName);
|
||||
if (ctx != null) str = SprEngine.Compile(str, ctx);
|
||||
if (bTrim) str = str.Trim();
|
||||
|
||||
strValue = str;
|
||||
return (str.Length != 0);
|
||||
}
|
||||
|
||||
private static bool? GetBoolEx(PwEntry pe, string strName, SprContext ctx)
|
||||
{
|
||||
string str;
|
||||
if (GetString(pe, strName, ctx, true, out str))
|
||||
{
|
||||
if (str.Equals("True", StrUtil.CaseIgnoreCmp))
|
||||
return true;
|
||||
if (str.Equals("False", StrUtil.CaseIgnoreCmp))
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class KeeAutoExec
|
||||
{
|
||||
|
||||
}
|
||||
}
|
205
src/keepass2android-app/KeeChallenge.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
/* KeeChallenge--Provides Yubikey challenge-response capability to Keepass
|
||||
* Copyright (C) 2014 Ben Rush
|
||||
*
|
||||
* This program 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 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program 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 this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
using System.Diagnostics;
|
||||
using System.Xml;
|
||||
|
||||
using KeePassLib.Keys;
|
||||
using KeePassLib.Utility;
|
||||
using KeePassLib.Cryptography;
|
||||
using KeePassLib.Cryptography.Cipher;
|
||||
using KeePassLib.Serialization;
|
||||
|
||||
using keepass2android;
|
||||
using keepass2android.Io;
|
||||
|
||||
namespace KeeChallenge
|
||||
{
|
||||
public sealed class KeeChallengeProv
|
||||
{
|
||||
private const string m_name = "Yubikey challenge-response";
|
||||
|
||||
public static string Name { get { return m_name; } }
|
||||
|
||||
public const int keyLenBytes = 20;
|
||||
public const int challengeLenBytes = 64;
|
||||
public const int responseLenBytes = 20;
|
||||
public const int secretLenBytes = 20;
|
||||
|
||||
//If variable length challenges are enabled, a 63 byte challenge is sent instead.
|
||||
//See GenerateChallenge() and http://forum.yubico.com/viewtopic.php?f=16&t=1078
|
||||
//This field is automatically set by calling GetSecret(). However, when creating
|
||||
//a new database it will need to be set manually based on the user's yubikey settings
|
||||
public bool LT64
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public KeeChallengeProv()
|
||||
{
|
||||
LT64 = false;
|
||||
}
|
||||
|
||||
private byte[] GenerateChallenge()
|
||||
{
|
||||
byte[] chal = CryptoRandom.Instance.GetRandomBytes(challengeLenBytes);
|
||||
if (LT64)
|
||||
{
|
||||
chal[challengeLenBytes - 2] = (byte)~chal[challengeLenBytes - 1];
|
||||
}
|
||||
|
||||
return chal;
|
||||
}
|
||||
|
||||
private byte[] GenerateResponse(byte[] challenge, byte[] key)
|
||||
{
|
||||
HMACSHA1 hmac = new HMACSHA1(key);
|
||||
|
||||
if (LT64)
|
||||
challenge = challenge.Take(challengeLenBytes - 1).ToArray();
|
||||
|
||||
byte[] resp = hmac.ComputeHash(challenge);
|
||||
hmac.Clear();
|
||||
return resp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method for generating encrypted ChallengeInfo to be saved. For security, this method should
|
||||
/// be called every time you get a successful challenge-response pair from the Yubikey. Failure to
|
||||
/// do so will permit password re-use attacks.
|
||||
/// </summary>
|
||||
/// <param name="secret">The un-encrypted secret</param>
|
||||
/// <returns>A fully populated ChallengeInfo object ready to be saved</returns>
|
||||
public ChallengeInfo Encrypt(byte[] secret)
|
||||
{
|
||||
//generate a random challenge for use next time
|
||||
byte[] challenge = GenerateChallenge();
|
||||
|
||||
//generate the expected HMAC-SHA1 response for the challenge based on the secret
|
||||
byte[] resp = GenerateResponse(challenge, secret);
|
||||
|
||||
//use the response to encrypt the secret
|
||||
SHA256 sha = SHA256Managed.Create();
|
||||
byte[] key = sha.ComputeHash(resp); // get a 256 bit key from the 160 bit hmac response
|
||||
byte[] secretHash = sha.ComputeHash(secret);
|
||||
|
||||
StandardAesEngine aes = new StandardAesEngine();
|
||||
const uint aesIVLenBytes = 16 ;
|
||||
byte[] IV = CryptoRandom.Instance.GetRandomBytes(aesIVLenBytes);
|
||||
byte[] encrypted;
|
||||
|
||||
using (MemoryStream msEncrypt = new MemoryStream())
|
||||
{
|
||||
using (CryptoStream csEncrypt = (CryptoStream)aes.EncryptStream(msEncrypt, key, IV))
|
||||
{
|
||||
csEncrypt.Write(secret, 0, secret.Length);
|
||||
csEncrypt.Close();
|
||||
}
|
||||
|
||||
encrypted = msEncrypt.ToArray();
|
||||
msEncrypt.Close();
|
||||
}
|
||||
|
||||
ChallengeInfo inf = new ChallengeInfo (encrypted, IV, challenge, secretHash, LT64);
|
||||
|
||||
sha.Clear();
|
||||
|
||||
return inf;
|
||||
}
|
||||
|
||||
private bool DecryptSecret(byte[] yubiResp, ChallengeInfo inf, out byte[] secret)
|
||||
{
|
||||
secret = new byte[keyLenBytes];
|
||||
|
||||
if (inf.IV == null) return false;
|
||||
if (inf.Verification == null) return false;
|
||||
|
||||
//use the response to decrypt the secret
|
||||
SHA256 sha = SHA256Managed.Create();
|
||||
byte[] key = sha.ComputeHash(yubiResp); // get a 256 bit key from the 160 bit hmac response
|
||||
|
||||
StandardAesEngine aes = new StandardAesEngine();
|
||||
|
||||
using (MemoryStream msDecrypt = new MemoryStream(inf.EncryptedSecret))
|
||||
{
|
||||
using (CryptoStream csDecrypt = (CryptoStream)aes.DecryptStream(msDecrypt, key, inf.IV))
|
||||
{
|
||||
csDecrypt.Read(secret, 0, secret.Length);
|
||||
csDecrypt.Close();
|
||||
}
|
||||
msDecrypt.Close();
|
||||
}
|
||||
|
||||
byte[] secretHash = sha.ComputeHash(secret);
|
||||
for (int i = 0; i < secretHash.Length; i++)
|
||||
{
|
||||
if (secretHash[i] != inf.Verification[i])
|
||||
{
|
||||
//wrong response
|
||||
Array.Clear(secret, 0, secret.Length);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//return the secret
|
||||
sha.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The primary access point for challenge-response utility functions. Accepts a pre-populated ChallengeInfo object
|
||||
/// containing at least the IV, EncryptedSecret, and Verification fields. These fields are combined with the Yubikey response
|
||||
/// to decrypt and verify the secret.
|
||||
/// </summary>
|
||||
/// <param name="inf">A pre-populated object containing minimally the IV, EncryptedSecret and Verification fields.
|
||||
/// This should be populated from the database.xml auxilliary file</param>
|
||||
/// <param name="resp" >The Yubikey's response to the issued challenge</param>
|
||||
/// <returns>The common secret, used as a composite key to encrypt a Keepass database</returns>
|
||||
public byte[] GetSecret(ChallengeInfo inf, byte[] resp)
|
||||
{
|
||||
if (resp.Length != responseLenBytes)
|
||||
return null;
|
||||
if (inf == null)
|
||||
return null;
|
||||
if (inf.Challenge == null ||
|
||||
inf.Verification == null)
|
||||
return null;
|
||||
|
||||
LT64 = inf.LT64;
|
||||
|
||||
byte[] secret;
|
||||
|
||||
if (DecryptSecret(resp, inf, out secret))
|
||||
{
|
||||
return secret;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
276
src/keepass2android-app/KeePass.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
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 Android.App;
|
||||
using Android.Content;
|
||||
using Android.Widget;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Content.PM;
|
||||
using Android.Text;
|
||||
using Android.Text.Method;
|
||||
using Java.Lang;
|
||||
using Java.Lang.Reflect;
|
||||
using keepass2android;
|
||||
using KeePassLib.Serialization;
|
||||
using Exception = System.Exception;
|
||||
using String = System.String;
|
||||
/**
|
||||
* General documentation
|
||||
*
|
||||
* Activity stack and activity results
|
||||
* ===================================
|
||||
*
|
||||
* Keepass2Android comprises quite a number of different activities and entry points: The app can be started
|
||||
* using the launcher icon (-> Activity "Keepass"), or by sending a URL (-> SelectCurrentDb) or opening a .kdb(x)-file (->SelectCurrentDb)
|
||||
* There is either only the KeePass activity on stack (no db loaded then) or the first activity on Stack is SelectCurrentDb
|
||||
*
|
||||
* Some possible stacks:
|
||||
* SelectCurrentDb -> Group ( -> Group (subgroups) ... ) -> EntryView -> EntryEdit
|
||||
* (AdvancedSearch Menu) -> Search -> SearchResults -> EntryView -> EntryEdit
|
||||
* (SearchWidget) -> SearchResults -> EntryView -> EntryEdit
|
||||
* SelectCurrentDb -> ShareUrlResults -> EntryView
|
||||
* SelectCurrentDb -> Password / CreateDb
|
||||
*
|
||||
* If the current database changes (e.g. by selecting a search result from another database), the Group/Entry activities of the previously selected database close automatically.
|
||||
* SelectCurrentDb is only noticable by the user if there are actually several databases, otherwises it either closes or starts another activity when it resumes.
|
||||
*
|
||||
* In each of the activities SelectCurrentDb/Group/Entry (but not Password/CreateDb/FileSelect), an AppTask may be present and must be passed to started activities and ActivityResults
|
||||
* must be returned. Therefore, if any Activity calls { StartActivity(newActivity);Finish(); }, it must specify FLAG_ACTIVITY_FORWARD_RESULT.
|
||||
*
|
||||
* Further sub-activities may be opened (e.g. Settings -> ExportDb, ...), but these are not necesarrily
|
||||
* part of the AppTask. Then, neither the task has to be passed nor must the sub-activity return an ActivityResult.
|
||||
*
|
||||
* Activities with AppTasks should check if they get a new AppTask in OnActivityResult.
|
||||
*
|
||||
* Note: Chrome fires the ActionSend (Share URL) intent with NEW_TASK (i.e. KP2A appears in a separate task, either a new one,
|
||||
* or, if it was running before, in the KP2A task), whereas Firefox doesn't specify that flag and KP2A appears "inside" Firefox.
|
||||
* This means that the AppTask must be cleared for use in Chrome after finding an entry or pressing back button in ShareUrlResults.
|
||||
* This would not be necessary for Firefox where the (Android) Task of standalone KP2A is not affected by the search.
|
||||
*/
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Launcher activity of Keepass2Android. This activity usually forwards to SelectCurrentDb but may show the revision dialog after installation or updates.
|
||||
/// </summary>
|
||||
[Activity(Label = AppNames.AppName, MainLauncher = false, Theme = "@style/Kp2aTheme_BlueActionBar", Exported = true)]
|
||||
[IntentFilter(new[] { Intent.ActionMain }, Categories = new[] { "android.intent.category.LAUNCHER", "android.intent.category.MULTIWINDOW_LAUNCHER" })]
|
||||
public class KeePass : LifecycleAwareActivity, IDialogInterfaceOnDismissListener
|
||||
{
|
||||
public const Result ExitNormal = Result.FirstUser;
|
||||
public const Result ExitLock = Result.FirstUser+1;
|
||||
public const Result ExitRefresh = Result.FirstUser+2;
|
||||
public const Result ExitRefreshTitle = Result.FirstUser+3;
|
||||
public const Result ExitCloseAfterTaskComplete = Result.FirstUser+4;
|
||||
public const Result TaskComplete = Result.FirstUser + 5;
|
||||
public const Result ExitReloadDb = Result.FirstUser+6;
|
||||
public const Result ExitClose = Result.FirstUser + 7;
|
||||
public const Result ExitFileStorageSelectionOk = Result.FirstUser + 8;
|
||||
public const Result ResultOkPasswordGenerator = Result.FirstUser + 9;
|
||||
public const Result ExitLoadAnotherDb = Result.FirstUser + 10;
|
||||
public const Result ExitLockByTimeout = Result.FirstUser + 11;
|
||||
|
||||
public const string AndroidAppScheme = "androidapp://";
|
||||
|
||||
|
||||
public const string TagsKey = "@tags";
|
||||
public const string OverrideUrlKey = "@override";
|
||||
public const string ExpDateKey = "@exp_date";
|
||||
|
||||
private AppTask _appTask;
|
||||
private AppTask AppTask
|
||||
{
|
||||
get { return _appTask; }
|
||||
set
|
||||
{
|
||||
_appTask = value;
|
||||
Kp2aLog.LogTask(value, MyDebugName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ActivityDesign _design;
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
_design.ApplyTheme();
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
//see comment to this in PasswordActivity.
|
||||
//Note that this activity is affected even though it's finished when the app is closed because it
|
||||
//seems that the "app launch intent" is re-delivered, so this might end up here.
|
||||
if ((AppTask == null) && (Intent.Flags.HasFlag(ActivityFlags.LaunchedFromHistory)))
|
||||
{
|
||||
AppTask = new NullTask();
|
||||
}
|
||||
else
|
||||
{
|
||||
AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent);
|
||||
}
|
||||
|
||||
|
||||
Kp2aLog.Log("KeePass.OnCreate");
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
Kp2aLog.Log("KeePass.OnResume");
|
||||
_design.ReapplyTheme();
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
Kp2aLog.Log("KeePass.OnStart");
|
||||
|
||||
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
|
||||
|
||||
|
||||
|
||||
bool showChangeLog = false;
|
||||
try
|
||||
{
|
||||
PackageInfo packageInfo = PackageManager.GetPackageInfo(PackageName, 0);
|
||||
int lastInfoVersionCode = prefs.GetInt(GetString(Resource.String.LastInfoVersionCode_key), 0);
|
||||
if (packageInfo.VersionCode > lastInfoVersionCode)
|
||||
{
|
||||
showChangeLog = true;
|
||||
|
||||
ISharedPreferencesEditor edit = prefs.Edit();
|
||||
edit.PutInt(GetString(Resource.String.LastInfoVersionCode_key), packageInfo.VersionCode);
|
||||
EditorCompat.Apply(edit);
|
||||
}
|
||||
|
||||
}
|
||||
catch (PackageManager.NameNotFoundException)
|
||||
{
|
||||
|
||||
}
|
||||
#if DEBUG
|
||||
showChangeLog = false;
|
||||
#endif
|
||||
|
||||
if (showChangeLog)
|
||||
{
|
||||
ChangeLog.ShowChangeLog(this, LaunchNextActivity);
|
||||
}
|
||||
else
|
||||
{
|
||||
LaunchNextActivity();
|
||||
}
|
||||
|
||||
}
|
||||
private static String SELECT_RUNTIME_PROPERTY = "persist.sys.dalvik.vm.lib";
|
||||
private static String LIB_DALVIK = "libdvm.so";
|
||||
private static String LIB_ART = "libart.so";
|
||||
private static String LIB_ART_D = "libartd.so";
|
||||
public static string StartWithTask = "keepass2android.ACTION_START_WITH_TASK";
|
||||
|
||||
public KeePass()
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
private String GetCurrentRuntimeValue()
|
||||
{
|
||||
try
|
||||
{
|
||||
Class systemProperties = Class.ForName("android.os.SystemProperties");
|
||||
try
|
||||
{
|
||||
Method get = systemProperties.GetMethod("get",
|
||||
Class.FromType(typeof (Java.Lang.String)),
|
||||
Class.FromType(typeof (Java.Lang.String)));
|
||||
if (get == null)
|
||||
{
|
||||
return "WTF?!";
|
||||
}
|
||||
try
|
||||
{
|
||||
String value = (String) get.Invoke(
|
||||
systemProperties, SELECT_RUNTIME_PROPERTY,
|
||||
/* Assuming default is */"Dalvik");
|
||||
if (LIB_DALVIK.Equals(value))
|
||||
{
|
||||
return "Dalvik";
|
||||
}
|
||||
else if (LIB_ART.Equals(value))
|
||||
{
|
||||
return "ART";
|
||||
}
|
||||
else if (LIB_ART_D.Equals(value))
|
||||
{
|
||||
return "ART debug build";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
catch (IllegalAccessException e)
|
||||
{
|
||||
return "IllegalAccessException";
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
return "IllegalArgumentException";
|
||||
}
|
||||
catch (InvocationTargetException e)
|
||||
{
|
||||
return "InvocationTargetException";
|
||||
}
|
||||
}
|
||||
catch (NoSuchMethodException e)
|
||||
{
|
||||
return "SystemProperties.get(String key, String def) method is not found";
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException e)
|
||||
{
|
||||
return "SystemProperties class is not found";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void LaunchNextActivity() {
|
||||
|
||||
Intent intent = new Intent(this, typeof(SelectCurrentDbActivity));
|
||||
AppTask.ToIntent(intent);
|
||||
intent.AddFlags(ActivityFlags.ForwardResult);
|
||||
StartActivity(intent);
|
||||
Finish();
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected override void OnDestroy() {
|
||||
Kp2aLog.Log("KeePass.OnDestroy"+IsFinishing.ToString());
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
|
||||
public void OnDismiss(IDialogInterface dialog)
|
||||
{
|
||||
LaunchNextActivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
177
src/keepass2android-app/KpEntryTemplatedEdit.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using KeePassLib;
|
||||
using KeePassLib.Security;
|
||||
using KeePassLib.Utility;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
class KpEntryTemplatedEdit : EditModeBase
|
||||
{
|
||||
internal class KeyOrderComparer : IComparer<string>
|
||||
{
|
||||
private readonly KpEntryTemplatedEdit _kpEntryTemplatedEdit;
|
||||
|
||||
public KeyOrderComparer(KpEntryTemplatedEdit kpEntryTemplatedEdit)
|
||||
{
|
||||
_kpEntryTemplatedEdit = kpEntryTemplatedEdit;
|
||||
}
|
||||
|
||||
public int Compare(string x, string y)
|
||||
{
|
||||
int orderX = _kpEntryTemplatedEdit.GetPosition(x);
|
||||
int orderY = _kpEntryTemplatedEdit.GetPosition(y);
|
||||
if (orderX == orderY)
|
||||
return String.Compare(x, y, StringComparison.CurrentCulture);
|
||||
else
|
||||
return orderX.CompareTo(orderY);
|
||||
}
|
||||
}
|
||||
|
||||
private int GetPosition(string key)
|
||||
{
|
||||
int res;
|
||||
if (!Int32.TryParse(_templateEntry.Strings.ReadSafe("_etm_position_" + key), out res))
|
||||
return Int32.MaxValue;
|
||||
return res;
|
||||
}
|
||||
|
||||
public const string EtmTemplateUuid = "_etm_template_uuid";
|
||||
private const string EtmTitle = "_etm_title_";
|
||||
private readonly PwEntry _entry;
|
||||
private readonly PwEntry _templateEntry;
|
||||
|
||||
public static bool IsTemplated(Database db, PwEntry entry)
|
||||
{
|
||||
if (entry.Strings.Exists(EtmTemplateUuid))
|
||||
{
|
||||
byte[] uuidBytes = MemUtil.HexStringToByteArray(entry.Strings.ReadSafe(EtmTemplateUuid));
|
||||
if (uuidBytes != null)
|
||||
{
|
||||
PwUuid templateUuid = new PwUuid(uuidBytes);
|
||||
return db.EntriesById.ContainsKey(templateUuid);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public KpEntryTemplatedEdit(Database db, PwEntry entry)
|
||||
{
|
||||
_entry = entry;
|
||||
PwUuid templateUuid = new PwUuid(MemUtil.HexStringToByteArray(entry.Strings.ReadSafe(EtmTemplateUuid)));
|
||||
_templateEntry = db.EntriesById[templateUuid];
|
||||
}
|
||||
|
||||
public static void InitializeEntry(PwEntry entry, PwEntry templateEntry)
|
||||
{
|
||||
entry.Strings.Set("_etm_template_uuid", new ProtectedString(false, templateEntry.Uuid.ToHexString()));
|
||||
entry.IconId = templateEntry.IconId;
|
||||
entry.CustomIconUuid = templateEntry.CustomIconUuid;
|
||||
entry.AutoType = templateEntry.AutoType.CloneDeep();
|
||||
entry.Binaries = templateEntry.Binaries.CloneDeep();
|
||||
entry.BackgroundColor = templateEntry.BackgroundColor;
|
||||
entry.ForegroundColor = templateEntry.ForegroundColor;
|
||||
|
||||
foreach (string name in templateEntry.Strings.GetKeys())
|
||||
{
|
||||
if (name.StartsWith(EtmTitle))
|
||||
{
|
||||
String fieldName = name.Substring(EtmTitle.Length);
|
||||
|
||||
if (fieldName.StartsWith("@"))
|
||||
{
|
||||
if (fieldName == KeePass.TagsKey) entry.Tags = templateEntry.Tags;
|
||||
if (fieldName == KeePass.OverrideUrlKey) entry.OverrideUrl = templateEntry.OverrideUrl;
|
||||
if (fieldName == KeePass.ExpDateKey)
|
||||
{
|
||||
entry.Expires = templateEntry.Expires;
|
||||
if (entry.Expires)
|
||||
entry.ExpiryTime = templateEntry.ExpiryTime;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
String type = templateEntry.Strings.ReadSafe("_etm_type_" + fieldName);
|
||||
|
||||
if ((type == "Divider") || (type == "@confirm"))
|
||||
continue;
|
||||
|
||||
bool protectedField = type.StartsWith("Protected");
|
||||
entry.Strings.Set(fieldName, new ProtectedString(protectedField, templateEntry.Strings.ReadSafe(fieldName)));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsVisible(string fieldKey)
|
||||
{
|
||||
if (fieldKey == EtmTemplateUuid)
|
||||
return false;
|
||||
if (fieldKey == PwDefs.TitleField)
|
||||
return true;
|
||||
|
||||
if ((fieldKey.StartsWith("@") || (PwDefs.IsStandardField(fieldKey))))
|
||||
{
|
||||
return !String.IsNullOrEmpty(GetFieldValue(fieldKey))
|
||||
|| _templateEntry.Strings.Exists(EtmTitle+fieldKey);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private string GetFieldValue(string fieldKey)
|
||||
{
|
||||
if (fieldKey == KeePass.ExpDateKey)
|
||||
return _entry.Expires ? _entry.ExpiryTime.ToString(CultureInfo.CurrentUICulture) : "";
|
||||
if (fieldKey == KeePass.OverrideUrlKey)
|
||||
return _entry.OverrideUrl;
|
||||
if (fieldKey == KeePass.TagsKey)
|
||||
return StrUtil.TagsToString(_entry.Tags, true);
|
||||
return _entry.Strings.ReadSafe(fieldKey);
|
||||
}
|
||||
|
||||
public override IEnumerable<string> SortExtraFieldKeys(IEnumerable<string> keys)
|
||||
{
|
||||
var c = new KeyOrderComparer(this);
|
||||
return keys.OrderBy(s => s, c);
|
||||
}
|
||||
|
||||
public override bool ShowAddAttachments
|
||||
{
|
||||
get
|
||||
{
|
||||
if (manualShowAddAttachments != null) return (bool)manualShowAddAttachments;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ShowAddExtras
|
||||
{
|
||||
get {
|
||||
if (manualShowAddExtras != null) return (bool)manualShowAddExtras;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetTitle(string key)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
public override string GetFieldType(string key)
|
||||
{
|
||||
//TODO return "bool" for boolean fields
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static bool IsTemplate(PwEntry entry)
|
||||
{
|
||||
if (entry == null) return false;
|
||||
return entry.Strings.Exists("_etm_template");
|
||||
}
|
||||
}
|
||||
}
|
149
src/keepass2android-app/LifecycleAwareActivity.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll.
|
||||
|
||||
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.Runtime.InteropServices;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
|
||||
public abstract class LifecycleAwareActivity : AndroidX.AppCompat.App.AppCompatActivity
|
||||
{
|
||||
protected override void AttachBaseContext(Context baseContext)
|
||||
{
|
||||
base.AttachBaseContext(LocaleManager.setLocale(baseContext));
|
||||
}
|
||||
|
||||
protected LifecycleAwareActivity(IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected LifecycleAwareActivity()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
string _className;
|
||||
|
||||
string ClassName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_className == null)
|
||||
_className = GetType().Name;
|
||||
return _className;
|
||||
}
|
||||
}
|
||||
|
||||
public string MyDebugName
|
||||
{
|
||||
get { return ClassName + " " + ID; }
|
||||
}
|
||||
|
||||
private static int staticCount = 0;
|
||||
|
||||
private int ID = staticCount++;
|
||||
|
||||
protected override void OnNewIntent(Intent intent)
|
||||
{
|
||||
base.OnNewIntent(intent);
|
||||
Kp2aLog.Log(ClassName + ".OnNewIntent " + ID);
|
||||
}
|
||||
|
||||
public Action<Bundle>? OnCreateListener { get; set; }
|
||||
public Action<Bundle>? OnSaveInstanceStateListener { get; set; }
|
||||
|
||||
public Func<bool?>? OnSupportNavigateUpListener { get; set; }
|
||||
|
||||
public override bool OnSupportNavigateUp()
|
||||
{
|
||||
bool baseRes = base.OnSupportNavigateUp();
|
||||
bool? res = OnSupportNavigateUpListener?.Invoke();
|
||||
if (res != null)
|
||||
return res.Value;
|
||||
return baseRes;
|
||||
}
|
||||
|
||||
public Action? OnResumeListener { get; set; }
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
OnResumeListener?.Invoke();
|
||||
|
||||
Kp2aLog.Log(ClassName + ".OnResume " + ID);
|
||||
if (App.Kp2a.CurrentDb == null)
|
||||
{
|
||||
Kp2aLog.Log(" DB null" + " " + ID);
|
||||
}
|
||||
else
|
||||
{
|
||||
Kp2aLog.Log(" DatabaseIsUnlocked=" + App.Kp2a.DatabaseIsUnlocked + " " + ID);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
ProgressTask.SetNewActiveActivity(this);
|
||||
base.OnStart();
|
||||
Kp2aLog.Log(ClassName + ".OnStart" + " " + ID);
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
|
||||
base.OnCreate(bundle);
|
||||
|
||||
OnCreateListener?.Invoke(bundle);
|
||||
|
||||
Kp2aLog.Log(ClassName + ".OnCreate" + " " + ID);
|
||||
Kp2aLog.Log(ClassName + ":apptask=" + Intent.GetStringExtra("KP2A_APP_TASK_TYPE") + " " + ID);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
Kp2aLog.Log(ClassName + ".OnDestroy" + IsFinishing.ToString() + " " + ID);
|
||||
}
|
||||
|
||||
protected override void OnPause()
|
||||
{
|
||||
base.OnPause();
|
||||
Kp2aLog.Log(ClassName + ".OnPause" + " " + ID);
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
base.OnStop();
|
||||
Kp2aLog.Log(ClassName + ".OnStop" + " " + ID);
|
||||
ProgressTask.RemoveActiveActivity(this);
|
||||
}
|
||||
|
||||
protected override void OnSaveInstanceState(Bundle outState)
|
||||
{
|
||||
base.OnSaveInstanceState(outState);
|
||||
OnSaveInstanceStateListener?.Invoke(outState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
129
src/keepass2android-app/LockCloseActivity.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
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 Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using KeePassLib.Serialization;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for activities displaying sensitive information.
|
||||
/// </summary>
|
||||
/// Checks in OnResume whether the timeout occured and the database must be locked/closed.
|
||||
public class LockCloseActivity : LockingActivity, ILockCloseActivity
|
||||
{
|
||||
|
||||
//the check if the database was locked/closed can be disabled by the caller for activities
|
||||
//which may be used "outside" the database (e.g. GeneratePassword for creating a master password)
|
||||
protected const string NoLockCheck = "NO_LOCK_CHECK";
|
||||
|
||||
protected IOConnectionInfo _ioc;
|
||||
private BroadcastReceiver _intentReceiver;
|
||||
private ActivityDesign _design;
|
||||
|
||||
public LockCloseActivity()
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
protected LockCloseActivity(IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
_design.ApplyTheme();
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
|
||||
Util.MakeSecureDisplay(this);
|
||||
|
||||
|
||||
_ioc = App.Kp2a.CurrentDb?.Ioc;
|
||||
|
||||
if (Intent.GetBooleanExtra(NoLockCheck, false))
|
||||
return;
|
||||
|
||||
_intentReceiver = new LockCloseActivityBroadcastReceiver(this);
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.AddAction(Intents.DatabaseLocked);
|
||||
filter.AddAction(Intent.ActionScreenOff);
|
||||
RegisterReceiver(_intentReceiver, filter, ReceiverFlags.Exported);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
if (Intent.GetBooleanExtra(NoLockCheck, false) == false)
|
||||
{
|
||||
try
|
||||
{
|
||||
UnregisterReceiver(_intentReceiver);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
|
||||
_design.ReapplyTheme();
|
||||
|
||||
if (Intent.GetBooleanExtra(NoLockCheck, false))
|
||||
return;
|
||||
|
||||
if (TimeoutHelper.CheckDbChanged(this, _ioc))
|
||||
{
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
//todo: it seems like OnResume can be called after dismissing a dialog, e.g. the Delete-permanently-Dialog.
|
||||
//in this case the following check might run in parallel with the check performed during the SaveDb check (triggered after the
|
||||
//aforementioned dialog is closed) which can cause odd behavior. However, this is a rare case and hard to resolve so this is currently
|
||||
//accepted. (If the user clicks cancel on the reload-dialog, everything will work.)
|
||||
App.Kp2a.CheckForOpenFileChanged(this);
|
||||
}
|
||||
|
||||
|
||||
public void OnLockDatabase(bool lockedByTimeout)
|
||||
{
|
||||
TimeoutHelper.Lock(this, lockedByTimeout);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,26 @@
|
||||
using Android.Content;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public class LockCloseActivityBroadcastReceiver : BroadcastReceiver
|
||||
{
|
||||
readonly ILockCloseActivity _activity;
|
||||
public LockCloseActivityBroadcastReceiver(ILockCloseActivity activity)
|
||||
{
|
||||
_activity = activity;
|
||||
}
|
||||
|
||||
public override void OnReceive(Context context, Intent intent)
|
||||
{
|
||||
switch (intent.Action)
|
||||
{
|
||||
case Intents.DatabaseLocked:
|
||||
_activity.OnLockDatabase(intent.GetBooleanExtra("ByTimeout", false));
|
||||
break;
|
||||
case Intent.ActionScreenOff:
|
||||
App.Kp2a.OnScreenOff();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
104
src/keepass2android-app/LockCloseListActivity.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
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 Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using KeePassLib.Serialization;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for list activities displaying sensitive information.
|
||||
/// </summary>
|
||||
/// Checks in OnResume whether the timeout occured and the database must be locked/closed.
|
||||
public class LockCloseListActivity : LockingListActivity, ILockCloseActivity
|
||||
{
|
||||
public LockCloseListActivity()
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
IOConnectionInfo _ioc;
|
||||
private BroadcastReceiver _intentReceiver;
|
||||
private ActivityDesign _design;
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
_design.ApplyTheme();
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
|
||||
Util.MakeSecureDisplay(this);
|
||||
|
||||
_ioc = App.Kp2a.CurrentDb.Ioc;
|
||||
|
||||
_intentReceiver = new LockCloseActivityBroadcastReceiver(this);
|
||||
IntentFilter filter = new IntentFilter();
|
||||
|
||||
filter.AddAction(Intents.DatabaseLocked);
|
||||
filter.AddAction(Intent.ActionScreenOff);
|
||||
RegisterReceiver(_intentReceiver, filter, ReceiverFlags.Exported);
|
||||
|
||||
}
|
||||
|
||||
public LockCloseListActivity (IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
_design.ReapplyTheme();
|
||||
|
||||
if (TimeoutHelper.CheckDbChanged(this, _ioc))
|
||||
{
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
//todo: see LockCloseActivity.OnResume
|
||||
App.Kp2a.CheckForOpenFileChanged(this);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
try
|
||||
{
|
||||
UnregisterReceiver(_intentReceiver);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(ex);
|
||||
}
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
|
||||
public void OnLockDatabase(bool lockedByTimeout)
|
||||
{
|
||||
TimeoutHelper.Lock(this, lockedByTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
162
src/keepass2android-app/LockingActivity.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
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.Runtime;
|
||||
using Google.Android.Material.Dialog;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for activities. Notifies the TimeoutHelper whether the app is active or not.
|
||||
/// </summary>
|
||||
public class LockingActivity : LifecycleAwareActivity {
|
||||
|
||||
public LockingActivity (IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public LockingActivity()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
|
||||
var xcKey = App.Kp2a.CurrentDb?.KpDatabase.MasterKey.GetUserKey<ChallengeXCKey>();
|
||||
if (CurrentlyWaitingKey == null && xcKey != null)
|
||||
{
|
||||
CurrentlyWaitingKey = xcKey;
|
||||
}
|
||||
if (CurrentlyWaitingKey != null)
|
||||
{
|
||||
CurrentlyWaitingKey.Activity = this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
base.OnStop();
|
||||
var xcKey = App.Kp2a.CurrentDb?.KpDatabase.MasterKey.GetUserKey<ChallengeXCKey>();
|
||||
if (xcKey != null)
|
||||
{
|
||||
//don't store a pointer to this activity in the static database object to avoid memory leak
|
||||
if (xcKey.Activity == this) //don't reset if another activity has come to foreground already
|
||||
xcKey.Activity = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override void OnPause() {
|
||||
base.OnPause();
|
||||
|
||||
TimeoutHelper.Pause(this);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
protected override void OnResume() {
|
||||
base.OnResume();
|
||||
|
||||
TimeoutHelper.Resume(this);
|
||||
}
|
||||
|
||||
public const int RequestCodeChallengeYubikey = 793;
|
||||
|
||||
//need to store this in the App object to make sure it survives activity recreation
|
||||
protected ChallengeXCKey CurrentlyWaitingKey
|
||||
{
|
||||
get { return App.Kp2a._currentlyWaitingXcKey; }
|
||||
set { App.Kp2a._currentlyWaitingXcKey = value; }
|
||||
}
|
||||
|
||||
|
||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
Kp2aLog.Log("LockingActivity: OnActivityResult " + (requestCode == RequestCodeChallengeYubikey ? "yubichall" : ""));
|
||||
base.OnActivityResult(requestCode, resultCode, data);
|
||||
if ((requestCode == RequestCodeChallengeYubikey) && (CurrentlyWaitingKey != null))
|
||||
{
|
||||
if (resultCode == Result.Ok)
|
||||
{
|
||||
byte[] challengeResponse = data.GetByteArrayExtra("response");
|
||||
if ((challengeResponse != null) && (challengeResponse.Length > 0))
|
||||
{
|
||||
CurrentlyWaitingKey.Response = challengeResponse;
|
||||
}
|
||||
else
|
||||
CurrentlyWaitingKey.Error = "Did not receive a valid response.";
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentlyWaitingKey.Error = "Cancelled Yubichallenge.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Intent TryGetYubichallengeIntentOrPrompt(byte[] challenge, bool promptToInstall)
|
||||
{
|
||||
Intent chalIntent = new Intent("net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE");
|
||||
chalIntent.PutExtra("challenge", challenge);
|
||||
|
||||
IList<ResolveInfo> activities = PackageManager.QueryIntentActivities(chalIntent, 0);
|
||||
bool isIntentSafe = activities.Count > 0;
|
||||
if (isIntentSafe)
|
||||
{
|
||||
return chalIntent;
|
||||
}
|
||||
if (promptToInstall)
|
||||
{
|
||||
MaterialAlertDialogBuilder b = new MaterialAlertDialogBuilder(this);
|
||||
string message = GetString(Resource.String.NoChallengeApp) + " " + GetString(Resource.String.PleaseInstallApp, new Java.Lang.Object[]{"ykDroid"});
|
||||
|
||||
Intent yubichalIntent = new Intent("com.yubichallenge.NFCActivity.CHALLENGE");
|
||||
IList<ResolveInfo> yubichallengeactivities = PackageManager.QueryIntentActivities(yubichalIntent, 0);
|
||||
bool hasYubichallenge = yubichallengeactivities.Count > 0;
|
||||
if (hasYubichallenge)
|
||||
message += " " + GetString(Resource.String.AppOutdated, new Java.Lang.Object[] {"YubiChallenge"});
|
||||
|
||||
b.SetMessage(message);
|
||||
b.SetPositiveButton(Android.Resource.String.Ok,
|
||||
delegate { Util.GotoUrl(this, GetString(Resource.String.MarketURL) + "net.pp3345.ykdroid"); });
|
||||
b.SetNegativeButton(Resource.String.cancel, delegate { });
|
||||
b.Create().Show();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
79
src/keepass2android-app/LockingClosePreferenceActivity.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
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 Android.Content;
|
||||
using Android.OS;
|
||||
using KeePassLib.Serialization;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
|
||||
public class LockingClosePreferenceActivity : LockingPreferenceActivity, ILockCloseActivity
|
||||
{
|
||||
|
||||
|
||||
IOConnectionInfo _ioc;
|
||||
private BroadcastReceiver _intentReceiver;
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
_ioc = App.Kp2a.CurrentDb.Ioc;
|
||||
|
||||
|
||||
_intentReceiver = new LockCloseActivityBroadcastReceiver(this);
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.AddAction(Intents.DatabaseLocked);
|
||||
RegisterReceiver(_intentReceiver, filter, ReceiverFlags.Exported);
|
||||
}
|
||||
|
||||
protected override void OnResume() {
|
||||
base.OnResume();
|
||||
|
||||
if (TimeoutHelper.CheckDbChanged(this, _ioc))
|
||||
{
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
try
|
||||
{
|
||||
UnregisterReceiver(_intentReceiver);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(ex);
|
||||
}
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
|
||||
public void OnLockDatabase(bool lockedByTimeout)
|
||||
{
|
||||
TimeoutHelper.Lock(this, lockedByTimeout);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
91
src/keepass2android-app/LockingListActivity.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
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 Android.App;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for list activities. Notifies the TimeoutHelper whether the app is active or not.
|
||||
/// </summary>
|
||||
public class LockingListActivity : ListActivity {
|
||||
|
||||
|
||||
public LockingListActivity (IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
|
||||
}
|
||||
public LockingListActivity ()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
string _className;
|
||||
string ClassName
|
||||
{
|
||||
get {
|
||||
if (_className == null)
|
||||
_className = GetType().Name;
|
||||
return _className;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
TimeoutHelper.Resume(this);
|
||||
Kp2aLog.Log(ClassName+".OnResume");
|
||||
}
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
Kp2aLog.Log(ClassName+".OnStart");
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
base.OnCreate(bundle);
|
||||
Kp2aLog.Log(ClassName+".OnCreate");
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
GC.Collect();
|
||||
Kp2aLog.Log(ClassName+".OnDestroy"+IsFinishing.ToString());
|
||||
}
|
||||
|
||||
protected override void OnPause()
|
||||
{
|
||||
base.OnPause();
|
||||
TimeoutHelper.Pause(this);
|
||||
Kp2aLog.Log(ClassName+".OnPause");
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
base.OnStop();
|
||||
Kp2aLog.Log(ClassName+".OnStop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
194
src/keepass2android-app/LockingPreferenceActivity.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
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 Android.Content.Res;
|
||||
using Android.Graphics;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Preferences;
|
||||
using Android.Views;
|
||||
using AndroidX.AppCompat.App;
|
||||
using Java.Lang;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
|
||||
public class AppCompatPreferenceActivity: PreferenceActivity
|
||||
{
|
||||
public AppCompatPreferenceActivity(IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public AppCompatPreferenceActivity()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private AppCompatDelegate _appCompatDelegate;
|
||||
|
||||
AppCompatDelegate Delegate
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_appCompatDelegate == null)
|
||||
_appCompatDelegate = AppCompatDelegate.Create(this, null);
|
||||
return _appCompatDelegate;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
Delegate.InstallViewFactory();
|
||||
Delegate.OnCreate(savedInstanceState);
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public override MenuInflater MenuInflater
|
||||
{
|
||||
get { return Delegate.MenuInflater; }
|
||||
}
|
||||
|
||||
|
||||
public override void SetContentView(int layoutResId)
|
||||
{
|
||||
Delegate.SetContentView(layoutResId);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public override void SetContentView(View view) {
|
||||
Delegate.SetContentView(view);
|
||||
}
|
||||
|
||||
public override void SetContentView(View view, ViewGroup.LayoutParams @params) {
|
||||
Delegate.SetContentView(view, @params);
|
||||
}
|
||||
|
||||
public override void AddContentView(View view, ViewGroup.LayoutParams @params) {
|
||||
Delegate.AddContentView(view, @params);
|
||||
}
|
||||
|
||||
protected override void OnPostResume()
|
||||
{
|
||||
base.OnPostResume();
|
||||
Delegate.OnPostResume();
|
||||
}
|
||||
|
||||
protected override void OnTitleChanged(ICharSequence title, Color color)
|
||||
{
|
||||
base.OnTitleChanged(title, color);
|
||||
Delegate.SetTitle(title);
|
||||
}
|
||||
|
||||
|
||||
public override void OnConfigurationChanged(Configuration newConfig)
|
||||
{
|
||||
base.OnConfigurationChanged(newConfig);
|
||||
Delegate.OnConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
base.OnStop();
|
||||
Delegate.OnStop();
|
||||
}
|
||||
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
Delegate.OnDestroy();
|
||||
}
|
||||
|
||||
public override void InvalidateOptionsMenu()
|
||||
{
|
||||
Delegate.InvalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class LockingPreferenceActivity : AppCompatPreferenceActivity {
|
||||
|
||||
public LockingPreferenceActivity (IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
|
||||
}
|
||||
public LockingPreferenceActivity ()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
string _className = null;
|
||||
string ClassName
|
||||
{
|
||||
get {
|
||||
if (_className == null)
|
||||
_className = this.GetType().Name;
|
||||
return _className;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
TimeoutHelper.Resume(this);
|
||||
Kp2aLog.Log(ClassName+".OnResume");
|
||||
}
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
Kp2aLog.Log(ClassName+".OnStart");
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
base.OnCreate(bundle);
|
||||
Kp2aLog.Log(ClassName+".OnCreate");
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
GC.Collect();
|
||||
Kp2aLog.Log(ClassName+".OnDestroy"+IsFinishing.ToString());
|
||||
}
|
||||
|
||||
protected override void OnPause()
|
||||
{
|
||||
base.OnPause();
|
||||
TimeoutHelper.Pause(this);
|
||||
Kp2aLog.Log(ClassName+".OnPause");
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
base.OnStop();
|
||||
Kp2aLog.Log(ClassName+".OnStop");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
270
src/keepass2android-app/Manifests/AndroidManifest_debug.xml
Normal file
@@ -0,0 +1,270 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="178"
|
||||
android:versionName="1.09b-pre0dbg"
|
||||
package="keepass2android.keepass2android_debug"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
|
||||
<queries>
|
||||
<!-- Specific intents and packages we query for (required since Android 11) -->
|
||||
<package android:name="keepass2android.plugin.keyboardswap2" />
|
||||
<package android:name="keepass2android.AncientIconSet" />
|
||||
<package android:name="keepass2android.plugin.qr" />
|
||||
<package android:name="it.andreacioni.kp2a.plugin.keelink" />
|
||||
<package android:name="com.inputstick.apps.kp2aplugin" />
|
||||
<package android:name="com.dropbox.android" />
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.OPEN_DOCUMENT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.GET_DOCUMENT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="com.dropbox.android.AUTHENTICATE_V2" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="com.yubichallenge.NFCActivity.CHALLENGE" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||
<permission android:description="@string/permission_desc2" android:icon="@drawable/ic_notify_locked" android:label="KP2A entry search" android:name="keepass2android.keepass2android_debug.permission.KP2aInternalSearch" android:protectionLevel="signature" />
|
||||
<permission android:description="@string/permission_desc3" android:icon="@drawable/ic_launcher" android:label="KP2A choose autofill dataset" android:name="keepass2android.keepass2android_debug.permission.Kp2aChooseAutofill" android:protectionLevel="signature" />
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher_online"
|
||||
android:roundIcon="@mipmap/ic_launcher_online_round"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
>
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="barcode_ui"/>
|
||||
|
||||
<uses-library
|
||||
android:name="org.apache.http.legacy"
|
||||
android:required="false"/>
|
||||
<activity android:name="microsoft.identity.client.BrowserTabActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="msal8374f801-0f55-407d-80cc-9a04fe86d9b2" android:host="auth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.dropbox.core.android.AuthActivity" android:launchMode="singleTask" android:configChanges="orientation|keyboard" android:exported="true">
|
||||
<intent-filter>
|
||||
<data android:scheme="db-2gormiq7iq1jls1" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<data android:scheme="db-ax0268uydp1ya57" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<provider android:name="group.pals.android.lib.ui.filechooser.providers.localfile.LocalFileProvider" android:authorities="keepass2android.keepass2android_debug.android-filechooser.localfile" android:exported="false" />
|
||||
<provider android:name="group.pals.android.lib.ui.filechooser.providers.history.HistoryProvider" android:authorities="keepass2android.keepass2android_debug.android-filechooser.history" android:exported="false" />
|
||||
<activity android:name="group.pals.android.lib.ui.filechooser.FileChooserActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:screenOrientation="user" android:theme="@style/Afc.Theme.Light">
|
||||
</activity>
|
||||
<service android:name="keepass2android.softkeyboard.KP2AKeyboard" android:permission="android.permission.BIND_INPUT_METHOD" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.view.InputMethod" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.view.im" android:resource="@xml/method" />
|
||||
</service>
|
||||
<activity android:name="keepass2android.softkeyboard.LatinIMESettings" android:label="@string/english_ime_settings" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="keepass2android.softkeyboard.LatinIMESettings" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="keepass2android.softkeyboard.InputLanguageSelection"
|
||||
android:label="language_selection_title"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<action android:name="keepass2android.softkeyboard.INPUT_LANGUAGE_SELECTION"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:configChanges="orientation|keyboard|keyboardHidden" android:label="@string/app_name" android:theme="@style/Kp2aTheme_BlueNoActionBar" android:name="keepass2android.SelectCurrentDbActivity" android:windowSoftInputMode="adjustResize" android:exported="true">
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="kp2a.action.SelectCurrentDbActivity" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<!-- intent filter for opening database files
|
||||
Note that this stopped working nicely with Android 7, see e.g. https://stackoverflow.com/a/26635162/292233
|
||||
KP2A was using
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
previously, but that leaded to complaints by users saying KP2A is showing up way too often, even when opening contacts and the like.
|
||||
|
||||
This is why this was reduced content with mimeType=application/octet-stream or content with pathPattern .
|
||||
|
||||
The scheme=file is still there for old OS devices. It's also queried by apps like Dropbox to find apps for a certain file type.
|
||||
|
||||
-->
|
||||
|
||||
|
||||
<!-- This intent filter is for apps which use content with a URI containing the extension but no specific mimeType, e.g. ASTRO file manager -->
|
||||
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- This intent filter is for apps which use content with a URI not containing the extension but at least specify mimeType=application/octet-stream, e.g. GoogleDrive or FolderSync -->
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<!-- This intent filter is for old OS versions (Android 6 and below) or for apps explicitly querying intents for a certain file:-URI -->
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<intent-filter android:label="@string/kp2a_findUrl">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="keepass2android.ACTION_START_WITH_TASK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<uses-library android:required="false" android:name="com.sec.android.app.multiwindow" />
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W" android:value="632.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_H" android:value="598.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W" android:value="426.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H" android:value="360.0dip" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" android:maxSdkVersion="22" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22" />
|
||||
<uses-permission android:name="keepass2android.keepass2android_debug.permission.KP2aInternalFileBrowsing" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!-- Samsung Pass permission -->
|
||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
||||
<!-- READ_PHONE_STATE seems to come from some library or so, not clear where. We don't want to have it, remove it: -->
|
||||
|
||||
</manifest>
|
56
src/keepass2android-app/Manifests/AndroidManifest_light.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="23" android:versionName="0.8.6" package="keepass2android.keepass2android" android:installLocation="auto">
|
||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="14" />
|
||||
<application android:label="keepass2android" android:icon="@drawable/ic_launcher">
|
||||
<activity android:name="com.dropbox.client2.android.AuthActivity" android:launchMode="singleTask" android:configChanges="orientation|keyboard">
|
||||
<intent-filter>
|
||||
<!-- Change this to be db- followed by your app key -->
|
||||
<data android:scheme="db-i8shu7v1hgh7ynt" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:configChanges="orientation|keyboard|keyboardHidden" android:label="@string/app_name" android:theme="@style/MyTheme" android:name="keepass2android.PasswordActivity">
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="my.yubico.com"
|
||||
android:pathPrefix="/neo"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
287
src/keepass2android-app/Manifests/AndroidManifest_net.xml
Normal file
@@ -0,0 +1,287 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="200"
|
||||
android:versionName="1.11-r0"
|
||||
package="keepass2android.keepass2android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
|
||||
|
||||
<queries>
|
||||
<!-- Specific intents and packages we query for (required since Android 11) -->
|
||||
<package android:name="keepass2android.plugin.keyboardswap2" />
|
||||
<package android:name="keepass2android.AncientIconSet" />
|
||||
<package android:name="keepass2android.plugin.qr" />
|
||||
<package android:name="it.andreacioni.kp2a.plugin.keelink" />
|
||||
<package android:name="com.inputstick.apps.kp2aplugin" />
|
||||
<package android:name="com.dropbox.android" />
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.OPEN_DOCUMENT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.GET_DOCUMENT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="com.dropbox.android.AUTHENTICATE_V2" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="com.yubichallenge.NFCActivity.CHALLENGE" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
</intent>
|
||||
</queries>
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||
|
||||
<permission android:description="@string/permission_desc2" android:icon="@drawable/ic_launcher" android:label="KP2A entry search" android:name="keepass2android.keepass2android.permission.KP2aInternalSearch" android:protectionLevel="signature" />
|
||||
<permission android:description="@string/permission_desc3" android:icon="@drawable/ic_launcher" android:label="KP2A choose autofill dataset" android:name="keepass2android.keepass2android.permission.Kp2aChooseAutofill" android:protectionLevel="signature" />
|
||||
|
||||
<application android:label="keepass2android"
|
||||
android:icon="@mipmap/ic_launcher_online"
|
||||
android:roundIcon="@mipmap/ic_launcher_online_round"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
|
||||
>
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="barcode_ui"/>
|
||||
|
||||
<uses-library
|
||||
android:name="org.apache.http.legacy"
|
||||
android:required="false"/>
|
||||
|
||||
<activity android:name="microsoft.identity.client.BrowserTabActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="msal8374f801-0f55-407d-80cc-9a04fe86d9b2" android:host="auth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.dropbox.core.android.AuthActivity" android:launchMode="singleTask" android:configChanges="orientation|keyboard" android:exported="true">
|
||||
<intent-filter>
|
||||
<data android:scheme="db-i8shu7v1hgh7ynt" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<data android:scheme="db-ax0268uydp1ya57" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<provider android:name="group.pals.android.lib.ui.filechooser.providers.localfile.LocalFileProvider" android:authorities="keepass2android.keepass2android.android-filechooser.localfile" android:exported="false" />
|
||||
<provider android:name="group.pals.android.lib.ui.filechooser.providers.history.HistoryProvider" android:authorities="keepass2android.keepass2android.android-filechooser.history" android:exported="false" />
|
||||
<activity android:name="group.pals.android.lib.ui.filechooser.FileChooserActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:screenOrientation="user" android:theme="@style/Afc.Theme.Light">
|
||||
</activity>
|
||||
|
||||
<service android:name="keepass2android.softkeyboard.KP2AKeyboard" android:permission="android.permission.BIND_INPUT_METHOD" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.view.InputMethod" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.view.im" android:resource="@xml/method" />
|
||||
</service>
|
||||
<activity android:name="keepass2android.softkeyboard.LatinIMESettings" android:label="@string/english_ime_settings" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="keepass2android.softkeyboard.LatinIMESettings" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="keepass2android.softkeyboard.InputLanguageSelection"
|
||||
|
||||
android:exported="true">
|
||||
<!-- android:label="@string/language_selection_title" TODO -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<action android:name="keepass2android.softkeyboard.INPUT_LANGUAGE_SELECTION"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:configChanges="orientation|keyboard|keyboardHidden" android:label="@string/app_name" android:theme="@style/Kp2aTheme_BlueNoActionBar" android:name="keepass2android.SelectCurrentDbActivity" android:windowSoftInputMode="adjustResize" android:exported="true">
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="kp2a.action.SelectCurrentDbActivity" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- intent filter for opening database files
|
||||
Note that this stopped working nicely with Android 7, see e.g. https://stackoverflow.com/a/26635162/292233
|
||||
KP2A was using
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
previously, but that leaded to complaints by users saying KP2A is showing up way too often, even when opening contacts and the like.
|
||||
|
||||
This is why this was reduced content with mimeType=application/octet-stream or content with pathPattern .
|
||||
|
||||
The scheme=file is still there for old OS devices. It's also queried by apps like Dropbox to find apps for a certain file type.
|
||||
|
||||
-->
|
||||
|
||||
|
||||
<!-- This intent filter is for apps which use content with a URI containing the extension but no specific mimeType, e.g. ASTRO file manager -->
|
||||
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- This intent filter is for apps which use content with a URI not containing the extension but at least specify mimeType=application/octet-stream, e.g. GoogleDrive or FolderSync -->
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<!-- This intent filter is for old OS versions (Android 6 and below) or for apps explicitly querying intents for a certain file:-URI -->
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/kp2a_findUrl">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="keepass2android.ACTION_START_WITH_TASK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
x
|
||||
</activity>
|
||||
<uses-library android:required="false" android:name="com.sec.android.app.multiwindow" />
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W" android:value="632.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_H" android:value="598.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W" android:value="426.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H" android:value="360.0dip" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" android:maxSdkVersion="22" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22" />
|
||||
<uses-permission android:name="keepass2android.keepass2android.permission.KP2aInternalFileBrowsing" />
|
||||
<uses-permission android:name="keepass2android.keepass2android.permission.KP2aInternalSearch" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
|
||||
<!-- Samsung Pass permission -->
|
||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
|
||||
</manifest>
|
259
src/keepass2android-app/Manifests/AndroidManifest_nonet.xml
Normal file
@@ -0,0 +1,259 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="200"
|
||||
android:versionName="1.11-r0"
|
||||
package="keepass2android.keepass2android_nonet"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
|
||||
|
||||
<queries>
|
||||
<!-- Specific intents and packages we query for (required since Android 11) -->
|
||||
<package android:name="keepass2android.plugin.keyboardswap2" />
|
||||
<package android:name="keepass2android.AncientIconSet" />
|
||||
<package android:name="com.dropbox.android" />
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.OPEN_DOCUMENT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.GET_DOCUMENT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="com.dropbox.android.AUTHENTICATE_V2" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="com.yubichallenge.NFCActivity.CHALLENGE" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||
<permission android:description="@string/permission_desc2" android:icon="@drawable/ic_launcher_offline" android:label="KP2A entry search" android:name="keepass2android.keepass2android_nonet.permission.KP2aInternalSearch" android:protectionLevel="signature" />
|
||||
<permission android:description="@string/permission_desc3" android:icon="@drawable/ic_launcher_offline" android:label="KP2A choose autofill dataset" android:name="keepass2android.keepass2android_nonet.permission.Kp2aChooseAutofill" android:protectionLevel="signature" />
|
||||
<application
|
||||
android:label="keepass2android"
|
||||
android:icon="@mipmap/ic_launcher_offline"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
>
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="barcode_ui"/>
|
||||
|
||||
<uses-library
|
||||
android:name="org.apache.http.legacy"
|
||||
android:required="false"/>
|
||||
|
||||
<provider android:name="group.pals.android.lib.ui.filechooser.providers.localfile.LocalFileProvider" android:authorities="keepass2android.keepass2android_nonet.android-filechooser.localfile" android:exported="false" />
|
||||
<provider android:name="group.pals.android.lib.ui.filechooser.providers.history.HistoryProvider" android:authorities="keepass2android.keepass2android_nonet.android-filechooser.history" android:exported="false" />
|
||||
<activity android:name="group.pals.android.lib.ui.filechooser.FileChooserActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:screenOrientation="user" android:theme="@style/Afc.Theme.Light">
|
||||
</activity>
|
||||
|
||||
<service android:name="keepass2android.softkeyboard.KP2AKeyboard" android:permission="android.permission.BIND_INPUT_METHOD" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.view.InputMethod" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.view.im" android:resource="@xml/method" />
|
||||
</service>
|
||||
<activity android:name="keepass2android.softkeyboard.LatinIMESettings" android:label="@string/english_ime_settings" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="keepass2android.softkeyboard.LatinIMESettings" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="keepass2android.softkeyboard.InputLanguageSelection"
|
||||
android:label="@string/language_selection_title"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<action android:name="keepass2android.softkeyboard.INPUT_LANGUAGE_SELECTION"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:configChanges="orientation|keyboard|keyboardHidden" android:label="@string/app_name" android:theme="@style/Kp2aTheme_BlueNoActionBar" android:name="keepass2android.SelectCurrentDbActivity" android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="kp2a.action.SelectCurrentDbActivity" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- intent filter for opening database files
|
||||
Note that this stopped working nicely with Android 7, see e.g. https://stackoverflow.com/a/26635162/292233
|
||||
KP2A was using
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
previously, but that leaded to complaints by users saying KP2A is showing up way too often, even when opening contacts and the like.
|
||||
|
||||
This is why this was reduced content with mimeType=application/octet-stream or content with pathPattern .
|
||||
|
||||
The scheme=file is still there for old OS devices. It's also queried by apps like Dropbox to find apps for a certain file type.
|
||||
|
||||
-->
|
||||
|
||||
|
||||
<!-- This intent filter is for apps which use content with a URI containing the extension but no specific mimeType, e.g. ASTRO file manager -->
|
||||
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- This intent filter is for apps which use content with a URI not containing the extension but at least specify mimeType=application/octet-stream, e.g. GoogleDrive or FolderSync -->
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<!-- This intent filter is for old OS versions (Android 6 and below) or for apps explicitly querying intents for a certain file:-URI -->
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbp" />
|
||||
<data android:pathPattern=".*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdb" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="my.yubico.com"
|
||||
android:pathPrefix="/neo"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/kp2a_findUrl">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="keepass2android.ACTION_START_WITH_TASK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<uses-library android:required="false" android:name="com.sec.android.app.multiwindow" />
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W" android:value="632.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_H" android:value="598.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W" android:value="426.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H" android:value="360.0dip" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
|
||||
<uses-permission android:name="keepass2android.keepass2android_nonet.permission.KP2aInternalFileBrowsing" />
|
||||
<uses-permission android:name="keepass2android.keepass2android_nonet.permission.KP2aInternalSearch" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
|
||||
|
||||
</manifest>
|
64
src/keepass2android-app/MeasuringLinearLayout.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using Android.Content;
|
||||
using Android.Runtime;
|
||||
using Android.Util;
|
||||
using Android.Widget;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public class MeasuringLinearLayout : LinearLayout
|
||||
{
|
||||
protected MeasuringLinearLayout(IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
}
|
||||
|
||||
public MeasuringLinearLayout(Context context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
||||
public MeasuringLinearLayout(Context context, IAttributeSet attrs)
|
||||
: base(context, attrs)
|
||||
{
|
||||
}
|
||||
|
||||
public MeasuringLinearLayout(Context context, IAttributeSet attrs, int defStyleAttr)
|
||||
: base(context, attrs, defStyleAttr)
|
||||
{
|
||||
}
|
||||
|
||||
public MeasuringLinearLayout(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes)
|
||||
: base(context, attrs, defStyleAttr, defStyleRes)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public class MeasureArgs
|
||||
{
|
||||
public int ActualHeight;
|
||||
public int ProposedHeight;
|
||||
|
||||
}
|
||||
|
||||
public event EventHandler<MeasureArgs> MeasureEvent;
|
||||
|
||||
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
MeasureArgs args = new MeasureArgs();
|
||||
|
||||
args.ProposedHeight = MeasureSpec.GetSize(heightMeasureSpec);
|
||||
args.ActualHeight = Height;
|
||||
|
||||
|
||||
OnMeasureEvent(args);
|
||||
base.OnMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
protected virtual void OnMeasureEvent(MeasureArgs args)
|
||||
{
|
||||
var handler = MeasureEvent;
|
||||
if (handler != null) handler(this, args);
|
||||
}
|
||||
}
|
||||
}
|
165
src/keepass2android-app/NfcOtpActivity.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Nfc;
|
||||
using Android.OS;
|
||||
using Android.Widget;
|
||||
using Java.Util;
|
||||
using Java.Util.Regex;
|
||||
using Keepass2android.Yubiclip.Scancode;
|
||||
using Pattern = Java.Util.Regex.Pattern;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name",
|
||||
ConfigurationChanges = ConfigChanges.Orientation |
|
||||
ConfigChanges.KeyboardHidden,
|
||||
NoHistory = true,
|
||||
Exported = true,
|
||||
ExcludeFromRecents = true,
|
||||
Theme = "@android:style/Theme.Dialog")]
|
||||
[IntentFilter(new[] { "android.nfc.action.NDEF_DISCOVERED" },
|
||||
Label = "@string/app_name",
|
||||
Categories = new[] { Intent.CategoryDefault },
|
||||
DataHost = "my.yubico.com",
|
||||
DataPathPrefix = "/neo",
|
||||
DataScheme = "https")]
|
||||
[IntentFilter(new[] { "android.nfc.action.NDEF_DISCOVERED", Android.Content.Intent.ActionView },
|
||||
Label = "@string/app_name",
|
||||
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
|
||||
DataHost = "keepass2android.crocoll.net",
|
||||
DataPathPrefix = "/neo",
|
||||
DataScheme = "https",
|
||||
AutoVerify=true)]
|
||||
public class NfcOtpActivity : Activity
|
||||
{
|
||||
private String GetOtpFromIntent(Intent intent)
|
||||
{
|
||||
var patterns = new List<Pattern>{OTP_PATTERN, OTP_PATTERN2};
|
||||
foreach (var pattern in patterns)
|
||||
{
|
||||
|
||||
Matcher matcher = pattern.Matcher(intent.DataString);
|
||||
if (matcher.Matches())
|
||||
{
|
||||
String otp = matcher.Group(1);
|
||||
return otp;
|
||||
}
|
||||
else
|
||||
{
|
||||
IParcelable[] raw = intent.GetParcelableArrayExtra(NfcAdapter.ExtraNdefMessages);
|
||||
byte[] bytes = ((NdefMessage)raw[0]).ToByteArray();
|
||||
if (bytes[0] == URL_NDEF_RECORD && Arrays.Equals(URL_PREFIX_BYTES, Arrays.CopyOfRange(bytes, 3, 3 + URL_PREFIX_BYTES.Length)))
|
||||
{
|
||||
if (Arrays.Equals(new Java.Lang.String("/neo/").GetBytes(), Arrays.CopyOfRange(bytes, 18, 18 + 5)))
|
||||
{
|
||||
bytes[22] = (byte)'#';
|
||||
}
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
if (bytes[i] == '#')
|
||||
{
|
||||
bytes = Arrays.CopyOfRange(bytes, i + 1, bytes.Length);
|
||||
String layout = "US";
|
||||
KeyboardLayout kbd = KeyboardLayout.ForName(layout);
|
||||
String otp = kbd.FromScanCodes(bytes);
|
||||
return otp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
Matcher matcher = OtpPattern.Matcher(data);
|
||||
if (matcher.Matches())
|
||||
{
|
||||
String otp = matcher.Group(1);
|
||||
return otp;
|
||||
}
|
||||
else
|
||||
{
|
||||
IParcelable[] raw = Intent.GetParcelableArrayExtra(NfcAdapter.ExtraNdefMessages);
|
||||
|
||||
byte[] bytes = ((NdefMessage) raw[0]).ToByteArray();
|
||||
bytes = Arrays.CopyOfRange(bytes, DATA_OFFSET, bytes.Length);
|
||||
String layout = "US";
|
||||
KeyboardLayout kbd = KeyboardLayout.ForName(layout);
|
||||
String otp = kbd.FromScanCodes(bytes);
|
||||
return otp;
|
||||
}*/
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static String URL_PREFIX = "https://my.yubico.com/";
|
||||
private static byte URL_NDEF_RECORD = (byte)0xd1;
|
||||
private static byte[] URL_PREFIX_BYTES = new byte[URL_PREFIX.Length + 2 - 8];
|
||||
|
||||
private static Pattern OTP_PATTERN = Pattern.Compile("^https://my\\.yubico\\.com/[a-z]+/#?([a-zA-Z0-9!]+)$");
|
||||
private static Pattern OTP_PATTERN2 = Pattern.Compile("^https://keepass2android\\.crocoll\\.net/[a-z]+/#?([a-zA-Z0-9!]+)$");
|
||||
|
||||
static NfcOtpActivity()
|
||||
{
|
||||
URL_PREFIX_BYTES[0] = 85;
|
||||
URL_PREFIX_BYTES[1] = 4;
|
||||
Java.Lang.JavaSystem.Arraycopy(new Java.Lang.String(URL_PREFIX.Substring(8)).GetBytes(), 0, URL_PREFIX_BYTES, 2, URL_PREFIX_BYTES.Length - 2);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private ActivityDesign _design;
|
||||
|
||||
public NfcOtpActivity()
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
_design.ApplyTheme();
|
||||
base.OnCreate(bundle);
|
||||
|
||||
|
||||
Intent i = new Intent(this, typeof (PasswordActivity));
|
||||
i.SetAction(Intents.StartWithOtp);
|
||||
|
||||
//things to consider:
|
||||
// PasswordActivity should be resumed if currently active -> this is why single top is used and why PasswordActivity is started
|
||||
// If PasswordActivity is not open already, it may be the wrong place to send our OTP to because maybe the user first needs to select
|
||||
// a file (which might require UI action like entering credentials, all of which is handled in FileSelectActivity)
|
||||
// FileSelectActivity is not on the back stack, it finishes itself.
|
||||
// -> PasswordActivity needs to handle this and return to FSA.
|
||||
|
||||
|
||||
i.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
||||
try
|
||||
{
|
||||
string otp = GetOtpFromIntent(Intent);
|
||||
if (otp == null)
|
||||
throw new Exception("Otp must not be null!");
|
||||
i.PutExtra(Intents.OtpExtraKey, otp);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(e);
|
||||
Toast.MakeText(this, "No Yubikey OTP found!", ToastLength.Long).Show();
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
StartActivity(i);
|
||||
Finish();
|
||||
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
_design.ReapplyTheme();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
67
src/keepass2android-app/NoSecureDisplayActivity.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using Android.Preferences;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = AppNames.AppName, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden, Theme = "@style/Kp2aTheme_BlueActionBar",
|
||||
LaunchMode = LaunchMode.SingleInstance)]
|
||||
public class NoSecureDisplayActivity : AndroidX.AppCompat.App.AppCompatActivity
|
||||
{
|
||||
private readonly ActivityDesign _design;
|
||||
|
||||
public NoSecureDisplayActivity()
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
_design.ApplyTheme();
|
||||
base.OnCreate(savedInstanceState);
|
||||
SetContentView(Resource.Layout.no_secure_display_layout);
|
||||
FindViewById<Button>(Resource.Id.btn_goto_settings).Click += (sender, args) =>
|
||||
{
|
||||
AppSettingsActivity.Launch(this);
|
||||
};
|
||||
FindViewById<Button>(Resource.Id.disable_secure_screen_check).Click += (sender, args) =>
|
||||
{
|
||||
var prefs = PreferenceManager.GetDefaultSharedPreferences(LocaleManager.LocalizedAppContext);
|
||||
prefs.Edit()
|
||||
.PutBoolean("no_secure_display_check", true)
|
||||
.Commit();
|
||||
Finish();
|
||||
};
|
||||
FindViewById<Button>(Resource.Id.btn_close).Click += (sender, args) =>
|
||||
{
|
||||
Finish();
|
||||
};
|
||||
|
||||
var toolbar = FindViewById<AndroidX.AppCompat.Widget.Toolbar>(Resource.Id.mytoolbar);
|
||||
|
||||
SetSupportActionBar(toolbar);
|
||||
|
||||
SupportActionBar.Title = AppNames.AppName;
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
_design.ReapplyTheme();
|
||||
//close if displays changed
|
||||
if (!Util.SecureDisplayConfigured(this) || !Util.HasUnsecureDisplay(this))
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
}
|
2272
src/keepass2android-app/PasswordActivity.cs
Normal file
18
src/keepass2android-app/PasswordFont.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Android.Graphics;
|
||||
using Android.Widget;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public class PasswordFont
|
||||
{
|
||||
private static Typeface _passwordFont;
|
||||
|
||||
public void ApplyTo(TextView view)
|
||||
{
|
||||
if (_passwordFont == null)
|
||||
_passwordFont = Typeface.CreateFromAsset(view.Context.Assets, "SourceCodePro-Regular.ttf");
|
||||
|
||||
view.Typeface = _passwordFont;
|
||||
}
|
||||
}
|
||||
}
|
350
src/keepass2android-app/PwGroupListAdapter.cs
Normal file
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
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.Globalization;
|
||||
using Android.Content;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Android.Preferences;
|
||||
using KeePassLib;
|
||||
using keepass2android.view;
|
||||
using keepass2android;
|
||||
using KeePassLib.Interfaces;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public interface IGroupViewSortOrder
|
||||
{
|
||||
int ResourceId { get; }
|
||||
bool RequiresSort { get; }
|
||||
int CompareEntries(PwEntry a, PwEntry b);
|
||||
int CompareGroups(PwGroup a, PwGroup b);
|
||||
}
|
||||
|
||||
class ModDateSortOrder : IGroupViewSortOrder
|
||||
{
|
||||
public int ResourceId
|
||||
{
|
||||
get { return Resource.String.sort_moddate; }
|
||||
}
|
||||
|
||||
public bool RequiresSort
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public int CompareEntries(PwEntry a, PwEntry b)
|
||||
{
|
||||
return -a.LastModificationTime.CompareTo(b.LastModificationTime);
|
||||
}
|
||||
|
||||
public int CompareGroups(PwGroup a, PwGroup b)
|
||||
{
|
||||
return -a.LastModificationTime.CompareTo(b.LastModificationTime);
|
||||
}
|
||||
}
|
||||
class CreationDateSortOrder : IGroupViewSortOrder
|
||||
{
|
||||
public int ResourceId
|
||||
{
|
||||
get { return Resource.String.sort_db; }
|
||||
}
|
||||
|
||||
public bool RequiresSort
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public int CompareEntries(PwEntry a, PwEntry b)
|
||||
{
|
||||
return a.CreationTime.CompareTo(b.CreationTime);
|
||||
}
|
||||
|
||||
public int CompareGroups(PwGroup a, PwGroup b)
|
||||
{
|
||||
return a.CreationTime.CompareTo(b.CreationTime);
|
||||
}
|
||||
}
|
||||
|
||||
public class DefaultSortOrder: IGroupViewSortOrder
|
||||
{
|
||||
public int ResourceId
|
||||
{
|
||||
get { return Resource.String.sort_default; }
|
||||
}
|
||||
|
||||
public bool RequiresSort
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public int CompareEntries(PwEntry a, PwEntry b)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int CompareGroups(PwGroup a, PwGroup b)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
public class NameSortOrder: IGroupViewSortOrder
|
||||
{
|
||||
public int ResourceId
|
||||
{
|
||||
get { return Resource.String.sort_name; }
|
||||
}
|
||||
|
||||
public bool RequiresSort
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public int CompareEntries(PwEntry x, PwEntry y)
|
||||
{
|
||||
String nameX = x.Strings.ReadSafe(PwDefs.TitleField);
|
||||
String nameY = y.Strings.ReadSafe(PwDefs.TitleField);
|
||||
if (nameX.ToLower() != nameY.ToLower())
|
||||
return String.Compare(nameX, nameY, StringComparison.CurrentCultureIgnoreCase);
|
||||
else
|
||||
{
|
||||
if (PwDefs.IsTanEntry(x) && PwDefs.IsTanEntry(y))
|
||||
{
|
||||
//compare the user name fields (=TAN index)
|
||||
String userX = x.Strings.ReadSafe(PwDefs.UserNameField);
|
||||
String userY = y.Strings.ReadSafe(PwDefs.UserNameField);
|
||||
if (userX != userY)
|
||||
{
|
||||
try
|
||||
{
|
||||
return int.Parse(userX).CompareTo(int.Parse(userY));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
//ignore
|
||||
}
|
||||
return String.Compare(userX, userY, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
//use creation time for non-tan entries:
|
||||
|
||||
return x.CreationTime.CompareTo(y.CreationTime);
|
||||
}
|
||||
}
|
||||
|
||||
public int CompareGroups(PwGroup a, PwGroup b)
|
||||
{
|
||||
return String.Compare(a.Name, b.Name, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public class GroupViewSortOrderManager
|
||||
{
|
||||
private readonly Context _context;
|
||||
private readonly IGroupViewSortOrder[] _orders = new IGroupViewSortOrder[] { new DefaultSortOrder(), new NameSortOrder(), new ModDateSortOrder(), new CreationDateSortOrder()};
|
||||
private readonly ISharedPreferences _prefs;
|
||||
|
||||
public GroupViewSortOrderManager(Context context)
|
||||
{
|
||||
_context = context;
|
||||
_prefs = PreferenceManager.GetDefaultSharedPreferences(_context);
|
||||
}
|
||||
|
||||
public IGroupViewSortOrder[] SortOrders
|
||||
{
|
||||
get { return _orders; }
|
||||
}
|
||||
|
||||
public bool SortGroups
|
||||
{
|
||||
get { return true; }
|
||||
//_prefs.GetBoolean(_context.GetString(Resource.String.sortgroups_key), false); }
|
||||
}
|
||||
|
||||
public IGroupViewSortOrder GetCurrentSortOrder()
|
||||
{
|
||||
return SortOrders[GetCurrentSortOrderIndex()];
|
||||
}
|
||||
|
||||
public int GetCurrentSortOrderIndex()
|
||||
{
|
||||
String sortKeyOld = _context.GetString(Resource.String.sort_key_old);
|
||||
String sortKey = _context.GetString(Resource.String.sort_key);
|
||||
|
||||
int sortId = _prefs.GetInt(sortKey, -1);
|
||||
if (sortId == -1)
|
||||
{
|
||||
sortId = _prefs.GetBoolean(sortKeyOld, true) ? 1 : 0;
|
||||
}
|
||||
return sortId;
|
||||
}
|
||||
|
||||
public void SetNewSortOrder(int selectedAfter)
|
||||
{
|
||||
String sortKey = _context.GetString(Resource.String.sort_key);
|
||||
ISharedPreferencesEditor editor = _prefs.Edit();
|
||||
editor.PutInt(sortKey, selectedAfter);
|
||||
//editor.PutBoolean(_context.GetString(Resource.String.sortgroups_key), false);
|
||||
EditorCompat.Apply(editor);
|
||||
}
|
||||
}
|
||||
|
||||
public class PwGroupListAdapter : BaseAdapter
|
||||
{
|
||||
|
||||
private readonly GroupBaseActivity _act;
|
||||
private readonly PwGroup _group;
|
||||
private List<PwGroup> _groupsForViewing;
|
||||
private List<PwEntry> _entriesForViewing;
|
||||
|
||||
public bool InActionMode { get; set; }
|
||||
|
||||
|
||||
public PwGroupListAdapter(GroupBaseActivity act, PwGroup group) {
|
||||
_act = act;
|
||||
_group = group;
|
||||
|
||||
|
||||
FilterAndSort();
|
||||
|
||||
}
|
||||
|
||||
|
||||
public override void NotifyDataSetChanged() {
|
||||
base.NotifyDataSetChanged();
|
||||
|
||||
FilterAndSort();
|
||||
}
|
||||
|
||||
|
||||
public override void NotifyDataSetInvalidated() {
|
||||
base.NotifyDataSetInvalidated();
|
||||
|
||||
FilterAndSort();
|
||||
}
|
||||
|
||||
private void FilterAndSort() {
|
||||
_entriesForViewing = new List<PwEntry>();
|
||||
|
||||
foreach (PwEntry entry in _group.Entries)
|
||||
{
|
||||
_entriesForViewing.Add(entry);
|
||||
}
|
||||
GroupViewSortOrderManager sortOrderManager = new GroupViewSortOrderManager(_act);
|
||||
var sortOrder = sortOrderManager.GetCurrentSortOrder();
|
||||
|
||||
if ( sortOrder.RequiresSort )
|
||||
{
|
||||
var sortGroups = sortOrderManager.SortGroups;
|
||||
_groupsForViewing = new List<PwGroup>(_group.Groups);
|
||||
_groupsForViewing.Sort( (x, y) =>
|
||||
{
|
||||
if (sortGroups)
|
||||
return sortOrder.CompareGroups(x, y);
|
||||
else
|
||||
return String.Compare (x.Name, y.Name, true);
|
||||
});
|
||||
_entriesForViewing.Sort(sortOrder.CompareEntries);
|
||||
} else {
|
||||
_groupsForViewing = new List<PwGroup>(_group.Groups);
|
||||
}
|
||||
}
|
||||
|
||||
public override int Count
|
||||
{
|
||||
get{
|
||||
|
||||
return _groupsForViewing.Count + _entriesForViewing.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public override Java.Lang.Object GetItem(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
public bool IsGroupAtPosition(int position)
|
||||
{
|
||||
return position < _groupsForViewing.Count;
|
||||
}
|
||||
|
||||
public override long GetItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
public override View GetView(int position, View convertView, ViewGroup parent) {
|
||||
int size = _groupsForViewing.Count;
|
||||
GroupListItemView view;
|
||||
if ( position < size ) {
|
||||
view = CreateGroupView(position, convertView);
|
||||
} else {
|
||||
view = CreateEntryView(position - size, convertView);
|
||||
}
|
||||
view.SetRightArrowVisibility(!InActionMode);
|
||||
return view;
|
||||
|
||||
}
|
||||
|
||||
private PwGroupView CreateGroupView(int position, View convertView) {
|
||||
PwGroup g = _groupsForViewing[position];
|
||||
PwGroupView gv;
|
||||
|
||||
if (convertView == null || !(convertView is PwGroupView)) {
|
||||
|
||||
gv = PwGroupView.GetInstance(_act, g);
|
||||
}
|
||||
else {
|
||||
gv = (PwGroupView) convertView;
|
||||
gv.ConvertView(g);
|
||||
|
||||
}
|
||||
|
||||
return gv;
|
||||
}
|
||||
|
||||
private PwEntryView CreateEntryView(int position, View convertView) {
|
||||
PwEntry entry = _entriesForViewing[position];
|
||||
PwEntryView ev;
|
||||
|
||||
if (convertView == null || !(convertView is PwEntryView)) {
|
||||
ev = PwEntryView.GetInstance(_act, entry, position);
|
||||
}
|
||||
else {
|
||||
ev = (PwEntryView) convertView;
|
||||
ev.ConvertView(entry, position);
|
||||
}
|
||||
|
||||
return ev;
|
||||
}
|
||||
|
||||
public IStructureItem GetItemAtPosition(int keyAt)
|
||||
{
|
||||
if (keyAt < _groupsForViewing.Count)
|
||||
{
|
||||
return _groupsForViewing[keyAt];
|
||||
}
|
||||
else
|
||||
{
|
||||
return _entriesForViewing[keyAt - _groupsForViewing.Count];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
210
src/keepass2android-app/QueryCredentialsActivity.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Keepass2android.Pluginsdk;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name",
|
||||
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden,
|
||||
Exported = true,
|
||||
Theme = "@style/Kp2aTheme_ActionBar")]
|
||||
[IntentFilter(new[] { Strings.ActionQueryCredentials},
|
||||
Categories = new[] { Intent.CategoryDefault })]
|
||||
[IntentFilter(new[] { Strings.ActionQueryCredentialsForOwnPackage },
|
||||
Categories = new[] { Intent.CategoryDefault })]
|
||||
public class QueryCredentialsActivity : Activity
|
||||
{
|
||||
private const int RequestCodePluginAccess = 1;
|
||||
private const int RequestCodeQuery = 2;
|
||||
private const string IsRecreate = "isRecreate";
|
||||
private const string StartedQuery = "startedQuery";
|
||||
private bool _startedQuery;
|
||||
private string _requiredScope;
|
||||
private string _requestedUrl;
|
||||
private string _pluginPackage;
|
||||
|
||||
public QueryCredentialsActivity (IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public QueryCredentialsActivity()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
//if launched from history, don't re-use the task. Proceed to FileSelect instead.
|
||||
if (Intent.Flags.HasFlag(ActivityFlags.LaunchedFromHistory))
|
||||
{
|
||||
Kp2aLog.Log("Forwarding to SelectCurrentDbActivity. QueryCredentialsActivity started from history.");
|
||||
Intent intent = new Intent(this, typeof(SelectCurrentDbActivity));
|
||||
intent.AddFlags(ActivityFlags.ForwardResult);
|
||||
StartActivity(intent);
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
_pluginPackage = null;
|
||||
if (CallingActivity != null)
|
||||
_pluginPackage = CallingActivity.PackageName;
|
||||
if (_pluginPackage == null)
|
||||
{
|
||||
Kp2aLog.Log("Couldn't retrieve calling package. Probably activity was started without startActivityForResult()");
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
if (Intent.Action == Strings.ActionQueryCredentialsForOwnPackage)
|
||||
{
|
||||
_requiredScope = Strings.ScopeQueryCredentialsForOwnPackage;
|
||||
_requestedUrl = KeePass.AndroidAppScheme + _pluginPackage;
|
||||
}
|
||||
else if (Intent.Action == Strings.ActionQueryCredentials)
|
||||
{
|
||||
_requiredScope = Strings.ScopeQueryCredentials;
|
||||
_requestedUrl = Intent.GetStringExtra(Strings.ExtraQueryString);
|
||||
}
|
||||
else
|
||||
{
|
||||
Kp2aLog.Log("Invalid action for QueryCredentialsActivity: " + Intent.Action);
|
||||
SetResult(Result.FirstUser);
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
//only start the query or request plugin access when creating the first time.
|
||||
//if we're restarting (after config change or low memory), we will get onActivityResult() later
|
||||
//which will either start the next activity or finish this one.
|
||||
if ((savedInstanceState == null) || (savedInstanceState.GetBoolean(IsRecreate, false) == false))
|
||||
{
|
||||
ShowToast();
|
||||
|
||||
if (new PluginDatabase(this).HasAcceptedScope(_pluginPackage,_requiredScope))
|
||||
{
|
||||
StartQuery();
|
||||
}
|
||||
else
|
||||
{
|
||||
RequestPluginAccess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
Kp2aLog.Log("Starting QueryCredentialsActivity");
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
Kp2aLog.Log("Resuming QueryCredentialsActivity");
|
||||
}
|
||||
|
||||
private void ShowToast()
|
||||
{
|
||||
string pluginDisplayName = _pluginPackage;
|
||||
try
|
||||
{
|
||||
pluginDisplayName = PackageManager.GetApplicationLabel(PackageManager.GetApplicationInfo(_pluginPackage, 0));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(e);
|
||||
}
|
||||
if (String.IsNullOrEmpty(_requestedUrl))
|
||||
Toast.MakeText(this, GetString(Resource.String.query_credentials, new Java.Lang.Object[] {pluginDisplayName}), ToastLength.Long).Show();
|
||||
else
|
||||
Toast.MakeText(this,
|
||||
GetString(Resource.String.query_credentials_for_url,
|
||||
new Java.Lang.Object[] { pluginDisplayName, _requestedUrl }), ToastLength.Long).Show(); ;
|
||||
}
|
||||
|
||||
private void StartQuery()
|
||||
{
|
||||
//launch SelectCurrentDbActivity (which is root of the stack (exception: we're even below!)) with the appropriate task.
|
||||
//will return the results later
|
||||
Intent i = new Intent(this, typeof (SelectCurrentDbActivity));
|
||||
//don't show user notifications when an entry is opened.
|
||||
var task = new SearchUrlTask() {UrlToSearchFor = _requestedUrl, ShowUserNotifications = ActivationCondition.WhenTotp, ActivateKeyboard = ActivationCondition.Never };
|
||||
task.ToIntent(i);
|
||||
StartActivityForResult(i, RequestCodeQuery);
|
||||
_startedQuery = true;
|
||||
}
|
||||
|
||||
private void RequestPluginAccess()
|
||||
{
|
||||
Intent i = new Intent(this, typeof(PluginDetailsActivity));
|
||||
i.SetAction(Strings.ActionEditPluginSettings);
|
||||
i.PutExtra(Strings.ExtraPluginPackage, _pluginPackage);
|
||||
StartActivityForResult(i, RequestCodePluginAccess);
|
||||
}
|
||||
|
||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
base.OnActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == RequestCodePluginAccess)
|
||||
{
|
||||
if (new PluginDatabase(this).HasAcceptedScope(_pluginPackage, _requiredScope))
|
||||
{
|
||||
//user granted access. Search for the requested credentials:
|
||||
StartQuery();
|
||||
}
|
||||
else
|
||||
{
|
||||
//user didn't grant access
|
||||
SetResult(Result.Canceled);
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
if (requestCode == RequestCodeQuery)
|
||||
{
|
||||
if (resultCode == KeePass.ExitCloseAfterTaskComplete)
|
||||
{
|
||||
//double check we really have the permission
|
||||
if (!new PluginDatabase(this).HasAcceptedScope(_pluginPackage, _requiredScope))
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(new Exception("Ohoh! Scope not available, shouldn't get here. Malicious app somewhere?"));
|
||||
SetResult(Result.Canceled);
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
//return credentials to caller:
|
||||
Intent credentialData = new Intent();
|
||||
PluginHost.AddEntryToIntent(credentialData, App.Kp2a.LastOpenedEntry);
|
||||
credentialData.PutExtra(Strings.ExtraQueryString,_requestedUrl);
|
||||
SetResult(Result.Ok, credentialData);
|
||||
Finish();
|
||||
}
|
||||
else
|
||||
{
|
||||
SetResult(Result.Canceled);
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnSaveInstanceState(Bundle outState)
|
||||
{
|
||||
base.OnSaveInstanceState(outState);
|
||||
outState.PutBoolean(StartedQuery, _startedQuery);
|
||||
outState.PutBoolean(IsRecreate, true);
|
||||
}
|
||||
}
|
||||
}
|
508
src/keepass2android-app/QuickUnlock.cs
Normal file
@@ -0,0 +1,508 @@
|
||||
/*
|
||||
This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll.
|
||||
|
||||
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.Linq;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Android.Content.PM;
|
||||
using KeePassLib.Keys;
|
||||
using Android.Preferences;
|
||||
using Android.Runtime;
|
||||
|
||||
using Android.Views.InputMethods;
|
||||
using Google.Android.Material.AppBar;
|
||||
using Google.Android.Material.Dialog;
|
||||
using keepass2android;
|
||||
using KeePassLib;
|
||||
using KeePassLib.Serialization;
|
||||
using Toolbar = AndroidX.AppCompat.Widget.Toolbar;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/app_name",
|
||||
ConfigurationChanges = ConfigChanges.Orientation,
|
||||
WindowSoftInputMode = SoftInput.AdjustResize,
|
||||
MainLauncher = false,
|
||||
Theme = "@style/Kp2aTheme_BlueNoActionBar")]
|
||||
public class QuickUnlock : LifecycleAwareActivity, IBiometricAuthCallback
|
||||
{
|
||||
private IOConnectionInfo _ioc;
|
||||
private QuickUnlockBroadcastReceiver _intentReceiver;
|
||||
private ActivityDesign _design;
|
||||
private IBiometricIdentifier _biometryIdentifier;
|
||||
private int _quickUnlockLength;
|
||||
|
||||
private int numFailedAttempts = 0;
|
||||
private int maxNumFailedAttempts = int.MaxValue;
|
||||
|
||||
public QuickUnlock()
|
||||
{
|
||||
_design = new ActivityDesign(this);
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
_design.ApplyTheme();
|
||||
base.OnCreate(bundle);
|
||||
|
||||
//use FlagSecure to make sure the last (revealed) character of the password is not visible in recent apps
|
||||
Util.MakeSecureDisplay(this);
|
||||
|
||||
_ioc = App.Kp2a.GetDbForQuickUnlock()?.Ioc;
|
||||
|
||||
|
||||
|
||||
if (_ioc == null)
|
||||
{
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
SetContentView(Resource.Layout.QuickUnlock);
|
||||
|
||||
|
||||
var collapsingToolbar = FindViewById<CollapsingToolbarLayout>(Resource.Id.collapsing_toolbar);
|
||||
collapsingToolbar.SetTitle(GetString(Resource.String.QuickUnlock_prefs));
|
||||
SetSupportActionBar(FindViewById<Toolbar>(Resource.Id.toolbar));
|
||||
|
||||
if (App.Kp2a.GetDbForQuickUnlock().KpDatabase.Name != "")
|
||||
{
|
||||
FindViewById(Resource.Id.filename_label).Visibility = ViewStates.Visible;
|
||||
((TextView) FindViewById(Resource.Id.filename_label)).Text = App.Kp2a.GetDbForQuickUnlock().KpDatabase.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (
|
||||
PreferenceManager.GetDefaultSharedPreferences(this)
|
||||
.GetBoolean(GetString(Resource.String.RememberRecentFiles_key),
|
||||
Resources.GetBoolean(Resource.Boolean.RememberRecentFiles_default)))
|
||||
{
|
||||
((TextView) FindViewById(Resource.Id.filename_label)).Text = App.Kp2a.GetFileStorage(_ioc).GetDisplayName(_ioc);
|
||||
}
|
||||
else
|
||||
{
|
||||
((TextView) FindViewById(Resource.Id.filename_label)).Text = "*****";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
TextView txtLabel = (TextView) FindViewById(Resource.Id.QuickUnlock_label);
|
||||
|
||||
_quickUnlockLength = App.Kp2a.QuickUnlockKeyLength;
|
||||
|
||||
bool useUnlockKeyFromDatabase =
|
||||
QuickUnlockFromDatabaseEnabled
|
||||
&& FindQuickUnlockEntry() != null;
|
||||
|
||||
|
||||
if (useUnlockKeyFromDatabase || PreferenceManager.GetDefaultSharedPreferences(this)
|
||||
.GetBoolean(GetString(Resource.String.QuickUnlockHideLength_key), false))
|
||||
{
|
||||
txtLabel.Text = GetString(Resource.String.QuickUnlock_label_secure);
|
||||
}
|
||||
else
|
||||
{
|
||||
txtLabel.Text = GetString(Resource.String.QuickUnlock_label, new Java.Lang.Object[] { _quickUnlockLength });
|
||||
}
|
||||
|
||||
|
||||
EditText pwd = (EditText) FindViewById(Resource.Id.QuickUnlock_password);
|
||||
pwd.SetEms(_quickUnlockLength);
|
||||
Util.MoveBottomBarButtons(Resource.Id.QuickUnlock_buttonLock, Resource.Id.QuickUnlock_button, Resource.Id.bottom_bar, this);
|
||||
|
||||
Button btnUnlock = (Button) FindViewById(Resource.Id.QuickUnlock_button);
|
||||
btnUnlock.Click += (object sender, EventArgs e) =>
|
||||
{
|
||||
OnUnlock(pwd);
|
||||
};
|
||||
|
||||
|
||||
|
||||
Button btnLock = (Button) FindViewById(Resource.Id.QuickUnlock_buttonLock);
|
||||
btnLock.Text = btnLock.Text.Replace("ß", "ss");
|
||||
btnLock.Click += (object sender, EventArgs e) =>
|
||||
{
|
||||
App.Kp2a.Lock(false);
|
||||
Finish();
|
||||
};
|
||||
pwd.EditorAction += (sender, args) =>
|
||||
{
|
||||
if ((args.ActionId == ImeAction.Done) || ((args.ActionId == ImeAction.ImeNull) && (args.Event.Action == KeyEventActions.Down)))
|
||||
OnUnlock(pwd);
|
||||
};
|
||||
|
||||
_intentReceiver = new QuickUnlockBroadcastReceiver(this);
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.AddAction(Intents.DatabaseLocked);
|
||||
RegisterReceiver(_intentReceiver, filter, ReceiverFlags.Exported);
|
||||
|
||||
Util.SetNoPersonalizedLearning(FindViewById<EditText>(Resource.Id.QuickUnlock_password));
|
||||
|
||||
if (bundle != null)
|
||||
numFailedAttempts = bundle.GetInt(NumFailedAttemptsKey, 0);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private bool QuickUnlockFromDatabaseEnabled =>
|
||||
PreferenceManager.GetDefaultSharedPreferences(this)
|
||||
.GetBoolean(GetString(Resource.String.QuickUnlockKeyFromDatabase_key), false);
|
||||
|
||||
private static PwEntry FindQuickUnlockEntry()
|
||||
{
|
||||
return App.Kp2a.GetDbForQuickUnlock()?.KpDatabase?.RootGroup?.Entries.SingleOrDefault(e => e.Strings.GetSafe(PwDefs.TitleField).ReadString() == "QuickUnlock");
|
||||
}
|
||||
|
||||
private const string NumFailedAttemptsKey = "FailedAttempts";
|
||||
|
||||
protected override void OnSaveInstanceState(Bundle outState)
|
||||
{
|
||||
base.OnSaveInstanceState(outState);
|
||||
outState.PutInt(NumFailedAttemptsKey, numFailedAttempts);
|
||||
|
||||
}
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
DonateReminder.ShowDonateReminderIfAppropriate(this);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void OnBiometricError(string message)
|
||||
{
|
||||
Kp2aLog.Log("fingerprint error: " + message);
|
||||
var btn = FindViewById<ImageButton>(Resource.Id.fingerprintbtn);
|
||||
|
||||
btn.SetImageResource(Resource.Drawable.ic_fingerprint_error);
|
||||
btn.PostDelayed(() =>
|
||||
{
|
||||
btn.SetImageResource(Resource.Drawable.baseline_fingerprint_24);
|
||||
|
||||
}, 1300);
|
||||
Toast.MakeText(this, message, ToastLength.Long).Show();
|
||||
}
|
||||
|
||||
|
||||
public void OnBiometricAttemptFailed(string message)
|
||||
{
|
||||
numFailedAttempts++;
|
||||
if (numFailedAttempts >= maxNumFailedAttempts)
|
||||
{
|
||||
FindViewById<ImageButton>(Resource.Id.fingerprintbtn).Visibility = ViewStates.Gone;
|
||||
_biometryIdentifier.StopListening();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBiometricAuthSucceeded()
|
||||
{
|
||||
Kp2aLog.Log("OnFingerprintAuthSucceeded");
|
||||
_biometryIdentifier.StopListening();
|
||||
var btn = FindViewById<ImageButton>(Resource.Id.fingerprintbtn);
|
||||
|
||||
btn.SetImageResource(Resource.Drawable.ic_fingerprint_success);
|
||||
|
||||
EditText pwd = (EditText)FindViewById(Resource.Id.QuickUnlock_password);
|
||||
pwd.Text = ExpectedPasswordPart;
|
||||
|
||||
btn.PostDelayed(() =>
|
||||
{
|
||||
UnlockAndSyncAndClose();
|
||||
}, 500);
|
||||
|
||||
|
||||
}
|
||||
private bool InitFingerprintUnlock()
|
||||
{
|
||||
Kp2aLog.Log("InitFingerprintUnlock");
|
||||
|
||||
if (_biometryIdentifier != null)
|
||||
{
|
||||
Kp2aLog.Log("Already listening for fingerprint!");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
var btn = FindViewById<ImageButton>(Resource.Id.fingerprintbtn);
|
||||
try
|
||||
{
|
||||
FingerprintUnlockMode um;
|
||||
Enum.TryParse(PreferenceManager.GetDefaultSharedPreferences(this).GetString(App.Kp2a.GetDbForQuickUnlock().CurrentFingerprintModePrefKey, ""), out um);
|
||||
btn.Visibility = (um != FingerprintUnlockMode.Disabled) ? ViewStates.Visible : ViewStates.Gone;
|
||||
|
||||
if (um == FingerprintUnlockMode.Disabled)
|
||||
{
|
||||
_biometryIdentifier = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (um == FingerprintUnlockMode.QuickUnlock && Util.GetCloseDatabaseAfterFailedBiometricQuickUnlock(this))
|
||||
{
|
||||
maxNumFailedAttempts = 3;
|
||||
}
|
||||
|
||||
BiometricModule fpModule = new BiometricModule(this);
|
||||
Kp2aLog.Log("fpModule.IsHardwareAvailable=" + fpModule.IsHardwareAvailable);
|
||||
if (fpModule.IsHardwareAvailable) //see FingerprintSetupActivity
|
||||
_biometryIdentifier = new BiometricDecryption(fpModule, App.Kp2a.GetDbForQuickUnlock().CurrentFingerprintPrefKey, this,
|
||||
App.Kp2a.GetDbForQuickUnlock().CurrentFingerprintPrefKey);
|
||||
|
||||
|
||||
if (_biometryIdentifier == null)
|
||||
{
|
||||
FindViewById<ImageButton>(Resource.Id.fingerprintbtn).Visibility = ViewStates.Gone;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (_biometryIdentifier.Init())
|
||||
{
|
||||
Kp2aLog.Log("successfully initialized fingerprint.");
|
||||
btn.SetImageResource(Resource.Drawable.baseline_fingerprint_24);
|
||||
_biometryIdentifier.StartListening(this);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Kp2aLog.Log("failed to initialize fingerprint.");
|
||||
HandleFingerprintKeyInvalidated();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.Log("Error initializing Fingerprint Unlock: " + e);
|
||||
btn.SetImageResource(Resource.Drawable.ic_fingerprint_error);
|
||||
btn.Tag = "Error initializing Fingerprint Unlock: " + e;
|
||||
|
||||
_biometryIdentifier = null;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
private void HandleFingerprintKeyInvalidated()
|
||||
{
|
||||
var btn = FindViewById<ImageButton>(Resource.Id.fingerprintbtn);
|
||||
//key invalidated permanently
|
||||
btn.SetImageResource(Resource.Drawable.ic_fingerprint_error);
|
||||
btn.Tag = GetString(Resource.String.fingerprint_unlock_failed) + " " + GetString(Resource.String.fingerprint_reenable2);
|
||||
_biometryIdentifier = null;
|
||||
}
|
||||
|
||||
private void OnUnlock(EditText pwd)
|
||||
{
|
||||
var expectedPasswordPart = ExpectedPasswordPart;
|
||||
if (pwd.Text == expectedPasswordPart)
|
||||
{
|
||||
UnlockAndSyncAndClose();
|
||||
}
|
||||
else
|
||||
{
|
||||
Kp2aLog.Log("QuickUnlock not successful!");
|
||||
App.Kp2a.Lock(false);
|
||||
Toast.MakeText(this, GetString(Resource.String.QuickUnlock_fail), ToastLength.Long).Show();
|
||||
Finish();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void UnlockAndSyncAndClose()
|
||||
{
|
||||
App.Kp2a.UnlockDatabase();
|
||||
|
||||
if (PreferenceManager.GetDefaultSharedPreferences(this)
|
||||
.GetBoolean(GetString(Resource.String.SyncAfterQuickUnlock_key), false))
|
||||
{
|
||||
new SyncUtil(this).SynchronizeDatabase(Finish);
|
||||
}
|
||||
else
|
||||
Finish();
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private string ExpectedPasswordPart
|
||||
{
|
||||
get
|
||||
{
|
||||
if (QuickUnlockFromDatabaseEnabled)
|
||||
{
|
||||
var quickUnlockEntry = FindQuickUnlockEntry();
|
||||
if (quickUnlockEntry != null)
|
||||
{
|
||||
return quickUnlockEntry.Strings.ReadSafe(PwDefs.PasswordField).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
KcpPassword kcpPassword = (KcpPassword) App.Kp2a.GetDbForQuickUnlock().KpDatabase.MasterKey.GetUserKey(typeof (KcpPassword));
|
||||
String password = kcpPassword.Password.ReadString();
|
||||
|
||||
var passwordStringInfo = new System.Globalization.StringInfo(password);
|
||||
|
||||
int passwordLength = passwordStringInfo.LengthInTextElements;
|
||||
|
||||
String expectedPasswordPart = passwordStringInfo.SubstringByTextElements(Math.Max(0, passwordLength - _quickUnlockLength),
|
||||
Math.Min(passwordLength, _quickUnlockLength));
|
||||
return expectedPasswordPart;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLockDatabase()
|
||||
{
|
||||
CheckIfUnloaded();
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
_design.ReapplyTheme();
|
||||
|
||||
CheckIfUnloaded();
|
||||
|
||||
InitFingerprintUnlock();
|
||||
|
||||
bool showKeyboard = true;
|
||||
|
||||
EditText pwd = (EditText)FindViewById(Resource.Id.QuickUnlock_password);
|
||||
pwd.PostDelayed(() =>
|
||||
{
|
||||
pwd.RequestFocus();
|
||||
InputMethodManager keyboard = (InputMethodManager)GetSystemService(Context.InputMethodService);
|
||||
if (showKeyboard)
|
||||
keyboard.ShowSoftInput(pwd, ShowFlags.Implicit);
|
||||
else
|
||||
keyboard.HideSoftInputFromWindow(pwd.WindowToken, HideSoftInputFlags.ImplicitOnly);
|
||||
}, 50);
|
||||
|
||||
|
||||
var btn = FindViewById<ImageButton>(Resource.Id.fingerprintbtn);
|
||||
btn.Click += (sender, args) =>
|
||||
{
|
||||
if ((_biometryIdentifier != null) && ((_biometryIdentifier.HasUserInterface)|| string.IsNullOrEmpty((string)btn.Tag) ))
|
||||
{
|
||||
_biometryIdentifier.StartListening(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
MaterialAlertDialogBuilder b = new MaterialAlertDialogBuilder(this);
|
||||
b.SetTitle(Resource.String.fingerprint_prefs);
|
||||
b.SetMessage(btn.Tag.ToString());
|
||||
b.SetPositiveButton(Android.Resource.String.Ok, (o, eventArgs) => ((Dialog)o).Dismiss());
|
||||
if (_biometryIdentifier != null)
|
||||
{
|
||||
b.SetNegativeButton(Resource.String.disable_sensor, (senderAlert, alertArgs) =>
|
||||
{
|
||||
btn.SetImageResource(Resource.Drawable.ic_fingerprint_error);
|
||||
_biometryIdentifier?.StopListening();
|
||||
_biometryIdentifier = null;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
b.SetNegativeButton(Resource.String.enable_sensor, (senderAlert, alertArgs) =>
|
||||
{
|
||||
InitFingerprintUnlock();
|
||||
});
|
||||
}
|
||||
b.Show();
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override void OnPause()
|
||||
{
|
||||
if (_biometryIdentifier != null)
|
||||
{
|
||||
Kp2aLog.Log("FP: Stop listening");
|
||||
_biometryIdentifier.StopListening();
|
||||
}
|
||||
|
||||
base.OnPause();
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
try
|
||||
{
|
||||
UnregisterReceiver(_intentReceiver);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void CheckIfUnloaded()
|
||||
{
|
||||
if (App.Kp2a.OpenDatabases.Any() == false)
|
||||
{
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnBackPressed()
|
||||
{
|
||||
SetResult(KeePass.ExitClose);
|
||||
base.OnBackPressed();
|
||||
}
|
||||
|
||||
private class QuickUnlockBroadcastReceiver : BroadcastReceiver
|
||||
{
|
||||
readonly QuickUnlock _activity;
|
||||
public QuickUnlockBroadcastReceiver(QuickUnlock activity)
|
||||
{
|
||||
_activity = activity;
|
||||
}
|
||||
|
||||
public override void OnReceive(Context context, Intent intent)
|
||||
{
|
||||
switch (intent.Action)
|
||||
{
|
||||
case Intents.DatabaseLocked:
|
||||
_activity.OnLockDatabase();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
8
src/keepass2android-app/Resources/anim/anim_enter.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<translate
|
||||
android:fromXDelta="100%" android:toXDelta="0%"
|
||||
android:fromYDelta="0%" android:toYDelta="0%"
|
||||
android:duration="400" />
|
||||
</set>
|
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<translate android:fromXDelta="-100%" android:toXDelta="0%"
|
||||
android:fromYDelta="0%" android:toYDelta="0%"
|
||||
android:duration="250"/>
|
||||
</set>
|
7
src/keepass2android-app/Resources/anim/anim_leave.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<translate android:fromXDelta="0%" android:toXDelta="0%"
|
||||
android:fromYDelta="0%" android:toYDelta="0%"
|
||||
android:duration="400"/>
|
||||
</set>
|
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<translate
|
||||
android:fromXDelta="0%" android:toXDelta="0%"
|
||||
android:fromYDelta="0%" android:toYDelta="0%"
|
||||
android:duration="250" />
|
||||
</set>
|
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/donate_cake.png
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic00.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic01.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic02.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic03.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic04.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic05.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic06.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic07.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic08.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic09.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic10.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic11.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic12.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic13.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic14.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/keepass2android-app/Resources/drawable-mdpi/ic15.png
Normal file
After Width: | Height: | Size: 16 KiB |