/*
This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file is based on Keepassdroid, Copyright Brian Pellin.
Keepass2Android is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Keepass2Android is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Keepass2Android. If not, see .
*/
using System;
using System.Collections.Generic;
using System.Linq;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Provider;
using Android.Views;
using Android.Widget;
using Android.Preferences;
using Java.IO;
using KeePassLib.Utility;
using KeePassLib;
using Android.Text;
using KeePassLib.Security;
using Android.Content.PM;
using System.IO;
using System.Globalization;
using System.Net;
using System.Text;
using Android.Content.Res;
using Android.Database;
#if !NO_QR_SCANNER
using Android.Gms.Common;
using Android.Gms.Tasks;
#endif
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Runtime;
using Android.Util;
using Google.Android.Material.Dialog;
using keepass2android.Io;
using KeePassLib.Serialization;
using KeeTrayTOTP.Libraries;
using PluginTOTP;
using Debug = System.Diagnostics.Debug;
using File = System.IO.File;
using Object = Java.Lang.Object;
using Uri = Android.Net.Uri;
using Resource = keepass2android.Resource;
using Google.Android.Material.TextField;
#if !NO_QR_SCANNER
using Xamarin.Google.MLKit.Vision.Barcode.Common;
using Xamarin.Google.MLKit.Vision.CodeScanner;
#endif
using Console = System.Console;
namespace keepass2android
{
[Activity(Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden, Theme = "@style/Kp2aTheme_ActionBar")]
public class EntryEditActivity : LockCloseActivity {
protected override View? SnackbarAnchorView => FindViewById(Resource.Id.main_content);
public const String KeyEntry = "entry";
public const String KeyParent = "parent";
public const String KeyTemplateUuid = "KeyTemplateUuid";
public const int ResultOkIconPicker = (int)Result.FirstUser + 1000;
//Certain additional (=extra) fields may have type="file". These fields show a browse button which triggers the file selection process, involving several activities.
//The same requestCode is used for all such fields. We store the field key to which the last triggered process belongs in the Activity state.
public const int requestCodeSelectFileExtra = 44000;
const string IntentContinueWithEditing = "ContinueWithEditing";
private PasswordFont _passwordFont = new PasswordFont();
EntryEditActivityState State
{
get { return App.Kp2a.EntryEditActivityState; }
}
public static void Launch(Activity act, PwEntry pw, AppTask appTask) {
Intent i = new Intent(act, typeof(EntryEditActivity));
i.PutExtra(KeyEntry, pw.Uuid.ToHexString());
appTask.ToIntent(i);
act.StartActivityForResult(i, 0);
}
public static void Launch(Activity act, PwGroup pw, PwUuid templateUuid, AppTask appTask) {
Intent i = new Intent(act, typeof(EntryEditActivity));
PwGroup parent = pw;
i.PutExtra(KeyParent, parent.Uuid.ToHexString());
i.PutExtra(KeyTemplateUuid, templateUuid.ToHexString());
appTask.ToIntent(i);
act.StartActivityForResult(i, 0);
}
bool _closeForReload;
private AppTask _appTask;
private AppTask AppTask
{
get { return _appTask; }
set
{
_appTask = value;
Kp2aLog.LogTask(value, MyDebugName);
}
}
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
if (LastNonConfigurationInstance != null)
{
//bug in Mono for Android or whatever: after config change the extra fields are wrong
// -> reload:
Reload();
return;
}
AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent);
SetContentView(Resource.Layout.entry_edit);
_closeForReload = false;
Util.SetNoPersonalizedLearning(FindViewById(Resource.Id.entry_scroll));
// Likely the app has been killed exit the activity
if (!App.Kp2a.DatabaseIsUnlocked)
{
Finish();
return;
}
if (Intent.GetBooleanExtra(IntentContinueWithEditing, false))
{
//property "State" will return the state
} else
{
Database db = App.Kp2a.CurrentDb;
App.Kp2a.EntryEditActivityState = new EntryEditActivityState();
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
State.ShowPassword = ! prefs.GetBoolean(GetString(Resource.String.maskpass_key), Resources.GetBoolean(Resource.Boolean.maskpass_default));
Intent i = Intent;
String uuidBytes = i.GetStringExtra(KeyEntry);
PwUuid entryId = PwUuid.Zero;
if (uuidBytes != null)
entryId = new PwUuid(MemUtil.HexStringToByteArray(uuidBytes));
State.ParentGroup = null;
if (entryId.Equals(PwUuid.Zero))
{
//creating new entry
String groupId = i.GetStringExtra(KeyParent);
State.ParentGroup = db.KpDatabase.RootGroup.FindGroup(new PwUuid(MemUtil.HexStringToByteArray(groupId)), true);
PwUuid templateId = new PwUuid(MemUtil.HexStringToByteArray(i.GetStringExtra(KeyTemplateUuid)));
PwEntry templateEntry = null;
if (!PwUuid.Zero.Equals(templateId))
{
templateEntry = db.EntriesById[templateId];
}
if (KpEntryTemplatedEdit.IsTemplate(templateEntry))
{
CreateNewFromKpEntryTemplate(db, templateEntry);
}
else if (templateEntry != null)
{
CreateNewFromStandardTemplate(templateEntry);
}
else
{
CreateNewWithoutTemplate(db);
}
AppTask.PrepareNewEntry(State.EntryInDatabase);
State.IsNew = true;
State.EntryModified = true;
}
else
{
Debug.Assert(entryId != null);
State.EntryInDatabase = db.EntriesById [entryId];
State.IsNew = false;
}
State.Entry = State.EntryInDatabase.CloneDeep();
if (State.Entry.ParentGroup != null && State.Entry.ParentGroup.Name.Equals("AutoOpen", StrUtil.CaseIgnoreCmp))
State.EditMode = new AutoOpenEdit(State.Entry);
else if (KpEntryTemplatedEdit.IsTemplated(db, State.Entry))
State.EditMode = new KpEntryTemplatedEdit(db, State.Entry);
else
State.EditMode = new DefaultEdit();
State.EditMode.InitializeEntry(State.Entry);
}
if (!State.EntryModified)
SetResult(KeePass.ExitNormal);
else
SetResult(KeePass.ExitRefreshTitle);
FillData();
View scrollView = FindViewById(Resource.Id.entry_scroll);
scrollView.ScrollBarStyle = ScrollbarStyles.InsideInset;
ImageButton iconButton = (ImageButton)FindViewById(Resource.Id.icon_button);
if (State.SelectedIcon)
{
App.Kp2a.CurrentDb.DrawableFactory.AssignDrawableTo(iconButton, this, App.Kp2a.CurrentDb.KpDatabase, (PwIcon)State.SelectedIconId, State.SelectedCustomIconId, false);
}
iconButton.Click += (sender, evt) => {
UpdateEntryFromUi(State.Entry);
IconPickerActivity.Launch(this);
};
// Generate password button
FindViewById(Resource.Id.generate_button).Click += (sender, e) =>
{
UpdateEntryFromUi(State.Entry);
GeneratePasswordActivity.Launch(this);
};
if (State.IsNew)
{
SupportActionBar.Title = GetString(Resource.String.add_entry);
}
else
{
SupportActionBar.Title = GetString(Resource.String.edit_entry);
}
SupportActionBar.SetDisplayHomeAsUpEnabled(true);
SupportActionBar.SetHomeButtonEnabled(true);
Button addButton = (Button) FindViewById(Resource.Id.add_advanced);
addButton.Visibility = ViewStates.Visible;
addButton.Click += (sender, e) =>
{
LinearLayout container = (LinearLayout) FindViewById(Resource.Id.advanced_container);
KeyValuePair pair = new KeyValuePair("" , new ProtectedString(true, ""));
View ees = CreateExtraStringView(pair);
container.AddView(ees);
State.EntryModified = true;
/*TextView keyView = (TextView) ees.FindViewById(Resource.Id.title);
keyView.RequestFocus();*/
EditAdvancedString(ees.FindViewById(Resource.Id.edit_extra));
};
SetAddExtraStringEnabled();
Button configureTotpButton = (Button)FindViewById(Resource.Id.configure_totp);
configureTotpButton.Visibility = CanConfigureOtpSettings() ? ViewStates.Gone : ViewStates.Visible;
configureTotpButton.Click += (sender, e) =>
{
bool added = false;
View ees = FindExtraEditSection("otp");
if (ees == null)
{
LinearLayout container = (LinearLayout) FindViewById(Resource.Id.advanced_container);
KeyValuePair pair =
new KeyValuePair("otp", new ProtectedString(true, ""));
ees = CreateExtraStringView(pair);
container.AddView(ees);
added = true;
}
EditTotpString(ees.FindViewById(Resource.Id.edit_extra));
};
FindViewById(Resource.Id.entry_extras_container).Visibility =
State.EditMode.ShowAddExtras || State.Entry.Strings.Any(s => !PwDefs.IsStandardField(s.Key)) ? ViewStates.Visible : ViewStates.Gone;
FindViewById(Resource.Id.entry_binaries_container).Visibility =
State.EditMode.ShowAddAttachments || State.Entry.Binaries.Any() ? ViewStates.Visible : ViewStates.Gone;
((CheckBox)FindViewById(Resource.Id.entry_expires_checkbox)).CheckedChange += (sender, e) =>
{
State.Entry.Expires = e.IsChecked;
if (e.IsChecked)
{
if (State.Entry.ExpiryTime < DateTime.Now)
State.Entry.ExpiryTime = DateTime.Now;
}
UpdateExpires();
State.EntryModified = true;
};
}
private bool hasRequestedKeyboardActivation = false;
protected override void OnStart()
{
base.OnStart();
if (PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean(GetString(Resource.String.UseKp2aKeyboardInKp2a_key), false)
&& (!hasRequestedKeyboardActivation))
{
//only try this once. if the user clicks cancel, we don't want to ask again
// (it may happen that the activity is restarted because of the NewTask flag immediately after the dialog)
hasRequestedKeyboardActivation = true;
CopyToClipboardService.ActivateKeyboard(this);
}
}
void AddBinaryOrAsk(Uri filename)
{
string strItem = GetFileName(filename);
if (String.IsNullOrEmpty(strItem))
strItem = "attachment.bin";
if (State.Entry.Binaries.Get(strItem) != null)
{
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.SetTitle(GetString(Resource.String.AskOverwriteBinary_title));
builder.SetMessage(GetString(Resource.String.AskOverwriteBinary));
builder.SetPositiveButton(GetString(Resource.String.AskOverwriteBinary_yes), (dlgSender, dlgEvt) =>
{
AddBinary(filename, true);
});
builder.SetNegativeButton(GetString(Resource.String.AskOverwriteBinary_no), (dlgSender, dlgEvt) =>
{
AddBinary(filename, false);
});
builder.SetNeutralButton(GetString(Android.Resource.String.Cancel),
(dlgSender, dlgEvt) => { });
Dialog dialog = builder.Create();
dialog.Show();
}
else
AddBinary(filename, true);
}
protected override void OnResume()
{
if (_uriToAddOrAsk != null)
{
AddBinaryOrAsk(_uriToAddOrAsk);
_uriToAddOrAsk = null;
}
base.OnResume();
}
private void CreateNewFromKpEntryTemplate(Database db, PwEntry templateEntry)
{
var entry = new PwEntry(true, true);
KpEntryTemplatedEdit.InitializeEntry(entry, templateEntry);
State.EntryInDatabase = entry;
}
private void CreateNewFromStandardTemplate(PwEntry templateEntry)
{
var newEntry = templateEntry.CloneDeep();
newEntry.SetUuid(new PwUuid(true), true); // Create new UUID
newEntry.CreationTime = newEntry.LastModificationTime = newEntry.LastAccessTime = DateTime.Now;
State.EntryInDatabase = newEntry;
}
private void CreateNewWithoutTemplate(Database db)
{
State.EntryInDatabase = new PwEntry(true, true);
State.EntryInDatabase.Strings.Set(PwDefs.UserNameField, new ProtectedString(
db.KpDatabase.MemoryProtection.ProtectUserName, db.KpDatabase.DefaultUserName));
if ((State.ParentGroup.IconId != PwIcon.Folder) && (State.ParentGroup.IconId != PwIcon.FolderOpen) &&
(State.ParentGroup.IconId != PwIcon.FolderPackage))
{
State.EntryInDatabase.IconId = State.ParentGroup.IconId; // Inherit icon from group
}
else
State.EntryInDatabase.IconId = PwIcon.Key;
State.EntryInDatabase.CustomIconUuid = State.ParentGroup.CustomIconUuid;
}
private void SetAddExtraStringEnabled()
{
((Button)FindViewById(Resource.Id.add_advanced)).Visibility = (!App.Kp2a.CurrentDb.DatabaseFormat.CanHaveCustomFields || !State.EditMode.ShowAddExtras) ? ViewStates.Gone : ViewStates.Visible;
((Button)FindViewById(Resource.Id.configure_totp)).Visibility = CanConfigureOtpSettings() ? ViewStates.Gone : ViewStates.Visible;
}
private bool CanConfigureOtpSettings()
{
return (!App.Kp2a.CurrentDb.DatabaseFormat.CanHaveCustomFields || !State.EditMode.ShowAddExtras)
&& (new Kp2aTotp().TryGetAdapter(new PwEntryOutput(State.Entry, App.Kp2a.CurrentDb)) == null || (State.Entry.Strings.GetKeys().Contains("otp"))) //only allow to edit KeeWeb/KeepassXC style otps
;
}
void SaveEntry()
{
Database db = App.Kp2a.CurrentDb;
EntryEditActivity act = this;
if (!ValidateBeforeSaving())
return;
PwEntry initialEntry = State.EntryInDatabase.CloneDeep();
PwEntry newEntry = State.EntryInDatabase;
//Clone history and re-assign:
newEntry.History = newEntry.History.CloneDeep();
//Based on KeePass Desktop
bool bCreateBackup = (!State.IsNew);
if(bCreateBackup) newEntry.CreateBackup(null);
if (State.SelectedIcon)
{
newEntry.IconId = State.SelectedIconId;
newEntry.CustomIconUuid = State.SelectedCustomIconId;
} //else the State.EntryInDatabase.Icon
/* KPDesktop
if(m_cbCustomForegroundColor.Checked)
newEntry.ForegroundColor = m_clrForeground;
else newEntry.ForegroundColor = Color.Empty;
if(m_cbCustomBackgroundColor.Checked)
newEntry.BackgroundColor = m_clrBackground;
else newEntry.BackgroundColor = Color.Empty;
*/
UpdateEntryFromUi(newEntry);
newEntry.Binaries = State.Entry.Binaries;
newEntry.Expires = State.Entry.Expires;
if (newEntry.Expires)
{
newEntry.ExpiryTime = State.Entry.ExpiryTime;
}
State.EditMode.PrepareForSaving(newEntry);
newEntry.Touch(true, false); // Touch *after* backup
StrUtil.NormalizeNewLines(newEntry.Strings, true);
bool bUndoBackup = false;
PwCompareOptions cmpOpt = (PwCompareOptions.NullEmptyEquivStd |
PwCompareOptions.IgnoreTimes);
if(bCreateBackup) cmpOpt |= PwCompareOptions.IgnoreLastBackup;
if(newEntry.EqualsEntry(initialEntry, cmpOpt, MemProtCmpMode.CustomOnly))
{
// No modifications at all => restore last mod time and undo backup
newEntry.LastModificationTime = initialEntry.LastModificationTime;
bUndoBackup = bCreateBackup;
}
else if(bCreateBackup)
{
// If only history items have been modified (deleted) => undo
// backup, but without restoring the last mod time
PwCompareOptions cmpOptNh = (cmpOpt | PwCompareOptions.IgnoreHistory);
if(newEntry.EqualsEntry(initialEntry, cmpOptNh, MemProtCmpMode.CustomOnly))
bUndoBackup = true;
}
if(bUndoBackup) newEntry.History.RemoveAt(newEntry.History.UCount - 1);
newEntry.MaintainBackups(db.KpDatabase);
//if ( newEntry.Strings.ReadSafe (PwDefs.TitleField).Equals(State.Entry.Strings.ReadSafe (PwDefs.TitleField)) ) {
// SetResult(KeePass.EXIT_REFRESH);
//} else {
//it's safer to always update the title as we might add further information in the title like expiry etc.
SetResult(KeePass.ExitRefreshTitle);
//}
RunnableOnFinish runnable;
ActionOnFinish closeOrShowError = new ActionOnFinish(this, (success, message, activity) => {
if (success)
{
activity?.Finish();
} else
{
OnFinish.DisplayMessage(activity, message, true);
//Re-initialize for editing:
State.EditMode.InitializeEntry(State.Entry);
}
});
//make sure we can close the EntryEditActivity activity even if the app went to background till we get to the OnFinish Action
closeOrShowError.AllowInactiveActivity = true;
ActionOnFinish afterAddEntry = new ActionOnFinish(this, (success, message, activity) =>
{
if (success && activity is EntryEditActivity entryEditActivity)
AppTask.AfterAddNewEntry(entryEditActivity, newEntry);
},closeOrShowError);
if ( State.IsNew ) {
runnable = AddEntry.GetInstance(this, App.Kp2a, newEntry, State.ParentGroup, afterAddEntry, db);
} else {
runnable = new UpdateEntry(this, App.Kp2a, initialEntry, newEntry, closeOrShowError);
}
ProgressTask pt = new ProgressTask(App.Kp2a, act, runnable);
pt.Run();
}
void UpdateEntryFromUi(PwEntry entry)
{
Database db = App.Kp2a.CurrentDb;
EntryEditActivity act = this;
entry.Strings.Set(PwDefs.TitleField, new ProtectedString(db.KpDatabase.MemoryProtection.ProtectTitle,
Util.GetEditText(act, Resource.Id.entry_title)));
entry.Strings.Set(PwDefs.UserNameField, new ProtectedString(db.KpDatabase.MemoryProtection.ProtectUserName,
Util.GetEditText(act, Resource.Id.entry_user_name)));
String pass = Util.GetEditText(act, Resource.Id.entry_password);
byte[] password = StrUtil.Utf8.GetBytes(pass);
entry.Strings.Set(PwDefs.PasswordField, new ProtectedString(db.KpDatabase.MemoryProtection.ProtectPassword,
password));
MemUtil.ZeroByteArray(password);
entry.Strings.Set(PwDefs.UrlField, new ProtectedString(db.KpDatabase.MemoryProtection.ProtectUrl,
Util.GetEditText(act, Resource.Id.entry_url)));
entry.Strings.Set(PwDefs.NotesField, new ProtectedString(db.KpDatabase.MemoryProtection.ProtectNotes,
Util.GetEditText(act, Resource.Id.entry_comment)));
// Validate expiry date
DateTime newExpiry = new DateTime();
if ((State.Entry.Expires) && (!DateTime.TryParse( Util.GetEditText(this,Resource.Id.entry_expires), out newExpiry)))
{
//ignore here
}
else
{
State.Entry.ExpiryTime = newExpiry.ToUniversalTime();
}
// Delete all non standard strings
var keys = entry.Strings.GetKeys();
foreach (String key in keys)
if (PwDefs.IsStandardField(key) == false)
entry.Strings.Remove(key);
LinearLayout container = (LinearLayout) FindViewById(Resource.Id.advanced_container);
for (int index = 0; index < container.ChildCount; index++) {
View view = container.GetChildAt(index);
TextView keyView = (TextView)view.FindViewById(Resource.Id.extrakey);
String key = keyView.Text;
if (String.IsNullOrEmpty(key))
continue;
TextView valueView = (TextView)view.FindViewById(Resource.Id.value);
String value = valueView.Text;
bool protect = ((CheckBox) view.FindViewById(Resource.Id.protection))?.Checked ?? State.EntryInDatabase.Strings.GetSafe(key).IsProtected;
entry.Strings.Set(key, new ProtectedString(protect, value));
}
entry.OverrideUrl = Util.GetEditText(this,Resource.Id.entry_override_url);
List vNewTags = StrUtil.StringToTags(Util.GetEditText(this,Resource.Id.entry_tags));
entry.Tags.Clear();
foreach(string strTag in vNewTags) entry.AddTag(strTag);
/*KPDesktop
m_atConfig.Enabled = m_cbAutoTypeEnabled.Checked;
m_atConfig.ObfuscationOptions = (m_cbAutoTypeObfuscation.Checked ?
AutoTypeObfuscationOptions.UseClipboard :
AutoTypeObfuscationOptions.None);
SaveDefaultSeq();
newEntry.AutoType = m_atConfig;
*/
}
public String GetFileName(Uri uri)
{
String result = null;
if (uri.Scheme.Equals("content"))
{
ICursor cursor = null;
try
{
cursor = ContentResolver.Query(uri, null, null, null, null);
if (cursor != null && cursor.MoveToFirst())
{
result = cursor.GetString(cursor.GetColumnIndex(OpenableColumns.DisplayName));
}
}
catch (Exception e)
{
Kp2aLog.Log(e.ToString());
}
finally
{
if (cursor != null)
cursor.Close();
}
}
if (result == null)
{
result = uri.Path;
int cut = result.LastIndexOf('/');
if (cut != -1)
{
result = result.Substring(cut + 1);
}
cut = result.LastIndexOf('?');
if (cut != -1)
{
result = result.Substring(0, cut);
}
}
return result;
}
void AddBinary(Uri filename, bool overwrite)
{
string strItem = GetFileName(filename);
if (!overwrite)
{
string strFileName = UrlUtil.StripExtension(strItem);
string strExtension = "." + UrlUtil.GetExtension(strItem);
int nTry = 0;
while(true)
{
string strNewName = strFileName + nTry.ToString(CultureInfo.InvariantCulture) + strExtension;
if(State.Entry.Binaries.Get(strNewName) == null)
{
strItem = strNewName;
break;
}
++nTry;
}
}
try
{
byte[] vBytes = null;
try
{
//Android standard way to read the contents (content or file scheme)
vBytes = ReadFully(ContentResolver.OpenInputStream(filename));
}
catch (Exception ex)
{
Kp2aLog.Log(ex.ToString());
//if standard way fails, try to read as a file
vBytes = File.ReadAllBytes(filename.Path);
}
ProtectedBinary pb = new ProtectedBinary(false, vBytes);
State.Entry.Binaries.Set(strItem, pb);
}
catch(Exception exAttach)
{
App.Kp2a.ShowMessage(this, GetString(Resource.String.AttachFailed)+" "+ Util.GetErrorMessage(exAttach), MessageSeverity.Error);
}
State.EntryModified = true;
PopulateBinaries();
}
public static byte[] ReadFully(Stream input)
{
byte[] buffer = new byte[16 * 1024];
using (MemoryStream ms = new MemoryStream())
{
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
}
protected override void OnSaveInstanceState(Bundle outState)
{
base.OnSaveInstanceState(outState);
AppTask.ToBundle(outState);
}
public override void OnBackPressed()
{
if (State.EntryModified == false)
{
base.OnBackPressed();
} else
{
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.SetTitle(GetString(Resource.String.AskDiscardChanges_title));
builder.SetMessage(GetString(Resource.String.AskDiscardChanges));
builder.SetPositiveButton(GetString(Android.Resource.String.Yes), (dlgSender, dlgEvt) =>
{
Finish();
});
builder.SetNegativeButton(GetString(Android.Resource.String.No), (dlgSender, dlgEvt) =>
{
});
Dialog dialog = builder.Create();
dialog.Show();
}
}
public void Reload() {
//this reload ìs necessary to overcome a strange problem with the extra string fields which get lost
//somehow after re-creating the activity. Maybe a Mono for Android bug?
Intent intent = Intent;
intent.PutExtra(IntentContinueWithEditing, true);
//OverridePendingTransition(0, 0);
intent.AddFlags(ActivityFlags.NoAnimation | ActivityFlags.ForwardResult);
_closeForReload = true;
SetResult(KeePass.ExitRefreshTitle); //probably the entry will be modified -> let the EditActivity refresh to be safe
Finish();
//OverridePendingTransition(0, 0);
StartActivity(intent);
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
FileSelectHelper fileSelectHelper = new FileSelectHelper(this, false, true, requestCodeSelectFileExtra);
fileSelectHelper.OnOpen += (sender, info) =>
{
State.EntryModified = true;
var ees = FindExtraEditSection(State.LastTriggeredFileSelectionProcessKey);
(sender as EntryEditActivity ?? this).UpdateFileView(ees, info.Path);
};
if (fileSelectHelper.HandleActivityResult(this, requestCode, resultCode, data))
return;
switch (resultCode)
{
case (Result)ResultOkIconPicker:
State.SelectedIconId = (PwIcon) data.Extras.GetInt(IconPickerActivity.KeyIconId,(int)PwIcon.Key);
State.SelectedCustomIconId = PwUuid.Zero;
String customIconIdString = data.Extras.GetString(IconPickerActivity.KeyCustomIconId);
if (!String.IsNullOrEmpty(customIconIdString))
State.SelectedCustomIconId = new PwUuid(MemUtil.HexStringToByteArray(customIconIdString));
State.SelectedIcon = true;
State.EntryModified = true;
Reload();
return;
case KeePass.ResultOkPasswordGenerator:
String generatedPassword = data.GetStringExtra("keepass2android.password.generated_password");
byte[] password = StrUtil.Utf8.GetBytes(generatedPassword);
State.Entry.Strings.Set(PwDefs.PasswordField, new ProtectedString(App.Kp2a.CurrentDb.KpDatabase.MemoryProtection.ProtectPassword,
password));
MemUtil.ZeroByteArray(password);
State.EntryModified = true;
Reload();
return;
case Result.Ok:
if (requestCode == Intents.RequestCodeFileBrowseForBinary)
{
Uri uri = data.Data;
if (data.Data == null)
{
string s = Util.GetFilenameFromInternalFileChooser(data, this);
if (s == null)
{
App.Kp2a.ShowMessage(this, "No URI retrieved.", MessageSeverity.Error);
return;
}
uri = Uri.Parse(s);
}
_uriToAddOrAsk = uri; //we can't launch a dialog in onActivityResult, so delay this to onResume
}
return;
case Result.Canceled:
Reload();
return;
}
}
View FindExtraEditSection(string key)
{
LinearLayout container = (LinearLayout) FindViewById(Resource.Id.advanced_container);
for (int i = 0; i < container.ChildCount; i++)
{
var ees = container.GetChildAt(i);
var extra_key_view = ees.FindViewById(Resource.Id.extrakey);
if (extra_key_view != null && extra_key_view.Text == key)
{
return ees;
}
}
return null;
}
void PopulateBinaries()
{
ViewGroup binariesGroup = (ViewGroup)FindViewById(Resource.Id.binaries);
binariesGroup.RemoveAllViews();
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.WrapContent);
foreach (KeyValuePair pair in State.Entry.Binaries.OrderBy(p => p.Key) )
{
String key = pair.Key;
String label = key;
if ((String.IsNullOrEmpty(label) || (!App.Kp2a.CurrentDb.DatabaseFormat.SupportsAttachmentKeys)))
{
label = "";
}
//Button binaryButton = new Button(this, null, Resource.Style.EditEntryButton) {Text = label};
Button binaryButton = (Button)LayoutInflater.Inflate(Resource.Layout.EntryEditButtonDelete, null);
binaryButton.Text = label;
//binaryButton.SetCompoundDrawablesWithIntrinsicBounds( Resources.GetDrawable(Android.Resource.Drawable.IcMenuDelete),null, null, null);
binaryButton.Click += (sender, e) =>
{
State.EntryModified = true;
State.Entry.Binaries.Remove(key);
PopulateBinaries();
};
binariesGroup.AddView(binaryButton,layoutParams);
}
//Button addBinaryButton = new Button(this, null, Resource.Style.EditEntryButton ) {Text = GetString(Resource.String.add_binary)};
//addBinaryButton.SetCompoundDrawablesWithIntrinsicBounds( Resources.GetDrawable(Android.Resource.Drawable.IcMenuAdd) , null, null, null);
Button addBinaryButton = (Button)LayoutInflater.Inflate(Resource.Layout.EntryEditButtonAdd, null);
addBinaryButton.Text = GetString(Resource.String.add_binary);
addBinaryButton.Enabled = true;
if (!App.Kp2a.CurrentDb.DatabaseFormat.CanHaveMultipleAttachments)
addBinaryButton.Enabled = !State.Entry.Binaries.Any();
addBinaryButton.Click += (sender, e) =>
{
Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false, true /*force OpenDocument if available, GetContent is not well support starting with Android 7 */);
};
binariesGroup.AddView(addBinaryButton,layoutParams);
var binariesLabel = FindViewById(Resource.Id.entry_binaries_label);
if (binariesLabel != null)
binariesLabel.Visibility = State.Entry.Binaries.UCount > 0 ? ViewStates.Visible : ViewStates.Gone;
binariesGroup.Visibility = State.EditMode.ShowAddAttachments ? ViewStates.Visible : ViewStates.Gone;
}
public override bool OnPrepareOptionsMenu(IMenu menu)
{
Util.PrepareDonateOptionMenu(menu, this);
menu.FindItem(Resource.Id.menu_show_all).SetVisible(_editModeHiddenViews.Any());
return base.OnPrepareOptionsMenu(menu);
}
public override bool OnCreateOptionsMenu(IMenu menu) {
base.OnCreateOptionsMenu(menu);
MenuInflater inflater = MenuInflater;
inflater.Inflate(Resource.Menu.entry_edit, menu);
return true;
}
public override bool OnOptionsItemSelected(IMenuItem item) {
switch ( item.ItemId ) {
case Resource.Id.menu_save:
SaveEntry();
return true;
case Resource.Id.menu_cancel:
Finish();
return true;
case Resource.Id.menu_show_all:
item.SetVisible(false);
foreach (View v in _editModeHiddenViews)
v.Visibility = ViewStates.Visible;
State.EditMode.ShowAddAttachments = true;
State.EditMode.ShowAddExtras = true;
ViewGroup binariesGroup = (ViewGroup)FindViewById(Resource.Id.binaries);
binariesGroup.Visibility = ViewStates.Visible;
FindViewById(Resource.Id.entry_binaries_container).Visibility = ViewStates.Visible;
((Button)FindViewById(Resource.Id.add_advanced)).Visibility = ViewStates.Visible;
((Button)FindViewById(Resource.Id.configure_totp)).Visibility = ViewStates.Visible;
FindViewById(Resource.Id.entry_extras_container).Visibility = ViewStates.Visible;
return true;
case Android.Resource.Id.Home:
OnBackPressed();
return true;
default:
return base.OnOptionsItemSelected(item);
}
}
void UpdateExpires()
{
if (State.Entry.Expires)
{
PopulateText(Resource.Id.entry_expires, getDateTime(State.Entry.ExpiryTime));
}
else
{
PopulateText(Resource.Id.entry_expires, GetString(Resource.String.never));
}
((CheckBox)FindViewById(Resource.Id.entry_expires_checkbox)).Checked = State.Entry.Expires;
FindViewById(Resource.Id.entry_expires).Enabled = State.Entry.Expires;
}
/*
* TODO required??
*
* public override Java.Lang.Object OnRetainNonConfigurationInstance()
{
UpdateEntryFromUi(State.Entry);
return this;
}*/
void UpdateFileView(View ees, string newValue)
{
var valueView = ((TextView)ees.FindViewById(Resource.Id.value));
valueView.Text = newValue;
IFileStorage fileStorage = null;
var ioc = IOConnectionInfo.FromPath(newValue);
try{
fileStorage = App.Kp2a.GetFileStorage(ioc);
}
catch (NoFileStorageFoundException ex)
{
//ignore.
}
ees.FindViewById(Resource.Id.filestorage_display).Visibility =
ees.FindViewById(Resource.Id.filestorage_display).Visibility = (string.IsNullOrEmpty(newValue) && fileStorage != null) ? ViewStates.Gone : ViewStates.Visible;
if (fileStorage != null)
{
int protocolSeparatorPos = ioc.Path.IndexOf("://", StringComparison.Ordinal);
string protocolId = protocolSeparatorPos < 0 ?
"file" : ioc.Path.Substring(0, protocolSeparatorPos);
Drawable drawable = App.Kp2a.GetStorageIcon(protocolId);
ees.FindViewById(Resource.Id.filestorage_logo).SetImageDrawable(drawable);
String fs_title = App.Kp2a.GetStorageDisplayName(protocolId);
ees.FindViewById(Resource.Id.filestorage_label).Text = fs_title;
string displayPath = fileStorage.GetDisplayName(ioc);
protocolSeparatorPos = displayPath.IndexOf("://", StringComparison.Ordinal);
ees.FindViewById(Resource.Id.label_filename).Text = protocolSeparatorPos < 0 ?
displayPath :
displayPath.Substring(protocolSeparatorPos + 3);
}
}
RelativeLayout CreateExtraStringView(KeyValuePair pair, string title = null, string type = "")
{
if (title == null)
title = pair.Key;
if (type == "bool")
{
RelativeLayout ees = (RelativeLayout)LayoutInflater.Inflate(Resource.Layout.entry_edit_section_bool, null);
ees.Tag = pair.Key;
var keyView = ((TextView)ees.FindViewById(Resource.Id.extrakey));
var checkbox = ((CheckBox)ees.FindViewById(Resource.Id.checkbox));
var valueView = ((TextView)ees.FindViewById(Resource.Id.value));
keyView.Text = pair.Key;
checkbox.Checked = pair.Value.ReadString().Equals("True", StrUtil.CaseIgnoreCmp);
checkbox.Text = title;
valueView.Text = checkbox.Checked.ToString();
checkbox.CheckedChange += (sender, e) =>
{
valueView.Text = checkbox.Checked.ToString();
State.EntryModified = true;
};
return ees;
}
else if (type == "file")
{
RelativeLayout ees = (RelativeLayout)LayoutInflater.Inflate(Resource.Layout.entry_edit_section_file, null);
ees.Tag = pair.Key;
var keyView = ((TextView)ees.FindViewById(Resource.Id.extrakey));
var titleView = ((TextView)ees.FindViewById(Resource.Id.title));
keyView.Text = pair.Key;
titleView.Text = title;
UpdateFileView(ees, pair.Value.ReadString());
ees.FindViewById(Resource.Id.btn_change_location).Click += (sender, e) =>
{
State.LastTriggeredFileSelectionProcessKey = pair.Key;
Intent intent = new Intent(this, typeof(FileStorageSelectionActivity));
StartActivityForResult(intent, requestCodeSelectFileExtra);
};
return ees;
}
else
{
RelativeLayout ees = (RelativeLayout)LayoutInflater.Inflate(Resource.Layout.entry_edit_section, null);
ees.Tag = pair.Key;
var keyView = ((TextView)ees.FindViewById(Resource.Id.extrakey));
keyView.Text = pair.Key;
((TextView)ees.FindViewById(Resource.Id.value)).Text = pair.Value.ReadString();
((TextInputLayout)ees.FindViewById(Resource.Id.value_container)).Hint = pair.Key;
((TextView)ees.FindViewById(Resource.Id.value)).TextChanged += (sender, e) => State.EntryModified = true;
_passwordFont.ApplyTo(((TextView)ees.FindViewById(Resource.Id.value)));
((CheckBox)ees.FindViewById(Resource.Id.protection)).Checked = pair.Value.IsProtected;
//ees.FindViewById(Resource.Id.edit_extra).Click += (sender, e) => DeleteAdvancedString((View)sender);
ees.FindViewById(Resource.Id.edit_extra).Click += (sender, e) => EditAdvancedString(ees.FindViewById(Resource.Id.edit_extra));
return ees;
}
}
private string[] _additionalKeys = null;
private List _editModeHiddenViews;
private Uri _uriToAddOrAsk;
public string[] AdditionalKeys
{
get
{
if (_additionalKeys == null)
{
_additionalKeys = App.Kp2a.CurrentDb.EntriesById
.Select(kvp => kvp.Value)
.SelectMany(x => x.Strings.GetKeys().Where(k => !PwDefs.IsStandardField(k)))
.Where(k => (k != null) && !k.StartsWith("_etm_") )
.Distinct()
.ToArray();
}
return _additionalKeys;
}
}
private void EditTotpString(View sender)
{
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
View dlgView = LayoutInflater.Inflate(Resource.Layout.
configure_totp_dialog, null);
builder.SetView(dlgView);
builder.SetNegativeButton(Android.Resource.String.Cancel, (o, args) => { });
builder.SetPositiveButton(Android.Resource.String.Ok, (o, args) =>
{
var targetField = ((TextView)((View)sender.Parent).FindViewById(Resource.Id.value));
if (targetField != null)
{
string entryTitle = Util.GetEditText(this, Resource.Id.entry_title);
string username = Util.GetEditText(this, Resource.Id.entry_user_name);
string secret = dlgView.FindViewById(Resource.Id.totp_secret_key).Text;
string totpLength = dlgView.FindViewById(Resource.Id.totp_length).Text;
string timeStep = dlgView.FindViewById(Resource.Id.totp_time_step).Text;
var checkedTotpId = (int)dlgView.FindViewById(Resource.Id.totp_encoding).CheckedRadioButtonId;
TotpEncoding encoding = (checkedTotpId == Resource.Id.totp_encoding_steam)
? TotpEncoding.Steam : (checkedTotpId == Resource.Id.totp_encoding_rfc6238 ? TotpEncoding.Default : TotpEncoding.Custom);
var algorithm = (int)dlgView.FindViewById(Resource.Id.totp_algorithm).SelectedItemPosition;
targetField.Text = BuildOtpString(entryTitle, username, secret, totpLength, timeStep, encoding, algorithm);
}
else
{
App.Kp2a.ShowMessage(this, "did not find target field", MessageSeverity.Error);
}
//not calling State.Entry.Strings.Set(...). We only do this when the user saves the changes.
State.EntryModified = true;
});
Dialog dialog = builder.Create();
dlgView.FindViewById(Resource.Id.totp_encoding_custom).CheckedChange += (o, args) =>
{
dlgView.FindViewById(Resource.Id.totp_custom_settings_group).Visibility = args.IsChecked ? ViewStates.Visible : ViewStates.Gone;
};
#if NO_QR_SCANNER
dlgView.FindViewById