Skip to content

Commit d41dacd

Browse files
hahn-kevmyieye
andauthored
Enhance Android app actions, deep linking, and SvelteNavigate integration (#2353)
* Add app actions and intent handling for Android, enable deep linking, and improve URL state persistence in FwLiteMaui * Expose `SvelteNavigate` for native host integrations and improve shortcut handling in FwLiteMaui * mark the android activity as resizable to allow resizing on chromebooks * Resolve type inconsistency in `SvelteNavigate` definition, improve JSON serialization of URLs, and clean up unused imports. * Enhance FwLiteMaui by adding a new shortcut for sharing debug logs, updating the App class to accept an IServiceProvider, and improving app action handling for better integration with troubleshooting services. --------- Co-authored-by: Tim Haasdyk <tim_haasdyk@sil.org>
1 parent 129957a commit d41dacd

8 files changed

Lines changed: 157 additions & 12 deletions

File tree

backend/FwLite/FwLiteMaui/App.xaml.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
using FwLiteShared.Services;
21

32
namespace FwLiteMaui;
43

54
public partial class App : Application
65
{
6+
public IServiceProvider ServiceProvider { get; }
77
private readonly MainPage _mainPage;
8+
public static string? OverrideStartupUrl { get; set; }
89

9-
public App(MainPage mainPage, IPreferencesService preferences)
10+
public App(MainPage mainPage, IServiceProvider serviceProvider)
1011
{
12+
ServiceProvider = serviceProvider;
1113
_mainPage = mainPage;
12-
var lastUrl = preferences.Get(nameof(PreferenceKey.AppLastUrl));
13-
if (lastUrl?.StartsWith('/') == true)
14-
{
15-
mainPage.StartPath = lastUrl;
16-
}
1714
InitializeComponent();
1815
}
1916

17+
internal void LoadAppUrl(string url)
18+
{
19+
_mainPage.LoadAppUrl(url);
20+
}
21+
2022
protected override Window CreateWindow(IActivationState? activationState)
2123
{
2224
return CreateWindow(_mainPage);

backend/FwLite/FwLiteMaui/MainPage.xaml.cs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,62 @@
1+
using FwLiteShared.Services;
12
using Microsoft.AspNetCore.Components.WebView;
2-
using Microsoft.Extensions.Hosting;
33
using Microsoft.Extensions.Logging;
4-
using Microsoft.Extensions.Options;
4+
using Microsoft.JSInterop;
55

66
namespace FwLiteMaui;
77

88
public partial class MainPage : ContentPage
99
{
10-
public MainPage()
10+
private readonly ILogger<MainPage> _logger;
11+
12+
public MainPage(IPreferencesService preferences, ILogger<MainPage> logger)
1113
{
14+
_logger = logger;
1215
InitializeComponent();
16+
var lastUrlFromPrefs = preferences.Get(nameof(PreferenceKey.AppLastUrl));
1317
blazorWebView.BlazorWebViewInitializing += BlazorWebViewInitializing;
1418
blazorWebView.BlazorWebViewInitialized += BlazorWebViewInitialized;
19+
blazorWebView.BlazorWebViewInitialized += (s, e) =>
20+
{
21+
// Decide initial StartPath here (after Android intent is processed) to avoid cold-start race
22+
var initial = App.OverrideStartupUrl ?? lastUrlFromPrefs ?? "/";
23+
//only change it if it's still the default, might have been changed already, for example when opening a new window
24+
if (blazorWebView.StartPath == "/")
25+
blazorWebView.StartPath = initial;
26+
App.OverrideStartupUrl = null;
27+
};
1528
blazorWebView.UrlLoading += BlazorWebViewOnUrlLoading;
1629
}
1730

18-
19-
2031
internal string StartPath
2132
{
2233
get => blazorWebView.StartPath;
2334
set => blazorWebView.StartPath = value;
2435
}
2536

37+
internal void LoadAppUrl(string url)
38+
{
39+
blazorWebView.StartPath = url;
40+
_ = blazorWebView.TryDispatchAsync(services =>
41+
{
42+
var jsRuntime = services.GetService<IJSRuntime>();
43+
if (jsRuntime != null)
44+
_ = NavigateAsync(jsRuntime, url);
45+
});
46+
}
47+
48+
private async Task NavigateAsync(IJSRuntime jsRuntime, string url)
49+
{
50+
try
51+
{
52+
await jsRuntime.InvokeVoidAsync("lexbox.SvelteNavigate", url, new { replace = true });
53+
}
54+
catch (Exception e)
55+
{
56+
_logger.LogError(e, "Failed to navigate to {Url} via SvelteNavigate", url);
57+
}
58+
}
59+
2660
#if ANDROID || WINDOWS
2761
private partial void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e);
2862
private partial void BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e);

backend/FwLite/FwLiteMaui/MauiProgram.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using FwLiteShared.Services;
12
using Microsoft.Extensions.Logging;
23
using Microsoft.Maui.LifecycleEvents;
34

@@ -52,6 +53,41 @@ public static MauiApp CreateMauiApp()
5253
builder.ConfigureEssentials(essentialsBuilder =>
5354
{
5455
essentialsBuilder.UseVersionTracking();
56+
57+
// Register all shortcuts from a single central place
58+
foreach (var action in Shortcuts.Declarations)
59+
{
60+
essentialsBuilder.AddAppAction(action);
61+
}
62+
63+
essentialsBuilder.OnAppAction(action =>
64+
{
65+
var app = (App?)Application.Current;
66+
if (app is null) return;
67+
if (Shortcuts.TryGetUrl(action.Id, out var url))
68+
{
69+
app.Dispatcher.Dispatch(() =>
70+
{
71+
app.LoadAppUrl(url);
72+
});
73+
}
74+
else if (action.Id == Shortcuts.ShareLogOut)
75+
{
76+
_ = app.Dispatcher.DispatchAsync(async () =>
77+
{
78+
try
79+
{
80+
await app.ServiceProvider.GetRequiredService<ITroubleshootingService>()
81+
.ShareLogFile();
82+
}
83+
catch (Exception e)
84+
{
85+
app.ServiceProvider.GetService<ILogger<App>>()?
86+
.LogError(e, "Failed to share log file from app action");
87+
}
88+
});
89+
}
90+
});
5591
});
5692
builder.Services.AddFwLiteMauiServices(builder.Configuration, builder.Logging);
5793

backend/FwLite/FwLiteMaui/Platforms/Android/MainActivity.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,41 @@ namespace FwLiteMaui;
1212
[Activity(Theme = "@style/Maui.SplashTheme",
1313
MainLauncher = true,
1414
LaunchMode = LaunchMode.SingleTop,
15+
ResizeableActivity = true,
1516
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |
1617
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
18+
[IntentFilter([Platform.Intent.ActionAppAction],
19+
Categories = [Intent.CategoryDefault])]
1720
public class MainActivity : MauiAppCompatActivity
1821
{
1922
protected override void OnCreate(Bundle? savedInstanceState)
2023
{
2124
base.OnCreate(savedInstanceState);
25+
26+
if (Intent?.Action == Platform.Intent.ActionAppAction)
27+
{
28+
//name comes from internal maui code: https://github.com/dotnet/maui/blob/271d2505eb436600bb84002c8941670abb0ae23b/src/Essentials/src/AppActions/AppActions.android.cs#L83
29+
var actionId = Intent.GetStringExtra("EXTRA_XE_APP_ACTION_ID");
30+
if (Shortcuts.TryGetUrl(actionId, out var url))
31+
{
32+
App.OverrideStartupUrl = url;
33+
}
34+
}
35+
2236
ApplyBrandedSystemBars();
2337
}
2438

39+
protected override void OnResume()
40+
{
41+
base.OnResume();
42+
Platform.OnResume(this);
43+
}
44+
45+
protected override void OnNewIntent(Intent? intent)
46+
{
47+
base.OnNewIntent(intent);
48+
Platform.OnNewIntent(intent);
49+
}
2550
public override void OnConfigurationChanged(Configuration newConfig)
2651
{
2752
base.OnConfigurationChanged(newConfig);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
namespace FwLiteMaui;
2+
3+
public static class Shortcuts
4+
{
5+
public const string Home = "home";
6+
public const string ShareLogOut = "share-log-out";
7+
8+
private static readonly IReadOnlyDictionary<string, string> IdToUrl = new Dictionary<string, string>
9+
{
10+
[Home] = "/",
11+
};
12+
13+
// Titles/subtitles shown in the system UI (if supported)
14+
public static readonly IReadOnlyList<AppAction> Declarations =
15+
[
16+
new(Home, "Home"),
17+
new(ShareLogOut, "Share Debug Log"),
18+
];
19+
20+
public static bool TryGetUrl(string? id, out string url)
21+
{
22+
if (id is null)
23+
{
24+
url = string.Empty;
25+
return false;
26+
}
27+
if (IdToUrl.TryGetValue(id, out var found))
28+
{
29+
url = found;
30+
return true;
31+
}
32+
url = string.Empty;
33+
return false;
34+
}
35+
}

backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ static int Fail(string message)
5353
if (cctor is null || !cctor.HasBody)
5454
return Fail("SqlTransparentExpression .cctor not found (or has no body).");
5555

56+
if (cctor.Body.Instructions.Count == 1 && cctor.Body.Instructions[0].OpCode == OpCodes.Ret)
57+
{
58+
Console.WriteLine($"Already patched (inferred from IL): {dllPath}");
59+
File.WriteAllText(markerPath, DateTime.UtcNow.ToString("O"));
60+
return 0;
61+
}
62+
5663
// Sanity-check the cctor shape: at least one stsfld targeting the _ctor field.
5764
// If upstream renames _ctor or restructures the field init, we want to know.
5865
var storesCtorField = cctor.Body.Instructions.Any(ins =>

frontend/viewer/src/lib/services/service-declaration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ declare global {
1010
ServiceProvider: LexboxServiceProvider;
1111
Search: { openSearch: (search: string) => void };
1212
IsDotnetHosted?: boolean;
13+
// Expose svelte-routing navigate for native hosts (MAUI) and other integrations
14+
SvelteNavigate?: (url: string, options?: { replace?: boolean }) => void;
1315
/* eslint-enable @typescript-eslint/naming-convention */
1416
}
1517

frontend/viewer/src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import '@formatjs/intl-durationformat/polyfill';
77

88
import App from './App.svelte';
99
import {mount} from 'svelte';
10+
import {navigate} from 'svelte-routing';
1011
import {setupDotnetServiceProvider} from './lib/services/service-provider-dotnet';
1112
import {setupServiceProvider} from '$lib/services/service-provider';
1213
import {setupBrowserAppServices} from '$lib/services/browser-app-services';
@@ -20,6 +21,9 @@ if (!window.lexbox.IsDotnetHosted) {
2021
}
2122
useEventBus();
2223

24+
// Wire up globally-accessible helpers for hosts (e.g., MAUI)
25+
window.lexbox.SvelteNavigate = (url: string, options?: { replace?: boolean }) => navigate(url, options);
26+
2327
//don't mount the app until after we've loaded the local
2428
void setLanguage('default')
2529
.then(() => {

0 commit comments

Comments
 (0)