Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,44 @@ public static Intent getAddEntryIntent(String outputData, ArrayList<String> prot
}


/**
* Creates and returns an intent to launch Keepass2Android for updating the entry whose field keyField is exactly
* keyValue. The key is a field of the app's choice holding a value the app generated when it created the entry,
* typically a random string, so the user may freely change the title, URL or anything else.
* The entry opens in the editor with the new values filled in; nothing is changed until the user saves. Fields not
* passed stay as they are; a field mapped to null is removed.
* @param keyField Name of the field that identifies the entry, e.g. "MyApp id".
* @param keyValue Exact value of that field.
* @param fields Key/Value pairs of the field values. See KeepassDefs for standard keys.
* @param protectedFields List of keys of the protected fields.
* @param createIfMissing If true and no entry has the key, a new entry is created with the fields and the key field instead.
* @return Intent to start Keepass2Android.
*/
public static Intent getUpdateEntryIntent(String keyField, String keyValue, HashMap<String, String> fields, ArrayList<String> protectedFields, boolean createIfMissing)
{
JSONObject data = new JSONObject();
try
{
for (java.util.Map.Entry<String, String> f : fields.entrySet())
data.put(f.getKey(), f.getValue() == null ? JSONObject.NULL : f.getValue());
}
catch (JSONException e)
{
throw new IllegalArgumentException(e);
}
Intent startKp2aIntent = new Intent(Strings.ACTION_START_WITH_TASK);
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT);
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startKp2aIntent.putExtra("KP2A_APPTASK", "UpdateEntryThenCloseTask");
startKp2aIntent.putExtra("UpdateEntry_KeyField", keyField);
startKp2aIntent.putExtra("UpdateEntry_KeyValue", keyValue);
startKp2aIntent.putExtra("UpdateEntry_CreateIfMissing", String.valueOf(createIfMissing)); //KP2A expects a StringExtra
startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, data.toString());
if (protectedFields != null)
startKp2aIntent.putStringArrayListExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST, protectedFields);
return startKp2aIntent;
}

