Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AllStream.Shared/Models/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public class Settings
public string TmdbApiKey { get; set; } = string.Empty;
public string NoAdsProxyBaseUrl { get; set; } = string.Empty;
public string LiveSportApiKey { get; set; } = "https://livesport.su/api/";
public string UpdateManifestUrl { get; set; } = string.Empty;
}
20 changes: 20 additions & 0 deletions AllStream.Shared/Models/UpdateManifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;

namespace AllStream.Shared.Models;

public class UpdateManifest
{
[JsonPropertyName("tag_name")]
public required string TagName { get; set; }

public int[] Version => TagName.Split("v")[1].Split(".").Select(x => int.Parse(x)).ToArray();
public required bool Prerelease { get; set; }
public required Asset[] Assets { get; set; }
}

public class Asset
{
[JsonPropertyName("browser_download_url")]
public required string DownloadUrl { get; set; }
public required string Name { get; set; }
}
34 changes: 34 additions & 0 deletions AllStream.Shared/Services/AppUpdateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Net.Http.Json;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using AllStream.Shared.Models;

namespace AllStream.Shared.Services
{
public class AppUpdateService(HttpClient client, string manifestUrl) : IAppUpdateService
{
public async Task<Asset?> CheckForUpdatesAsync(string? currentVersion, string formFactor)

Check failure on line 13 in AllStream.Shared/Services/AppUpdateService.cs

View workflow job for this annotation

GitHub Actions / android-ui-tests

'AppUpdateService.CheckForUpdatesAsync(string?, string)': not all code paths return a value

Check failure on line 13 in AllStream.Shared/Services/AppUpdateService.cs

View workflow job for this annotation

GitHub Actions / android-ui-tests

'AppUpdateService.CheckForUpdatesAsync(string?, string)': not all code paths return a value

Check warning on line 13 in AllStream.Shared/Services/AppUpdateService.cs

View workflow job for this annotation

GitHub Actions / android-ui-tests

Nullability of reference types in return type of 'Task<Asset?> AppUpdateService.CheckForUpdatesAsync(string? currentVersion, string formFactor)' doesn't match implicitly implemented member 'Task<Asset> IAppUpdateService.CheckForUpdatesAsync(string? currentVersion, string formFactor)'.
{

}

public async Task<UpdateManifest?> GetLatestRelease()
{
var manifest = await client.GetFromJsonAsync<UpdateManifest>(manifestUrl, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
});
return manifest;
switch (formFactor)

Check failure on line 25 in AllStream.Shared/Services/AppUpdateService.cs

View workflow job for this annotation

GitHub Actions / android-ui-tests

The name 'formFactor' does not exist in the current context

Check warning on line 25 in AllStream.Shared/Services/AppUpdateService.cs

View workflow job for this annotation

GitHub Actions / android-ui-tests

Unreachable code detected

Check failure on line 25 in AllStream.Shared/Services/AppUpdateService.cs

View workflow job for this annotation

GitHub Actions / android-ui-tests

The name 'formFactor' does not exist in the current context
{
case "Android":
return manifest.Assets.FirstOrDefault(x => x.Name.Contains(".apk"));

Check failure on line 28 in AllStream.Shared/Services/AppUpdateService.cs

View workflow job for this annotation

GitHub Actions / android-ui-tests

Cannot implicitly convert type 'AllStream.Shared.Models.Asset' to 'AllStream.Shared.Models.UpdateManifest'

Check warning on line 28 in AllStream.Shared/Services/AppUpdateService.cs

View workflow job for this annotation

GitHub Actions / android-ui-tests

Unreachable code detected

Check failure on line 28 in AllStream.Shared/Services/AppUpdateService.cs

View workflow job for this annotation

GitHub Actions / android-ui-tests

Cannot implicitly convert type 'AllStream.Shared.Models.Asset' to 'AllStream.Shared.Models.UpdateManifest'
default:
return null;

Check warning on line 30 in AllStream.Shared/Services/AppUpdateService.cs

View workflow job for this annotation

GitHub Actions / android-ui-tests

Unreachable code detected
}
}
}
}
10 changes: 10 additions & 0 deletions AllStream.Shared/Services/IAppUpdateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using AllStream.Shared.Models;

namespace AllStream.Shared.Services
{
public interface IAppUpdateService
{
Task<Asset> CheckForUpdatesAsync(string? currentVersion, string formFactor);
}
}
3 changes: 3 additions & 0 deletions AllStream.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
.AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(AllStream.Shared._Imports).Assembly);

var manifest = builder.Configuration.GetSection("Manifest").Get<UpdateManifest>() ?? new UpdateManifest();
app.MapGet("/manifest.json", () => Results.Json(manifest));

app.Run();

