Skip to content

Commit 26148e2

Browse files
committed
restore Activity stack after opening a database if EntryActivity wasn't "intentionally" closed (e.g. lock by timeout)
1 parent 5aa92ea commit 26148e2

5 files changed

Lines changed: 252 additions & 20 deletions

File tree

src/keepass2android-app/EntryActivity.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@ private void OnDataUpdated()
151151

152152
}
153153

154+
public static Intent GetLaunchIntent(Context ctx, Database db, PwEntry pw, int pos, AppTask appTask)
155+
{
156+
Intent i = new Intent(ctx, typeof(EntryActivity));
157+
i.PutExtra(KeyEntry, new ElementAndDatabaseId(db, pw).FullId);
158+
i.PutExtra(KeyRefreshPos, pos);
159+
i.PutExtra(KeyEntryHistoryIndex, -1);
160+
appTask.ToIntent(i);
161+
return i;
162+
}
163+
154164
public static void Launch(Activity act, PwEntry pw, int pos, AppTask appTask, ActivityFlags? flags = null, int historyIndex = -1)
155165
{
156166
Intent i = new Intent(act, typeof(EntryActivity));
@@ -532,6 +542,8 @@ protected override void OnCreate(Bundle savedInstanceState)
532542
SetupEditButtons();
533543

534544
App.Kp2a.LastOpenedEntry = new PwEntryOutput(Entry, App.Kp2a.CurrentDb);
545+
if (_historyIndex < 0)
546+
App.Kp2a.LastOpenedEntryFullIdForRestore = new ElementAndDatabaseId(App.Kp2a.CurrentDb, Entry).FullId;
535547

536548
_pluginActionReceiver = new PluginActionReceiver(this);
537549
ContextCompat.RegisterReceiver(this, _pluginActionReceiver, new IntentFilter(Strings.ActionAddEntryAction), (int)ReceiverFlags.Exported);
@@ -986,6 +998,7 @@ public static String GetMimeType(String url)
986998

987999
public override void OnBackPressed()
9881000
{
1001+
App.Kp2a.LastOpenedEntryFullIdForRestore = null;
9891002
base.OnBackPressed();
9901003
//OverridePendingTransition(Resource.Animation.anim_enter_back, Resource.Animation.anim_leave_back);
9911004
}
@@ -1535,6 +1548,7 @@ public override bool OnOptionsItemSelected(IMenuItem item)
15351548
//Currently the action bar only displays the home button when we come from a previous activity.
15361549
//So we can simply Finish. See this page for information on how to do this in more general (future?) cases:
15371550
//http://developer.android.com/training/implementing-navigation/ancestral.html
1551+
App.Kp2a.LastOpenedEntryFullIdForRestore = null;
15381552
Finish();
15391553
//OverridePendingTransition(Resource.Animation.anim_enter_back, Resource.Animation.anim_leave_back);
15401554

@@ -1637,6 +1651,7 @@ public void AddEntryToIntent(Intent intent)
16371651

16381652
public void CloseAfterTaskComplete()
16391653
{
1654+
App.Kp2a.LastOpenedEntryFullIdForRestore = null;
16401655
//before closing, wait a little to get plugin updates
16411656
int numPlugins = new PluginDatabase(this).GetPluginsWithAcceptedScope(Strings.ScopeCurrentEntry).Count();
16421657
var timeToWait = TimeSpan.FromMilliseconds(500 * numPlugins);

src/keepass2android-app/GroupActivity.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ public static void Launch(Activity act, AppTask appTask, ActivityLaunchMode laun
6969
Launch(act, null, appTask, launchMode);
7070
}
7171

72+
public static Intent GetRootLaunchIntent(Context ctx, AppTask appTask)
73+
{
74+
return GetLaunchIntent(ctx, App.Kp2a.CurrentDb.Root, appTask);
75+
}
76+
77+
public static Intent GetLaunchIntent(Context ctx, PwGroup g, AppTask appTask)
78+
{
79+
Intent i = new Intent(ctx, typeof(GroupActivity));
80+
i.PutExtra(KeyEntry, g.Uuid.ToHexString());
81+
appTask.ToIntent(i);
82+
return i;
83+
}
84+
7285
public static void Launch(Activity act, PwGroup g, AppTask appTask, ActivityLaunchMode launchMode)
7386
{
7487
Intent i = new Intent(act, typeof(GroupActivity));

src/keepass2android-app/SelectCurrentDbActivity.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ private void OnDatabaseSelected(Database selectedDatabase)
224224
{
225225
App.Kp2a.CurrentDb = selectedDatabase;
226226
LaunchingOther = true;
227-
AppTask.LaunchFirstGroupActivity(this);
227+
GetEffectiveTask().LaunchFirstGroupActivity(this);
228228
}
229229

230230
public override bool OnCreateOptionsMenu(IMenu menu)
@@ -300,7 +300,7 @@ protected override void OnCreate(Bundle savedInstanceState)
300300
{
301301
LaunchingOther = true;
302302
AppTask.CanActivateSearchViewOnStart = true;
303-
AppTask.LaunchFirstGroupActivity(this);
303+
GetEffectiveTask().LaunchFirstGroupActivity(this);
304304
}
305305
}
306306
else
@@ -436,7 +436,7 @@ private bool GetIocFromViewIntent(Intent intent)
436436
if (OpenAutoExecEntries(App.Kp2a.CurrentDb)) return false;
437437
LaunchingOther = true;
438438
AppTask.CanActivateSearchViewOnStart = true;
439-
AppTask.LaunchFirstGroupActivity(this);
439+
GetEffectiveTask().LaunchFirstGroupActivity(this);
440440
}
441441
else
442442
{
@@ -450,6 +450,13 @@ private bool GetIocFromViewIntent(Intent intent)
450450
return true;
451451
}
452452

453+
private AppTask GetEffectiveTask()
454+
{
455+
if (AppTask is NullTask && !string.IsNullOrEmpty(App.Kp2a.LastOpenedEntryFullIdForRestore))
456+
return new OpenLastEntryTask(App.Kp2a.LastOpenedEntryFullIdForRestore);
457+
return AppTask;
458+
}
459+
453460
protected override void OnResume()
454461
{
455462
_isForeground = true;
@@ -493,7 +500,7 @@ protected override void OnResume()
493500
if ((App.Kp2a.OpenDatabases.Count() == 1) || (AppTask is SearchUrlTask))
494501
{
495502
LaunchingOther = true;
496-
AppTask.LaunchFirstGroupActivity(this);
503+
GetEffectiveTask().LaunchFirstGroupActivity(this);
497504
return;
498505
}
499506

@@ -649,7 +656,7 @@ protected override void OnActivityResult(int requestCode, Result resultCode, Int
649656

650657
_pendingBackgroundSyncs.Clear();
651658

652-
AppTask.LaunchFirstGroupActivity(this);
659+
GetEffectiveTask().LaunchFirstGroupActivity(this);
653660
}
654661

655662

src/keepass2android-app/app/App.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ public void Lock(bool allowQuickUnlock = true, bool lockWasTriggeredByTimeout =
138138
Kp2aLog.Log("QuickLocking database");
139139
QuickLocked = true;
140140
LastOpenedEntry = null;
141+
if (!lockWasTriggeredByTimeout)
142+
LastOpenedEntryFullIdForRestore = null;
141143
BroadcastDatabaseAction(LocaleManager.LocalizedAppContext, Strings.ActionLockDatabase);
142144
}
143145
else
@@ -157,6 +159,7 @@ public void Lock(bool allowQuickUnlock = true, bool lockWasTriggeredByTimeout =
157159

158160
_currentDatabase = null;
159161
LastOpenedEntry = null;
162+
LastOpenedEntryFullIdForRestore = null;
160163
QuickLocked = false;
161164

162165

@@ -481,6 +484,13 @@ public void MarkAllGroupsAsDirty()
481484
/// </summary>
482485
public PwEntryOutput LastOpenedEntry { get; set; }
483486

487+
/// <summary>
488+
/// FullId of the last entry opened in EntryActivity that should be restored after a
489+
/// timeout-triggered QuickLock/unlock cycle. Cleared when the user explicitly navigates
490+
/// away from the entry (back, Home, CloseAfterTaskComplete) or explicitly locks the database.
491+
/// </summary>
492+
public string? LastOpenedEntryFullIdForRestore { get; set; }
493+
484494
public Database CurrentDb
485495
{
486496
get { return _currentDatabase; }

src/keepass2android-app/app/AppTask.cs

Lines changed: 202 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
2-
//
3-
// Keepass2Android is free software: you can redistribute it and/or modify
4-
// it under the terms of the GNU General Public License as published by
5-
// the Free Software Foundation, either version 3 of the License, or
6-
// (at your option) any later version.
7-
//
8-
// Keepass2Android is distributed in the hope that it will be useful,
9-
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10-
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11-
// GNU General Public License for more details.
12-
//
13-
// You should have received a copy of the GNU General Public License
14-
// along with Keepass2Android. If not, see <http://www.gnu.org/licenses/>.
15-
1+
// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
2+
//
3+
// Keepass2Android is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// Keepass2Android is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with Keepass2Android. If not, see <http://www.gnu.org/licenses/>.
15+
1616
using System;
1717
using System.Globalization;
1818
using Android.App;
@@ -1081,6 +1081,93 @@ public bool GroupIsFound(GroupBaseActivity groupBaseActivity)
10811081
}
10821082
}
10831083

1084+
/// <summary>
1085+
/// Restores the full activity back stack (parent groups + EntryActivity) for
1086+
/// the last entry that was on screen when the database was locked by timeout.
1087+
/// Falls back silently to the root group if anything goes wrong.
1088+
/// </summary>
1089+
public class OpenLastEntryTask : AppTask
1090+
{
1091+
private readonly string _entryFullId;
1092+
1093+
public OpenLastEntryTask(string entryFullId)
1094+
{
1095+
_entryFullId = entryFullId;
1096+
}
1097+
1098+
public override void LaunchFirstGroupActivity(Activity act)
1099+
{
1100+
try
1101+
{
1102+
if (string.IsNullOrEmpty(_entryFullId))
1103+
{
1104+
FallBack(act);
1105+
return;
1106+
}
1107+
1108+
var fullId = new ElementAndDatabaseId(_entryFullId);
1109+
var db = App.Kp2a.GetDatabase(fullId.DatabaseId);
1110+
if (db == null)
1111+
{
1112+
FallBack(act);
1113+
return;
1114+
}
1115+
1116+
var uuid = new PwUuid(MemUtil.HexStringToByteArray(fullId.ElementIdString));
1117+
if (!db.EntriesById.ContainsKey(uuid))
1118+
{
1119+
FallBack(act);
1120+
return;
1121+
}
1122+
1123+
var entry = db.EntriesById[uuid];
1124+
1125+
// Build group chain from root down to the entry's immediate parent.
1126+
var groupChain = new System.Collections.Generic.List<PwGroup>();
1127+
var current = entry.ParentGroup;
1128+
while (current != null)
1129+
{
1130+
groupChain.Insert(0, current);
1131+
current = current.ParentGroup;
1132+
}
1133+
1134+
// Verify every group still exists in the database (handles moved/deleted entries).
1135+
foreach (var g in groupChain)
1136+
{
1137+
if (!db.GroupsById.ContainsKey(g.Uuid))
1138+
{
1139+
FallBack(act);
1140+
return;
1141+
}
1142+
}
1143+
1144+
if (App.Kp2a.CurrentDb != db)
1145+
App.Kp2a.CurrentDb = db;
1146+
1147+
var nullTask = new NullTask();
1148+
var stackBuilder = AndroidX.Core.App.TaskStackBuilder.Create(act);
1149+
1150+
foreach (var group in groupChain)
1151+
stackBuilder.AddNextIntent(GroupActivity.GetLaunchIntent(act, group, nullTask));
1152+
1153+
stackBuilder.AddNextIntent(EntryActivity.GetLaunchIntent(act, db, entry, 0, nullTask));
1154+
1155+
stackBuilder.StartActivities();
1156+
act.Finish();
1157+
}
1158+
catch (Exception e)
1159+
{
1160+
Kp2aLog.LogUnexpectedError(e);
1161+
FallBack(act);
1162+
}
1163+
}
1164+
1165+
private static void FallBack(Activity act)
1166+
{
1167+
GroupActivity.Launch(act, new NullTask(), new ActivityLaunchModeRequestCode(0));
1168+
}
1169+
}
1170+
10841171
public class NavigateToFolder : NavigateAndLaunchTask
10851172
{
10861173

@@ -1097,6 +1184,106 @@ public NavigateToFolder(Database db, PwGroup groups, bool toastEnable = false)
10971184

10981185
}
10991186

1187+
/// <summary>
1188+
/// When reached in a group activity, opens a specific entry by its FullId.
1189+
/// Used as the inner task of NavigateAndOpenEntryTask.
1190+
/// </summary>
1191+
public class OpenEntryTask : AppTask
1192+
{
1193+
public const string EntryFullIdKey = "OpenEntryFullId";
1194+
public string EntryFullId { get; set; }
1195+
1196+
public override void Setup(Bundle b)
1197+
{
1198+
base.Setup(b);
1199+
EntryFullId = b.GetString(EntryFullIdKey);
1200+
}
1201+
1202+
public override IEnumerable<IExtra> Extras
1203+
{
1204+
get
1205+
{
1206+
foreach (var e in base.Extras) yield return e;
1207+
if (EntryFullId != null)
1208+
yield return new StringExtra { Key = EntryFullIdKey, Value = EntryFullId };
1209+
}
1210+
}
1211+
1212+
public override void StartInGroupActivity(GroupBaseActivity groupBaseActivity)
1213+
{
1214+
if (string.IsNullOrEmpty(EntryFullId)) return;
1215+
try
1216+
{
1217+
var id = new ElementAndDatabaseId(EntryFullId);
1218+
var uuid = new PwUuid(MemUtil.HexStringToByteArray(id.ElementIdString));
1219+
var db = App.Kp2a.CurrentDb;
1220+
if (db == null || !db.EntriesById.ContainsKey(uuid)) return;
1221+
groupBaseActivity.LaunchActivityForEntry(db.EntriesById[uuid], 0);
1222+
}
1223+
catch (Exception e)
1224+
{
1225+
Kp2aLog.LogUnexpectedError(e);
1226+
}
1227+
}
1228+
}
1229+
1230+
/// <summary>
1231+
/// Navigates through the group hierarchy to the parent of a specific entry,
1232+
/// then opens that entry.
1233+
/// </summary>
1234+
public class NavigateAndOpenEntryTask : NavigateAndLaunchTask
1235+
{
1236+
public NavigateAndOpenEntryTask()
1237+
{
1238+
}
1239+
1240+
public NavigateAndOpenEntryTask(PwGroup targetGroup, string entryFullId)
1241+
: base(targetGroup, new OpenEntryTask { EntryFullId = entryFullId })
1242+
{
1243+
}
1244+
1245+
public override void Setup(Bundle b)
1246+
{
1247+
base.Setup(b);
1248+
TaskToBeLaunchedAfterNavigation = new OpenEntryTask();
1249+
TaskToBeLaunchedAfterNavigation.Setup(b);
1250+
}
1251+
}
1252+
1253+
/// <summary>
1254+
/// Restores the last opened entry (from App.Kp2a.LastOpenedEntryFullIdForRestore)
1255+
/// by navigating through the group hierarchy and reopening the entry.
1256+
/// Falls back to the root group if the entry or its parent groups can no longer be found.
1257+
/// </summary>
1258+
public class RestoreLastEntryTask : AppTask
1259+
{
1260+
public override void LaunchFirstGroupActivity(Activity act)
1261+
{
1262+
try
1263+
{
1264+
string fullId = App.Kp2a.LastOpenedEntryFullIdForRestore;
1265+
if (string.IsNullOrEmpty(fullId)) { new NullTask().LaunchFirstGroupActivity(act); return; }
1266+
1267+
var id = new ElementAndDatabaseId(fullId);
1268+
var db = App.Kp2a.GetDatabase(id.DatabaseId);
1269+
if (db == null) { new NullTask().LaunchFirstGroupActivity(act); return; }
1270+
1271+
var uuid = new PwUuid(MemUtil.HexStringToByteArray(id.ElementIdString));
1272+
if (!db.EntriesById.ContainsKey(uuid)) { new NullTask().LaunchFirstGroupActivity(act); return; }
1273+
1274+
var entry = db.EntriesById[uuid];
1275+
if (entry.ParentGroup == null) { new NullTask().LaunchFirstGroupActivity(act); return; }
1276+
1277+
new NavigateAndOpenEntryTask(entry.ParentGroup, fullId).LaunchFirstGroupActivity(act);
1278+
}
1279+
catch (Exception e)
1280+
{
1281+
Kp2aLog.LogUnexpectedError(e);
1282+
new NullTask().LaunchFirstGroupActivity(act);
1283+
}
1284+
}
1285+
}
1286+
11001287
public class NavigateToFolderAndLaunchMoveElementTask : NavigateAndLaunchTask
11011288
{
11021289

0 commit comments

Comments
 (0)