/**
* Creates an intent to open a Password Entry matching searchText
* @param searchText queryString
Expand Down
17 changes: 16 additions & 1 deletion src/keepass2android-app/EntryEditActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,16 @@ protected override void OnCreate(Bundle savedInstanceState)
}

State.Entry = State.EntryInDatabase.CloneDeep();
if (!State.IsNew)
{
// a task may bring new values for an existing entry: they go into the copy, the database changes only on save
var changed = AppTask.PrepareEditEntry(State.Entry);
if (changed.Count > 0)
{
State.EntryModified = true;
App.Kp2a.ShowMessage(this, GetString(Resource.String.UpdateEntryFieldsChanged, string.Join(", ", changed)), MessageSeverity.Info);
}
}
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))
Expand Down Expand Up @@ -1094,7 +1104,12 @@ void SaveEntry()
}
else
{
runnable = new UpdateEntry(App.Kp2a, initialEntry, newEntry, closeOrShowError);
ActionOnOperationFinished afterUpdateEntry = new ActionInContextInstanceOnOperationFinished(ContextInstanceId, App.Kp2a, (success, message, context) =>
{
if (success && context is EntryEditActivity entryEditActivity)
AppTask.AfterUpdateEntry(entryEditActivity, newEntry);
}, closeOrShowError);
runnable = new UpdateEntry(App.Kp2a, initialEntry, newEntry, afterUpdateEntry);
}
BlockingOperationStarter pt = new BlockingOperationStarter(App.Kp2a, runnable);
pt.Run();
Expand Down
2 changes: 2 additions & 0 deletions src/keepass2android-app/Resources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@
<string name="AutoFillTotp_prefs_ActivateKeyboard_title">Activate built-in keyboard</string>

<string name="TotpCopiedToClipboard">Copied TOTP to clipboard</string>
<string name="UpdateEntryFieldsChanged">Fields changed by the request (saved only when you save): %1$s</string>
<string name="UpdateEntryNotFound">No entry has the requested value in the field %1$s. Nothing was changed.</string>

<string name="ShowKp2aKeyboardNotification_title">KP2A keyboard notification</string>
<string name="ShowKp2aKeyboardNotification_summary">Make full entry accessible through the KP2A keyboard (recommended).</string>
Expand Down
158 changes: 158 additions & 0 deletions src/keepass2android-app/app/AppTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,19 @@ public virtual void PrepareNewEntry(PwEntry newEntry)

}

/// <summary>
/// An existing entry is opened for editing: a task may change the copy being edited before it is shown. Nothing is
/// written until the user saves. Returns the names of the fields it changed (empty if none).
/// </summary>
public virtual IList<string> PrepareEditEntry(PwEntry entryCopy)
{
return Array.Empty<string>();
}

public virtual void AfterUpdateEntry(EntryEditActivity entryEditActivity, PwEntry entry)
{
}

public const String AppTaskKey = "KP2A_APPTASK";

/// <summary>
Expand Down Expand Up @@ -851,6 +864,151 @@ public override void CompleteOnCreateEntryActivity(EntryActivity activity, Threa
}


/// <summary>
/// Another app asks to change fields of an existing entry. The entry is found by a key field: a field of the app's
/// choice (for example "MyApp id") holding a value the app generated when it created the entry, typically a random
/// string, so the user may freely change the title, URL or anything else. The entry opens in the editor with the new
/// values filled in and the changed fields named; nothing is written until the user saves, the same consent as for
/// creating an entry. Fields that are not passed stay as they are; a field passed as JSON null is removed. If no entry
/// has the key, the user is told and nothing changes, or, with <see cref="CreateIfMissingKey"/>, a new entry is created
/// with the fields and the key field instead.
/// Extras: <see cref="KeyFieldKey"/> and <see cref="KeyValueKey"/> (the key field and its exact value), the JSON
/// key/value map in <see cref="AllFieldsKey"/>, the protected field names in <see cref="ProtectedFieldsListKey"/>.
/// </summary>
public class UpdateEntryThenCloseTask : AppTask
{
public const String KeyFieldKey = "UpdateEntry_KeyField";
public const String KeyValueKey = "UpdateEntry_KeyValue";
public const String AllFieldsKey = Keepass2android.Pluginsdk.Strings.ExtraEntryOutputData;
public const String ProtectedFieldsListKey = Keepass2android.Pluginsdk.Strings.ExtraProtectedFieldsList;

/// <summary>"true": if no entry has the key, a new one is created with the fields instead.</summary>
public const String CreateIfMissingKey = "UpdateEntry_CreateIfMissing";

public string KeyField { get; set; }
public string KeyValue { get; set; }
public string AllFields { get; set; }
public IList<string> ProtectedFieldsList { get; set; }
public bool CreateIfMissing { get; set; }

public override bool CanActivateSearchViewOnStart
{
get { return false; }
set { }
}

public override void Setup(Bundle b)
{
KeyField = b.GetString(KeyFieldKey);
KeyValue = b.GetString(KeyValueKey);
AllFields = b.GetString(AllFieldsKey);
ProtectedFieldsList = b.GetStringArrayList(ProtectedFieldsListKey);
CreateIfMissing = GetBoolFromBundle(b, CreateIfMissingKey, false);
}

public override IEnumerable<IExtra> Extras
{
get
{
yield return new StringExtra { Key = CreateIfMissingKey, Value = CreateIfMissing.ToString() };
if (KeyField != null)
yield return new StringExtra { Key = KeyFieldKey, Value = KeyField };
if (KeyValue != null)
yield return new StringExtra { Key = KeyValueKey, Value = KeyValue };
if (AllFields != null)
yield return new StringExtra { Key = AllFieldsKey, Value = AllFields };
if (ProtectedFieldsList != null)
yield return new StringArrayListExtra { Key = ProtectedFieldsListKey, Value = ProtectedFieldsList };
}
}

/// <summary>
/// The entry whose field <see cref="KeyField"/> is exactly <see cref="KeyValue"/>, outside the recycle bin, in any open
/// database.
/// </summary>
PwEntry FindEntry()
{
if (string.IsNullOrEmpty(KeyField) || string.IsNullOrEmpty(KeyValue)) return null;
foreach (var db in App.Kp2a.OpenDatabases)
{
var kp = db.KpDatabase;
var bin = kp.RecycleBinEnabled ? kp.RootGroup.FindGroup(kp.RecycleBinUuid, true) : null;
foreach (var e in db.EntriesById.Values)
{
if (e.Strings.ReadSafe(KeyField) != KeyValue) continue;
if (bin != null && e.IsContainedIn(bin)) continue;
App.Kp2a.CurrentDb = db;
return e;
}
}
return null;
}

public override void LaunchFirstGroupActivity(Activity act)
{
var entry = FindEntry();
if (entry != null)
{
EntryEditActivity.Launch(act, entry, this);
return;
}
if (!CreateIfMissing)
{
App.Kp2a.ShowMessage(act, act.GetString(Resource.String.UpdateEntryNotFound, KeyField), MessageSeverity.Warning);
act.Finish();
return;
}
// no such entry yet: it is created with the same fields (the ones to remove left out) and the key field; the user
// picks its group and saves, as for any new entry
var fields = AllFields == null ? new Org.Json.JSONObject() : new Org.Json.JSONObject(AllFields);
var removed = new List<string>();
for (var iter = fields.Keys(); iter.HasNext;)
{
string key = iter.Next().ToString();
if (fields.IsNull(key)) removed.Add(key);
}
foreach (var key in removed) fields.Remove(key);
if (!string.IsNullOrEmpty(KeyField)) fields.Put(KeyField, KeyValue ?? "");
new CreateEntryThenCloseTask { AllFields = fields.ToString(), ProtectedFieldsList = ProtectedFieldsList, ShowUserNotifications = ActivationCondition.Never }
.LaunchFirstGroupActivity(act);
}

public override IList<string> PrepareEditEntry(PwEntry entryCopy)
{
var changed = new List<string>();
if (AllFields == null) return changed;
var allFields = new Org.Json.JSONObject(AllFields);
for (var iter = allFields.Keys(); iter.HasNext;)
{
string key = iter.Next().ToString();
if (allFields.IsNull(key))
{
// JSON null: the field goes; the standard fields stay, emptied
if (!entryCopy.Strings.Exists(key)) continue;
if (PwDefs.IsStandardField(key)) entryCopy.Strings.Set(key, new ProtectedString(entryCopy.Strings.GetSafe(key).IsProtected, ""));
else entryCopy.Strings.Remove(key);
changed.Add(key);
continue;
}
string value = allFields.Get(key).ToString();
if (entryCopy.Strings.ReadSafe(key) == value) continue;
bool isProtected = ((ProtectedFieldsList != null) && (ProtectedFieldsList.Contains(key)))
|| (key == PwDefs.PasswordField);
entryCopy.Strings.Set(key, new ProtectedString(isProtected, value));
changed.Add(key);
}
return changed;
}

public override void AfterUpdateEntry(EntryEditActivity entryEditActivity, PwEntry entry)
{
EntryActivity.Launch(entryEditActivity, entry, -1,
new SelectEntryTask() { ShowUserNotifications = ActivationCondition.Never, ActivateKeyboard = ActivationCondition.Never },
ActivityFlags.ForwardResult);
}
}


/// <summary>
/// Navigate to a folder and open a Task (appear in SearchResult)
/// </summary>
Expand Down