Commit: 15df0dc
Parent: 4ed961b

Fix title-bar overlap and add folder-picker prefetch caching

Mårten Åsberg committed on 2026-08-01 at 16:52
Pin targetSdkVersion to 34 so Android 15+'s forced edge-to-edge
rendering doesn't draw buttons under the title bar. Also cache and
prefetch one level of OneDrive folder listings so navigating into a
recently-viewed or already-warmed folder renders instantly instead
of waiting on a network round trip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
src/Activities/FolderPickerActivity.cs +41 -0
diff --git a/src/Activities/FolderPickerActivity.cs b/src/Activities/FolderPickerActivity.cs
index 584b1bf..b4ddb5f 100644
@@ -64,11 +64,24 @@ public class FolderPickerActivity : Activity
private async Task LoadFoldersAsync()
{
var folderKey = _folderId ?? "root";
// Render instantly from a prior visit or a parent-level prefetch if we have one;
// otherwise fall back to the normal network fetch with a spinner.
if (FolderChildrenCache.TryGet(folderKey, out var cached))
{
_adapter.SetItems(cached);
PrefetchChildrenOf(cached);
return;
}
_progressBar.Visibility = ViewStates.Visible;
try
{
var folders = await _graphService.ListChildFoldersAsync(_folderId);
FolderChildrenCache.Set(folderKey, folders);
_adapter.SetItems(folders);
PrefetchChildrenOf(folders);
}
catch (Exception ex)
{
@@ -80,6 +93,34 @@ public class FolderPickerActivity : Activity
}
}
// Warms the cache one level ahead so tapping into a folder shown here is usually instant
// instead of waiting on a network round trip. Fire-and-forget: a normal fetch still runs
// (with its own spinner/error handling) if the user navigates in before this finishes.
private void PrefetchChildrenOf(List<DriveFolderItem> folders)
{
foreach (var folder in folders)
{
if (FolderChildrenCache.TryGet(folder.Id, out _))
continue;
_ = PrefetchOneAsync(folder.Id);
}
}
private async Task PrefetchOneAsync(string folderId)
{
try
{
var children = await _graphService.ListChildFoldersAsync(folderId);
FolderChildrenCache.Set(folderId, children);
}
catch
{
// Best-effort warm-up; a real fetch with error handling runs if the user
// navigates in before this succeeds.
}
}
private void OnFolderTapped(DriveFolderItem folder)
{
var intent = new Intent(this, typeof(FolderPickerActivity));
src/AndroidManifest.xml +4 -0
diff --git a/src/AndroidManifest.xml b/src/AndroidManifest.xml
index cc6f90e..5948b9e 100644
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- targetSdkVersion pinned below 35 to avoid Android 15's forced edge-to-edge, which
draws content behind the status bar/title bar unless every Activity applies window
inset padding - not worth the boilerplate for a personal, non-Play-Store app. -->
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34" />
<application
android:allowBackup="true"
android:icon="@mipmap/appicon"
src/Graph/FolderChildrenCache.cs +19 -0
diff --git a/src/Graph/FolderChildrenCache.cs b/src/Graph/FolderChildrenCache.cs
new file mode 100644
index 0000000..65a8f9d
@@ -0,0 +1,19 @@
using NotesApp.Models;
namespace NotesApp.Graph;
// In-memory only, process-lifetime cache of folder id -> its child folders. Lets the folder
// picker render a level instantly if it was already fetched (either from a prior visit, or
// pre-warmed while browsing the parent level), instead of waiting on a network call on every
// tap. No TTL/invalidation: folder structure rarely changes mid-setup, and the cache is gone
// the moment the process dies.
public static class FolderChildrenCache
{
private static readonly Dictionary<string, List<DriveFolderItem>> Cache = [];
public static bool TryGet(string folderKey, out List<DriveFolderItem> children) =>
Cache.TryGetValue(folderKey, out children!);
public static void Set(string folderKey, List<DriveFolderItem> children) =>
Cache[folderKey] = children;
}
src/Resources/values/strings.xml +3 -4
diff --git a/src/Resources/values/strings.xml b/src/Resources/values/strings.xml
index 975ccbc..28c873f 100644
@@ -2,10 +2,9 @@
<string name="app_name">NotesApp</string>
<string name="app_text">Hello, Notes!</string>
<!-- Replace both with the Application (client) ID from your Azure AD app registration.
msal_redirect_scheme must be "msal" followed by the same client ID (no "://auth" suffix, that part is in the manifest). -->
<string name="msal_client_id" translatable="false">REPLACE_WITH_YOUR_CLIENT_ID</string>
<string name="msal_redirect_scheme" translatable="false">msalREPLACE_WITH_YOUR_CLIENT_ID</string>
<string name="msal_client_id" translatable="false">61377814-56c4-4e92-b5ae-fb06e46ca864</string>
<string name="msal_redirect_scheme" translatable="false"
>msal61377814-56c4-4e92-b5ae-fb06e46ca864</string>
<string name="settings_title">Settings</string>
<string name="not_signed_in">Not signed in</string>