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; using PluginTOTP; namespace keepass2android { /// /// Interface for data stored in an intent or bundle as extra string /// public interface IExtra { /// /// put data to a bundle by calling one of the PutXX methods /// void ToBundle(Bundle b); /// /// Put data to an intent by calling PutExtra /// void ToIntent(Intent i); } /// /// represents data stored in an intent or bundle as extra string /// 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 } /// /// represents data stored in an intent or bundle as extra string array list /// public class StringArrayListExtra : IExtra { public string Key { get; set; } public IList 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 } /// /// represents data stored in an intent or bundle as extra int /// 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 } /// /// represents data stored in an intent or bundle as extra bool /// 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 } /// /// represents data stored in an intent or bundle as extra string array /// 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 } /// /// base class for "tasks": these are things the user wants to do and which require several activities /// /// 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 { /// /// Loads the parameters of the task from the given bundle /// public virtual void Setup(Bundle b) { CanActivateSearchViewOnStart = b.GetBoolean(CanActivateSearchViewOnStartKey, true); } public const String CanActivateSearchViewOnStartKey = "CanActivateSearchViewOnStart"; /// /// Can be overwritten to indicate that it is not desired to bring up the search view when starting a groupactivity /// public virtual bool CanActivateSearchViewOnStart { get; set; } /// /// Returns the parameters of the task for storage in a bundle or intent /// /// The extras. public virtual IEnumerable 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"; /// /// 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. /// 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; } } /// /// Adds the extras of the task to the intent /// public void ToIntent(Intent intent) { GetTypeExtra(GetType()).ToIntent(intent); foreach (IExtra extra in Extras) { extra.ToIntent(intent); } } /// /// Adds the extras of the task to the bundle /// public void ToBundle(Bundle bundle) { GetTypeExtra(GetType()).ToBundle(bundle); foreach (IExtra extra in Extras) { extra.ToBundle(bundle); } } /// /// Returns an IExtra which must be part of the Extras of a task to describe the type /// 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); } /// /// 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. /// 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; } } /// /// Implementation of AppTask for "no task currently active" (Null pattern) /// public class NullTask: AppTask { } /// /// User is about to search an entry for a given URL /// /// 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 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); } /// /// brings up a dialog asking the user whether he wants to add the given URL to the entry for automatic finding /// 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 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 } /// /// User is about to select an entry for use in another app /// 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 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) { DoCopyTotpToClipboard(activity, pwEntryOutput, totpPluginAdapter); } if (CloseAfterCreate) { //give plugins and TOTP time to do their work: notifyPluginsOnOpenThread.Join(TimeSpan.FromSeconds(1)); //close activity.CloseAfterTaskComplete(); } } private static void DoCopyTotpToClipboard(EntryActivity activity, PwEntryOutput pwEntryOutput, ITotpPluginAdapter? totpPluginAdapter) { Dictionary 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); } } } /// /// User is about to move entries and/or groups to another group /// public class MoveElementsTask: AppTask { public override bool CanActivateSearchViewOnStart { get { return false; } set { } } public const String UuidsKey = "MoveElement_Uuids"; public IEnumerable 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 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(); } } /// /// User is about to create a new entry. The task might already "know" some information about the contents. /// public class CreateEntryThenCloseTask: AppTask { public CreateEntryThenCloseTask() { ShowUserNotifications = ActivationCondition.Always; } public override bool CanActivateSearchViewOnStart { get { return false; } set { } } /// /// extra key if only a URL is passed. optional. /// public const String UrlKey = "CreateEntry_Url"; /// /// extra key if a json serialized key/value mapping is passed. optional. /// /// 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; /// /// extra key to specify a list of protected field keys in AllFieldsKey. Passed as StringArrayExtra. optional. /// public const String ProtectedFieldsListKey = Keepass2android.Pluginsdk.Strings.ExtraProtectedFieldsList; /// /// Extra key to specify whether user notifications (e.g. for copy password or keyboard) should be displayed when the entry /// is selected after creating. /// public const String ShowUserNotificationsKey = "ShowUserNotifications"; public string Url { get; set; } public string AllFields { get; set; } public IList 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 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); } } /// /// Navigate to a folder and open a Task (appear in SearchResult) /// 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 groupNameList; #endif private LinkedList _fullGroupIds; protected AppTask TaskToBeLaunchedAfterNavigation; protected String FullGroupName { get ; set ; } protected bool ToastEnable { get; set; } protected NavigateAndLaunchTask() { TaskToBeLaunchedAfterNavigation = new NullTask(); FullGroupName = ""; ToastEnable = false; } /// /// Initializes a new instance of the class. /// /// Groups. /// Task to be launched after navigation. /// If set to true, toast will be displayed after navigation. protected NavigateAndLaunchTask(PwGroup groups, AppTask taskToBeLaunchedAfterNavigation, bool toastEnable = false) { TaskToBeLaunchedAfterNavigation = taskToBeLaunchedAfterNavigation; PopulateGroups (groups); ToastEnable = toastEnable; } public void PopulateGroups(PwGroup groups) { _fullGroupIds = new LinkedList(); #if INCLUDE_DEBUG_MOVE_GROUPNAME groupNameList = new LinkedList{}; #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; } } /// /// Loads the parameters of the task from the given bundle. Embeded task is not setup from this bundle /// /// The bundle component. public override void Setup(Bundle b) { int numberOfGroups = b.GetInt(NumberOfGroupsKey); _fullGroupIds = new LinkedList(); #if INCLUDE_DEBUG_MOVE_GROUPNAME groupNameList = new LinkedList{}; #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 Extras { get { // Return Navigate group Extras #if INCLUDE_DEBUG_MOVE_GROUPNAME IEnumerator 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 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); } } }