Files
keepass2android/src/keepass2android-app/app/AppTask.cs
2025-04-08 10:37:40 +02:00

1090 lines
33 KiB
C#

using System;
using System.Globalization;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Widget;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using KeePassLib;
using KeePassLib.Security;
using KeePassLib.Utility;
using KeeTrayTOTP.Libraries;
using Android.Content.Res;
using Android.Preferences;
using Google.Android.Material.Dialog;
using keepass2android;
namespace keepass2android
{
/// <summary>
/// Interface for data stored in an intent or bundle as extra string
/// </summary>
public interface IExtra
{
/// <summary>
/// put data to a bundle by calling one of the PutXX methods
/// </summary>
void ToBundle(Bundle b);
/// <summary>
/// Put data to an intent by calling PutExtra
/// </summary>
void ToIntent(Intent i);
}
/// <summary>
/// represents data stored in an intent or bundle as extra string
/// </summary>
public class StringExtra: IExtra
{
public string Key { get; set; }
public string Value{ get; set; }
#region IExtra implementation
public void ToBundle(Bundle b)
{
b.PutString(Key, Value);
}
public void ToIntent(Intent i)
{
i.PutExtra(Key, Value);
}
#endregion
}
/// <summary>
/// represents data stored in an intent or bundle as extra string array list
/// </summary>
public class StringArrayListExtra : IExtra
{
public string Key { get; set; }
public IList<string> Value { get; set; }
#region IExtra implementation
public void ToBundle(Bundle b)
{
b.PutStringArrayList(Key, Value);
}
public void ToIntent(Intent i)
{
i.PutStringArrayListExtra(Key, Value);
}
#endregion
}
/// <summary>
/// represents data stored in an intent or bundle as extra int
/// </summary>
public class IntExtra: IExtra
{
public string Key { get; set; }
public int Value{ get; set; }
#region IExtra implementation
public void ToBundle(Bundle b)
{
b.PutInt(Key, Value);
}
public void ToIntent(Intent i)
{
i.PutExtra(Key, Value);
}
#endregion
}
/// <summary>
/// represents data stored in an intent or bundle as extra bool
/// </summary>
public class BoolExtra: IExtra
{
public string Key { get; set; }
public bool Value{ get; set; }
#region IExtra implementation
public void ToBundle(Bundle b)
{
b.PutBoolean(Key, Value);
}
public void ToIntent(Intent i)
{
i.PutExtra(Key, Value);
}
#endregion
}
/// <summary>
/// represents data stored in an intent or bundle as extra string array
/// </summary>
public class StringArrayExtra : IExtra
{
public string Key { get; set; }
public string[] Value { get; set; }
#region IExtra implementation
public void ToBundle(Bundle b)
{
b.PutStringArray(Key, Value);
}
public void ToIntent(Intent i)
{
i.PutExtra(Key, Value);
}
#endregion
}
/// <summary>
/// base class for "tasks": these are things the user wants to do and which require several activities
/// </summary>
/// Therefore AppTasks need to be serializable to bundles and intents to "survive" saving to instance state and changing activities.
/// An AppTask has a type and may have several parameters ("extras").
/// Activities call the task at special points so tasks can change the behaviour at these points.
public abstract class AppTask
{
/// <summary>
/// Loads the parameters of the task from the given bundle
/// </summary>
public virtual void Setup(Bundle b)
{
CanActivateSearchViewOnStart = b.GetBoolean(CanActivateSearchViewOnStartKey, true);
}
public const String CanActivateSearchViewOnStartKey = "CanActivateSearchViewOnStart";
/// <summary>
/// Can be overwritten to indicate that it is not desired to bring up the search view when starting a groupactivity
/// </summary>
public virtual bool CanActivateSearchViewOnStart
{
get;
set;
}
/// <summary>
/// Returns the parameters of the task for storage in a bundle or intent
/// </summary>
/// <value>The extras.</value>
public virtual IEnumerable<IExtra> Extras
{
get
{
yield return new BoolExtra { Key = CanActivateSearchViewOnStartKey, Value = CanActivateSearchViewOnStart };
}
}
public virtual void LaunchFirstGroupActivity(Activity act)
{
GroupActivity.Launch(act, this, new ActivityLaunchModeRequestCode(0));
}
public virtual void AfterAddNewEntry(EntryEditActivity entryEditActivity, PwEntry newEntry)
{
}
public virtual void PrepareNewEntry(PwEntry newEntry)
{
}
public const String AppTaskKey = "KP2A_APPTASK";
/// <summary>
/// Should be used in OnCreate to (re)create a task
/// if savedInstanceState is not null, the task is recreated from there. Otherwise it's taken from the intent.
/// </summary>
public static AppTask GetTaskInOnCreate(Bundle savedInstanceState, Intent intent)
{
AppTask task;
if (savedInstanceState != null)
{
task = CreateFromBundle(savedInstanceState);
}
else
{
task = CreateFromIntent(intent);
}
Kp2aLog.Log("Loaded task " + task);
return task;
}
public static AppTask CreateFromIntent(Intent i)
{
return CreateFromBundle(i.Extras);
}
public static AppTask CreateFromBundle(Bundle b)
{
return CreateFromBundle(b, new NullTask());
}
public static AppTask CreateFromBundle(Bundle b, AppTask failureReturn)
{
if (b == null)
return failureReturn;
string taskType = b.GetString(AppTaskKey);
if (string.IsNullOrEmpty(taskType))
return failureReturn;
try
{
Type type = Type.GetType("keepass2android." + taskType);
if (type == null)
return failureReturn;
AppTask task = (AppTask)Activator.CreateInstance(type);
task.Setup(b);
return task;
}
catch (Exception e)
{
Kp2aLog.Log("Cannot convert " + taskType + " in task: " + e);
return failureReturn;
}
}
/// <summary>
/// Adds the extras of the task to the intent
/// </summary>
public void ToIntent(Intent intent)
{
GetTypeExtra(GetType()).ToIntent(intent);
foreach (IExtra extra in Extras)
{
extra.ToIntent(intent);
}
}
/// <summary>
/// Adds the extras of the task to the bundle
/// </summary>
public void ToBundle(Bundle bundle)
{
GetTypeExtra(GetType()).ToBundle(bundle);
foreach (IExtra extra in Extras)
{
extra.ToBundle(bundle);
}
}
/// <summary>
/// Returns an IExtra which must be part of the Extras of a task to describe the type
/// </summary>
static IExtra GetTypeExtra(Type type)
{
return new StringExtra { Key=AppTaskKey, Value=type.Name};
}
public virtual void StartInGroupActivity(GroupBaseActivity groupBaseActivity)
{
}
public virtual void SetupGroupBaseActivityButtons(GroupBaseActivity groupBaseActivity)
{
groupBaseActivity.SetupNormalButtons();
}
public void SetActivityResult(Activity activity, Result result)
{
Intent data = new Intent();
ToIntent(data);
activity.SetResult(result, data);
}
/// <summary>
/// Tries to extract the task from the data given as an Intent object in OnActivityResult. If successful, the task is assigned,
/// otherwise, false is returned.
/// </summary>
public static bool TryGetFromActivityResult(Intent data, ref AppTask task)
{
if (data == null)
{
Kp2aLog.Log("TryGetFromActivityResult: no data");
return false;
}
AppTask tempTask = CreateFromBundle(data.Extras, null);
if (tempTask == null)
{
Kp2aLog.Log("No AppTask in OnActivityResult");
return false;
}
task = tempTask;
Kp2aLog.Log("AppTask " +task+" in OnActivityResult");
return true;
}
protected void RemoveTaskFromIntent(Activity act)
{
if (act.Intent != null)
act.Intent.RemoveExtra(AppTaskKey);
}
public virtual void CompleteOnCreateEntryActivity(EntryActivity activity, Thread notifyPluginsOnOpenThread)
{
//this default implementation is executed when we're opening an entry manually, i.e. without search/autofill.
//We only activate the keyboard if this is enabled in "silent mode"
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(activity);
bool activateKeyboard = prefs.GetBoolean("kp2a_switch_rooted", false) &&
!prefs.GetBoolean(
activity.GetString(Resource.String
.OpenKp2aKeyboardAutomaticallyOnlyAfterSearch_key), false);
activity.StartNotificationsService(activateKeyboard);
}
public virtual void PopulatePasswordAccessServiceIntent(Intent intent)
{
}
protected static bool GetBoolFromBundle(Bundle b, string key, bool defaultValue)
{
bool boolValue;
string stringValue = b.GetString(key);
if (!Boolean.TryParse(stringValue, out boolValue))
{
boolValue = defaultValue;
}
return boolValue;
}
protected static int GetIntFromBundle(Bundle b, string key, int defaultValue)
{
int intValue;
var strValue = b.GetString(key);
if (!Int32.TryParse(strValue, out intValue))
{
intValue = defaultValue;
}
return intValue;
}
}
/// <summary>
/// Implementation of AppTask for "no task currently active" (Null pattern)
/// </summary>
public class NullTask: AppTask
{
}
/// <summary>
/// User is about to search an entry for a given URL
/// </summary>
/// Derive from SelectEntryTask. This means that as soon as an Entry is opened, we're returning with
/// ExitAfterTaskComplete. This also allows to specify the flag if we need to display the user notifications.
public class SearchUrlTask: SelectEntryTask
{
public SearchUrlTask()
{
AutoReturnFromQuery = true;
}
public const String UrlToSearchKey = "UrlToSearch";
public const String AutoReturnFromQueryKey = "AutoReturnFromQuery";
public string UrlToSearchFor
{
get;
set;
}
public override void Setup(Bundle b)
{
base.Setup(b);
UrlToSearchFor = b.GetString(UrlToSearchKey);
AutoReturnFromQuery = b.GetBoolean(AutoReturnFromQueryKey, true);
}
public override IEnumerable<IExtra> Extras
{
get
{
foreach (IExtra e in base.Extras)
yield return e;
yield return new StringExtra { Key=UrlToSearchKey, Value = UrlToSearchFor };
yield return new BoolExtra { Key = AutoReturnFromQueryKey, Value = AutoReturnFromQuery };
}
}
public bool AutoReturnFromQuery { get; set; }
public override void LaunchFirstGroupActivity(Activity act)
{
if (String.IsNullOrEmpty(UrlToSearchFor))
{
GroupActivity.Launch(act, new SelectEntryTask() {
ShowUserNotifications = ShowUserNotifications,
CopyTotpToClipboard = CopyTotpToClipboard,
ActivateKeyboard = ActivateKeyboard
},
new ActivityLaunchModeRequestCode(0));
}
else
{
ShareUrlResults.Launch(act, this, new ActivityLaunchModeRequestCode(0));
}
//removed. this causes an issue in the following workflow:
//When the user wants to find an entry for a URL but has the wrong database open he needs
//to switch to another database. But the Task is removed already the first time when going through PasswordActivity
// (with the wrong db).
//Then after switching to the right database, the task is gone.
//A reason this code existed was the following workflow:
//Using Chrome browser (with NEW_TASK flag for ActionSend): Share URL -> KP2A.
//Now the AppTask was in PasswordActivity and didn't get out of it.
//This is now solved by returning new tasks in ActivityResult.
//RemoveTaskFromIntent(act);
//act.AppTask = new NullTask();
}
public override void PopulatePasswordAccessServiceIntent(Intent intent)
{
base.PopulatePasswordAccessServiceIntent(intent);
intent.PutExtra(UrlToSearchKey, UrlToSearchFor);
}
public override void CompleteOnCreateEntryActivity(EntryActivity activity, Thread notifyPluginsOnOpenThread)
{
if (App.Kp2a.LastOpenedEntry != null)
App.Kp2a.LastOpenedEntry.SearchUrl = UrlToSearchFor;
//if the database is readonly (or no URL exists), don't offer to modify the URL
if ((App.Kp2a.CurrentDb.CanWrite == false) || (String.IsNullOrEmpty(UrlToSearchFor) || keepass2android.ShareUrlResults.GetSearchResultsForUrl(UrlToSearchFor).Entries.Any(e => e == activity.Entry) ))
{
base.CompleteOnCreateEntryActivity(activity, notifyPluginsOnOpenThread);
return;
}
AskAddUrlThenCompleteCreate(activity, UrlToSearchFor, notifyPluginsOnOpenThread);
}
/// <summary>
/// brings up a dialog asking the user whether he wants to add the given URL to the entry for automatic finding
/// </summary>
public void AskAddUrlThenCompleteCreate(EntryActivity activity, string url, Thread notifyPluginsOnOpenThread)
{
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
builder.SetTitle(activity.GetString(Resource.String.AddUrlToEntryDialog_title));
builder.SetMessage(activity.GetString(Resource.String.AddUrlToEntryDialog_text, new Java.Lang.Object[] { url }));
builder.SetPositiveButton(activity.GetString(Resource.String.yes), (dlgSender, dlgEvt) =>
{
activity.AddUrlToEntry(url, (EntryActivity thenActiveActivity) => base.CompleteOnCreateEntryActivity(thenActiveActivity, notifyPluginsOnOpenThread
));
});
builder.SetNegativeButton(activity.GetString(Resource.String.no), (dlgSender, dlgEvt) =>
{
base.CompleteOnCreateEntryActivity(activity, notifyPluginsOnOpenThread);
});
Dialog dialog = builder.Create();
dialog.Show();
}
}
public class OpenSpecificEntryTask : SelectEntryTask
{
public OpenSpecificEntryTask()
{
}
public const String EntryUuidKey = "EntryUuid";
public string EntryUuid
{
get;
set;
}
public override void Setup(Bundle b)
{
base.Setup(b);
EntryUuid = b.GetString(EntryUuidKey);
}
public override IEnumerable<IExtra> Extras
{
get
{
foreach (IExtra e in base.Extras)
yield return e;
yield return new StringExtra { Key = EntryUuidKey, Value = EntryUuid };
}
}
public override void LaunchFirstGroupActivity(Activity act)
{
ShareUrlResults.Launch(act, this, new ActivityLaunchModeRequestCode(0));
}
}
public enum ActivationCondition
{
Never,
WhenTotp,
Always
}
/// <summary>
/// User is about to select an entry for use in another app
/// </summary>
public class SelectEntryTask: AppTask
{
public SelectEntryTask()
{
ShowUserNotifications = ActivationCondition.Always;
CloseAfterCreate = true;
ActivateKeyboard = ActivationCondition.Never;
CopyTotpToClipboard = false;
}
public const String ShowUserNotificationsKey = "ShowUserNotifications";
public ActivationCondition ShowUserNotifications { get; set; }
public const String CloseAfterCreateKey = "CloseAfterCreate";
public const String ActivateKeyboardKey = "ActivateKeyboard";
public const String CopyTotpToClipboardKey = "CopyTotpToClipboard";
public bool CloseAfterCreate { get; set; }
public ActivationCondition ActivateKeyboard { get; set; }
public bool CopyTotpToClipboard { get; set; }
public override void Setup(Bundle b)
{
ShowUserNotifications = (ActivationCondition) GetIntFromBundle(b, ShowUserNotificationsKey, (int)ActivationCondition.Always);
CloseAfterCreate = GetBoolFromBundle(b, CloseAfterCreateKey, true);
ActivateKeyboard = (ActivationCondition)GetIntFromBundle(b, ActivateKeyboardKey, (int)ActivationCondition.Always);
CopyTotpToClipboard = GetBoolFromBundle(b, CopyTotpToClipboardKey, false);
}
public override IEnumerable<IExtra> Extras
{
get
{
yield return new StringExtra { Key = ShowUserNotificationsKey, Value = ((int)ShowUserNotifications).ToString() };
yield return new StringExtra { Key = CloseAfterCreateKey, Value = CloseAfterCreate.ToString() };
yield return new StringExtra { Key = ActivateKeyboardKey, Value = ((int)ActivateKeyboard).ToString() };
yield return new StringExtra { Key = CopyTotpToClipboardKey, Value = CopyTotpToClipboard.ToString() };
}
}
public override void CompleteOnCreateEntryActivity(EntryActivity activity, Thread notifyPluginsOnOpenThread)
{
Context ctx = activity;
if (ctx == null)
ctx = LocaleManager.LocalizedAppContext;
var pwEntryOutput = new PwEntryOutput(activity.Entry, App.Kp2a.CurrentDb);
var totpPluginAdapter = new Kp2aTotp().TryGetAdapter(pwEntryOutput);
bool isTotpEntry = totpPluginAdapter != null;
bool activateKeyboard = ActivateKeyboard == ActivationCondition.Always || (ActivateKeyboard == ActivationCondition.WhenTotp && isTotpEntry);
if ((ShowUserNotifications == ActivationCondition.Always)
|| ((ShowUserNotifications == ActivationCondition.WhenTotp) && isTotpEntry)
|| activateKeyboard)
{
//show the notifications
activity.StartNotificationsService(activateKeyboard);
}
else
{
//to avoid getting into inconsistent state (LastOpenedEntry and Notifications): clear notifications:
CopyToClipboardService.CancelNotifications(activity);
}
if (CopyTotpToClipboard && isTotpEntry)
{
Dictionary<string, string> entryFields = pwEntryOutput.OutputStrings.ToDictionary(pair => StrUtil.SafeXmlString(pair.Key), pair => pair.Value.ReadString());
var totpData= totpPluginAdapter.GetTotpData(entryFields, activity, true);
if (totpData.IsTotpEntry)
{
TOTPProvider prov = new TOTPProvider(totpData);
string totp = prov.GenerateByByte(totpData.TotpSecret);
CopyToClipboardService.CopyValueToClipboardWithTimeout(activity, totp, true);
App.Kp2a.ShowMessage(activity, activity.GetString(Resource.String.TotpCopiedToClipboard),
MessageSeverity.Info);
}
}
if (CloseAfterCreate)
{
//give plugins and TOTP time to do their work:
notifyPluginsOnOpenThread.Join(TimeSpan.FromSeconds(1));
//close
activity.CloseAfterTaskComplete();
}
}
}
/// <summary>
/// User is about to move entries and/or groups to another group
/// </summary>
public class MoveElementsTask: AppTask
{
public override bool CanActivateSearchViewOnStart
{
get { return false; }
set { }
}
public const String UuidsKey = "MoveElement_Uuids";
public IEnumerable<PwUuid> Uuids
{
get;
set;
}
public override void Setup(Bundle b)
{
Uuids = b.GetString(UuidsKey).Split(';')
.Where(s => !String.IsNullOrEmpty(s))
.Select(stringPart => new PwUuid(MemUtil.HexStringToByteArray(stringPart)))
.ToList(); //property might be accessed several times, avoid parsing each time
}
public override IEnumerable<IExtra> Extras
{
get
{
yield return new StringExtra
{
Key = UuidsKey,
Value = Uuids.Select(uuid => MemUtil.ByteArrayToHexString(uuid.UuidBytes))
.Aggregate((a, b) => a + ";" + b)
};
}
}
public override void StartInGroupActivity(GroupBaseActivity groupBaseActivity)
{
base.StartInGroupActivity(groupBaseActivity);
groupBaseActivity.StartMovingElements();
}
public override void SetupGroupBaseActivityButtons(GroupBaseActivity groupBaseActivity)
{
groupBaseActivity.ShowInsertElementsButtons();
}
}
/// <summary>
/// User is about to create a new entry. The task might already "know" some information about the contents.
/// </summary>
public class CreateEntryThenCloseTask: AppTask
{
public CreateEntryThenCloseTask()
{
ShowUserNotifications = ActivationCondition.Always;
}
public override bool CanActivateSearchViewOnStart
{
get { return false; }
set { }
}
/// <summary>
/// extra key if only a URL is passed. optional.
/// </summary>
public const String UrlKey = "CreateEntry_Url";
/// <summary>
/// extra key if a json serialized key/value mapping is passed. optional.
/// </summary>
/// Uses the PluginSDKs keys because this is mainly used for communicating with plugins.
/// Of course the data might also contain "non-output-data" (e.g. placeholders), but usually won't.
public const String AllFieldsKey = Keepass2android.Pluginsdk.Strings.ExtraEntryOutputData;
/// <summary>
/// extra key to specify a list of protected field keys in AllFieldsKey. Passed as StringArrayExtra. optional.
/// </summary>
public const String ProtectedFieldsListKey = Keepass2android.Pluginsdk.Strings.ExtraProtectedFieldsList;
/// <summary>
/// Extra key to specify whether user notifications (e.g. for copy password or keyboard) should be displayed when the entry
/// is selected after creating.
/// </summary>
public const String ShowUserNotificationsKey = "ShowUserNotifications";
public string Url { get; set; }
public string AllFields { get; set; }
public IList<string> ProtectedFieldsList { get; set; }
public ActivationCondition ShowUserNotifications { get; set; }
public override void Setup(Bundle b)
{
ShowUserNotifications = (ActivationCondition)GetIntFromBundle(b,ShowUserNotificationsKey, (int)ActivationCondition.Always);
Url = b.GetString(UrlKey);
AllFields = b.GetString(AllFieldsKey);
ProtectedFieldsList = b.GetStringArrayList(ProtectedFieldsListKey);
}
public override IEnumerable<IExtra> Extras
{
get
{
if (Url != null)
yield return new StringExtra { Key = UrlKey, Value = Url };
if (AllFields != null)
yield return new StringExtra { Key = AllFieldsKey, Value = AllFields };
if (ProtectedFieldsList != null)
yield return new StringArrayListExtra { Key = ProtectedFieldsListKey, Value = ProtectedFieldsList };
yield return new StringExtra { Key = ShowUserNotificationsKey, Value = ShowUserNotifications.ToString() };
}
}
public override void PrepareNewEntry(PwEntry newEntry)
{
if (Url != null)
{
Util.SetNextFreeUrlField(newEntry, Url);
}
if (AllFields != null)
{
var allFields = new Org.Json.JSONObject(AllFields);
for (var iter = allFields.Keys(); iter.HasNext; )
{
string key = iter.Next().ToString();
string value = allFields.Get(key).ToString();
bool isProtected = ((ProtectedFieldsList != null) && (ProtectedFieldsList.Contains(key)))
|| (key == PwDefs.PasswordField);
newEntry.Strings.Set(key, new ProtectedString(isProtected, value));
}
}
}
public override void AfterAddNewEntry(EntryEditActivity entryEditActivity, PwEntry newEntry)
{
EntryActivity.Launch(entryEditActivity, newEntry, -1,
new SelectEntryTask() { ShowUserNotifications = this.ShowUserNotifications, ActivateKeyboard = ActivationCondition.Never },
ActivityFlags.ForwardResult);
//no need to call Finish here, that's done in EntryEditActivity ("closeOrShowError")
}
public override void CompleteOnCreateEntryActivity(EntryActivity activity, Thread notifyPluginsOnOpenThread)
{
//if the user selects an entry before creating the new one, we're not closing the app
base.CompleteOnCreateEntryActivity(activity, notifyPluginsOnOpenThread);
}
}
/// <summary>
/// Navigate to a folder and open a Task (appear in SearchResult)
/// </summary>
public abstract class NavigateAndLaunchTask: AppTask
{
public override bool CanActivateSearchViewOnStart
{
get { return false; }
set { }
}
// All group Uuid are stored in guuidKey + indice
// The last one is the destination group
public const String NumberOfGroupsKey = "NumberOfGroups";
public const String GFullIdKey = "gFullIdKey";
public const String FullGroupNameKey = "fullGroupNameKey";
public const String ToastEnableKey = "toastEnableKey";
#if INCLUDE_DEBUG_MOVE_GROUPNAME
public const String gNameKey = "gNameKey";
private LinkedList<string> groupNameList;
#endif
private LinkedList<string> _fullGroupIds;
protected AppTask TaskToBeLaunchedAfterNavigation;
protected String FullGroupName {
get ;
set ;
}
protected bool ToastEnable {
get;
set;
}
protected NavigateAndLaunchTask() {
TaskToBeLaunchedAfterNavigation = new NullTask();
FullGroupName = "";
ToastEnable = false;
}
/// <summary>
/// Initializes a new instance of the <see cref="keepass2android.NavigateAndLaunchTask"/> class.
/// </summary>
/// <param name="groups">Groups.</param>
/// <param name="taskToBeLaunchedAfterNavigation">Task to be launched after navigation.</param>
/// <param name="toastEnable">If set to <c>true</c>, toast will be displayed after navigation.</param>
protected NavigateAndLaunchTask(PwGroup groups, AppTask taskToBeLaunchedAfterNavigation, bool toastEnable = false) {
TaskToBeLaunchedAfterNavigation = taskToBeLaunchedAfterNavigation;
PopulateGroups (groups);
ToastEnable = toastEnable;
}
public void PopulateGroups(PwGroup groups) {
_fullGroupIds = new LinkedList<String>();
#if INCLUDE_DEBUG_MOVE_GROUPNAME
groupNameList = new LinkedList<String>{};
#endif
FullGroupName = "";
PwGroup readGroup = groups;
while (readGroup != null) {
if ( (readGroup.ParentGroup != null) ||
(readGroup.ParentGroup == null) && (readGroup == groups) ) {
FullGroupName = readGroup.Name + "." + FullGroupName;
}
_fullGroupIds.AddFirst(new ElementAndDatabaseId(App.Kp2a.FindDatabaseForElement(readGroup),readGroup).FullId);
#if INCLUDE_DEBUG_MOVE_GROUPNAME
groupNameList.AddFirst (readGroup.Name);
#endif
readGroup = readGroup.ParentGroup;
}
}
/// <summary>
/// Loads the parameters of the task from the given bundle. Embeded task is not setup from this bundle
/// </summary>
/// <param name="b">The bundle component.</param>
public override void Setup(Bundle b)
{
int numberOfGroups = b.GetInt(NumberOfGroupsKey);
_fullGroupIds = new LinkedList<String>();
#if INCLUDE_DEBUG_MOVE_GROUPNAME
groupNameList = new LinkedList<String>{};
#endif
int i = 0;
while (i < numberOfGroups)
{
_fullGroupIds.AddLast ( b.GetString (GFullIdKey + i.ToString(CultureInfo.InvariantCulture)) ) ;
#if INCLUDE_DEBUG_MOVE_GROUPNAME
groupNameList.AddLast ( b.GetString (gNameKey + i);
#endif
i++;
}
FullGroupName = b.GetString (FullGroupNameKey);
ToastEnable = b.GetBoolean (ToastEnableKey);
}
public override IEnumerable<IExtra> Extras
{
get
{
// Return Navigate group Extras
#if INCLUDE_DEBUG_MOVE_GROUPNAME
IEnumerator<String> eGroupName = groupNameList.GetEnumerator ();
#endif
int i = 0;
foreach (var fullGroupId in _fullGroupIds)
{
yield return new StringExtra { Key = GFullIdKey + i.ToString (CultureInfo.InvariantCulture), Value = fullGroupId };
#if INCLUDE_DEBUG_MOVE_GROUPNAME
eGroupName.MoveNext();
yield return new StringExtra { Key = gNameKey + i.ToString (), Value = eGroupName.Current };
#endif
i++;
}
yield return new IntExtra{ Key = NumberOfGroupsKey, Value = i };
yield return new StringExtra{ Key = FullGroupNameKey, Value = FullGroupName };
yield return new BoolExtra{ Key = ToastEnableKey, Value = ToastEnable };
// Return afterTaskExtras
foreach (var extra in TaskToBeLaunchedAfterNavigation.Extras)
{
yield return extra;
}
}
}
public override void StartInGroupActivity(GroupBaseActivity groupBaseActivity)
{
base.StartInGroupActivity(groupBaseActivity);
if (GroupIsFound(groupBaseActivity) ){ // Group has been found: display toaster and stop here
if (ToastEnable) {
String toastMessage = groupBaseActivity.GetString (Resource.String.NavigationToGroupCompleted_message, new Java.Lang.Object[] { FullGroupName});
App.Kp2a.ShowMessage(groupBaseActivity, toastMessage, MessageSeverity.Info);
}
groupBaseActivity.StartTask (TaskToBeLaunchedAfterNavigation);
return;
} else if ((groupBaseActivity.FullGroupId != null) && _fullGroupIds.Contains(groupBaseActivity.FullGroupId.FullId)) { // Need to down up in groups tree
// Get next Group Uuid
var linkedListNode = _fullGroupIds.Find(groupBaseActivity.FullGroupId.FullId);
if (linkedListNode != null)
{
//Note: Resharper says there is a possible NullRefException.
//This is not the case because it was checked above if we're already there or not.
String nextGroupFullId = linkedListNode.Next.Value;
ElementAndDatabaseId fullId = new ElementAndDatabaseId(nextGroupFullId);
PwUuid nextGroupPwUuid = new PwUuid (MemUtil.HexStringToByteArray (fullId.ElementIdString));
// Create Group Activity
PwGroup nextGroup = App.Kp2a.GetDatabase(fullId.DatabaseId).GroupsById[nextGroupPwUuid];
GroupActivity.Launch (groupBaseActivity, nextGroup, this, new ActivityLaunchModeRequestCode(0));
}
return;
} else { // Need to go up in groups tree
ElementAndDatabaseId fullId = new ElementAndDatabaseId(_fullGroupIds.Last.Value);
var targetDb = App.Kp2a.GetDatabase(fullId.DatabaseId);
if (App.Kp2a.CurrentDb != targetDb)
{
App.Kp2a.CurrentDb = targetDb;
GroupActivity.Launch(groupBaseActivity,targetDb.Root,this,new ActivityLaunchModeForward());
}
else
{
SetActivityResult(groupBaseActivity, KeePass.ExitNormal);
}
groupBaseActivity.Finish();
}
}
public override void SetupGroupBaseActivityButtons(GroupBaseActivity groupBaseActivity)
{
}
public bool GroupIsFound(GroupBaseActivity groupBaseActivity)
{
var fullId = groupBaseActivity.FullGroupId;
return fullId != null && _fullGroupIds.Last.Value.Equals (fullId.FullId);
}
}
public class NavigateToFolder: NavigateAndLaunchTask {
public NavigateToFolder()
{
}
public NavigateToFolder(Database db, PwGroup groups, bool toastEnable = false)
: base(groups, new NullTask(), toastEnable)
{
}
}
public class NavigateToFolderAndLaunchMoveElementTask: NavigateAndLaunchTask {
public NavigateToFolderAndLaunchMoveElementTask()
{
}
public NavigateToFolderAndLaunchMoveElementTask(Database db, PwGroup groups, List<PwUuid> uuids, bool toastEnable = false)
:base(groups, new MoveElementsTask() { Uuids = uuids }, toastEnable) {
}
public override void Setup(Bundle b) {
base.Setup(b);
TaskToBeLaunchedAfterNavigation = new MoveElementsTask ();
TaskToBeLaunchedAfterNavigation.Setup (b);
}
}
}