public partial class Program { }
9 changes: 7 additions & 2 deletions AllStream.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@
}
},
"AllowedHosts": "*",
"TmdbApiKey": "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJkZDkyMzY0MWVhZTIxYWE1OWZhZjczYWE4OTdlNGUzYiIsIm5iZiI6MTc2Mzk4NTkzOC45ODUsInN1YiI6IjY5MjQ0YTEyNjk3YzljMWI5NWI3NDliNyIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.U-nF0JTZLIcMIXR8qFZuWwPI3bo2CuDfinmijRyuTJo"
}
"TmdbApiKey": "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJkZDkyMzY0MWVhZTIxYWE1OWZhZjczYWE4OTdlNGUzYiIsIm5iZiI6MTc2Mzk4NTkzOC45ODUsInN1YiI6IjY5MjQ0YTEyNjk3YzljMWI5NWI3NDliNyIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.U-nF0JTZLIcMIXR8qFZuWwPI3bo2CuDfinmijRyuTJo",
"Manifest": {
"LatestVersion": "1.0.0",
"ApkUrl": "https://example.com/allstream.apk",
"ReleaseNotes": "Initial release"
}
}
7 changes: 0 additions & 7 deletions AllStream.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AllStream.Web.Tests", "AllS
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AllStream.Android.UITests", "AllStream.Android.UITests\AllStream.Android.UITests.csproj", "{F2C0A9E9-AC3D-4D6C-9329-9A3A5B268D12}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{FF9F4FC5-0330-4E83-928E-C908C8F7A58F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -58,9 +56,4 @@ Global
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {20E6FD03-9002-4EBA-ABF2-9DDE2C488842}
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F2C0A9E9-AC3D-4D6C-9329-9A3A5B268D12} = {FF9F4FC5-0330-4E83-928E-C908C8F7A58F}
{B8D3F38C-0E9E-4D9A-8AF6-8B9F7D5D0E10} = {FF9F4FC5-0330-4E83-928E-C908C8F7A58F}
{6D7ABF43-5B3E-4F76-8D0A-0E48798A8C3A} = {FF9F4FC5-0330-4E83-928E-C908C8F7A58F}
EndGlobalSection
EndGlobal
17 changes: 16 additions & 1 deletion AllStream/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ public MainPage()
#endif
}

#if ANDROID
void OnLoaded(object? sender, EventArgs e)
{
// WebView configuration is now handled in MauiProgram.cs via Handler Mapping
TryCheckUpdates();
}

#if ANDROID

static double GetStatusBarHeightDp()
{
var res = Android.App.Application.Context.Resources;
Expand All @@ -35,5 +37,18 @@ static double GetStatusBarHeightDp()

return density > 0 ? px / density : 0;
}

void TryCheckUpdates()
{
try
{
var svc = this.Handler?.MauiContext?.Services?.GetService<AllStream.Shared.Services.IAppUpdateService>();
if (svc != null)
{
Dispatcher.Dispatch(async () => await svc.CheckForUpdatesAsync());
}
}
catch { }
}
#endif
}
6 changes: 5 additions & 1 deletion AllStream/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ public static MauiApp CreateMauiApp()
}
Settings settingsFromJson = builder.Configuration.Get<Settings>() ?? new Settings();
builder.Services.AddMauiBlazorWebView();
builder.Services.AddSharedServices(sp => new FormFactor(), settingsFromJson);
builder.Services.AddSharedServices(sp => new FormFactor(),
settingsFromJson);
#if ANDROID
builder.Services.AddSingleton<AllStream.Shared.Services.IAppUpdateService, AllStream.Platforms.Android.Updates.AndroidAppUpdateService>();
#endif

#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
Expand Down
83 changes: 83 additions & 0 deletions AllStream/Platforms/Android/Updates/AndroidAppUpdateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Microsoft.Maui.ApplicationModel;
using AllStream.Shared.Models;
using AllStream.Shared.Services;

namespace AllStream.Platforms.Android.Updates
{
public class AndroidAppUpdateService : IAppUpdateService
{
private readonly Settings _settings;
private readonly IHttpClientFactory _httpClientFactory;

public AndroidAppUpdateService(Settings settings)
{
_settings = settings;
_httpClientFactory = new HttpClientFactory();
}

public async Task CheckForUpdatesAsync()
{
try
{
if (string.IsNullOrWhiteSpace(_settings.UpdateManifestUrl)) return;
var client = _httpClientFactory.CreateClient();
using var res = await client.GetAsync(_settings.UpdateManifestUrl);
if (!res.IsSuccessStatusCode) return;
var json = await res.Content.ReadAsStringAsync();
var manifest = JsonSerializer.Deserialize<UpdateManifest>(json);
if (manifest == null) return;

var currentVersion = AppInfo.Current.VersionString;
if (IsNewer(manifest.LatestVersion, currentVersion))
{
await ShowPromptAndUpdate(manifest.ApkUrl, manifest.ReleaseNotes);
}
}
catch { }
}

private static bool IsNewer(string latest, string current)
{
Version vLatest, vCurrent;
if (Version.TryParse(latest, out vLatest) && Version.TryParse(current, out vCurrent))
{
return vLatest > vCurrent;
}
return !string.Equals(latest, current, StringComparison.OrdinalIgnoreCase);
}

private static async Task ShowPromptAndUpdate(string apkUrl, string notes)
{
try
{
if (string.IsNullOrWhiteSpace(apkUrl)) return;
var title = "Update Available";
var message = string.IsNullOrWhiteSpace(notes) ? "A new version is available. Update now?" : notes;
var ok = await MainThread.InvokeOnMainThreadAsync(() => Microsoft.Maui.Controls.Application.Current!.MainPage!.DisplayAlert(title, message, "Update", "Later"));
if (ok)
{
await Launcher.OpenAsync(new Uri(apkUrl));
}
}
catch { }
}

private class HttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name = "default") => new HttpClient();
}

private class UpdateManifest
{
public string LatestVersion { get; set; } = string.Empty;
public string ApkUrl { get; set; } = string.Empty;
public string ReleaseNotes { get; set; } = string.Empty;
}
}
}
Loading