Skip to content

Commit 29d6c5d

Browse files
committed
Added push notification for when vault is unlocked on Android
1 parent a4174c7 commit 29d6c5d

7 files changed

Lines changed: 212 additions & 6 deletions

File tree

src/Core/SecureFolderFS.Core.MobileFS/SecureFolderFS.Core.MobileFS.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<Nullable>enable</Nullable>
1212

1313
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">17.0</SupportedOSPlatformVersion>
14-
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">28.0</SupportedOSPlatformVersion>
14+
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">29.0</SupportedOSPlatformVersion>
1515
</PropertyGroup>
1616

1717
<ItemGroup>

src/Platforms/SecureFolderFS.Maui/Platforms/Android/AndroidManifest.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@
1313
<action android:name="androidx.media3.session.MediaSessionService"/>
1414
</intent-filter>
1515
</service>
16+
17+
<service
18+
android:name="securefolderfs.VaultForegroundService"
19+
android:foregroundServiceType="dataSync"
20+
android:exported="false" />
1621
</application>
22+
1723
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
1824
<uses-permission android:name="android.permission.INTERNET" />
1925
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
26+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
2027
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
2128
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
2229
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />

src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidLifecycleHelper.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
using Android.Content;
2+
using Android.OS;
3+
using CommunityToolkit.Mvvm.Messaging;
14
using OwlCore.Storage;
25
using OwlCore.Storage.System.IO;
36
using SecureFolderFS.Maui.Extensions;
47
using SecureFolderFS.Maui.Platforms.Android.ServiceImplementation;
8+
using SecureFolderFS.Sdk.Messages;
59
using SecureFolderFS.Sdk.Services;
610
using SecureFolderFS.Shared.Extensions;
711
using SecureFolderFS.UI;
@@ -12,8 +16,10 @@
1216
namespace SecureFolderFS.Maui.Platforms.Android.Helpers
1317
{
1418
/// <inheritdoc cref="BaseLifecycleHelper"/>
15-
internal sealed class AndroidLifecycleHelper : BaseLifecycleHelper
19+
internal sealed class AndroidLifecycleHelper : BaseLifecycleHelper, IRecipient<VaultUnlockedMessage>
1620
{
21+
private bool _isForegroundServiceStarted;
22+
1723
/// <inheritdoc/>
1824
public override string AppDirectory { get; } = FileSystem.Current.AppDataDirectory;
1925

@@ -25,6 +31,7 @@ public override Task InitAsync(CancellationToken cancellationToken = default)
2531
var settingsFolder = new SystemFolder(Directory.CreateDirectory(settingsFolderPath));
2632
ConfigureServices(settingsFolder);
2733

34+
WeakReferenceMessenger.Default.Register(this);
2835
return Task.CompletedTask;
2936
}
3037

@@ -33,6 +40,26 @@ public override void LogExceptionToFile(Exception? ex)
3340
{
3441
_ = ex;
3542
}
43+
44+
/// <inheritdoc/>
45+
public async void Receive(VaultUnlockedMessage message)
46+
{
47+
if (_isForegroundServiceStarted || MainActivity.Instance is null)
48+
return;
49+
50+
// Start the vault foreground service so it's ready to receive messenger messages
51+
var serviceIntent = new Intent(MainActivity.Instance, typeof(VaultForegroundService));
52+
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
53+
MainActivity.Instance.StartForegroundService(serviceIntent);
54+
else
55+
MainActivity.Instance.StartService(serviceIntent);
56+
57+
_isForegroundServiceStarted = true;
58+
59+
// Add the initial vault
60+
var foregroundService = await VaultForegroundService.GetInstanceAsync();
61+
foregroundService.UnlockedVaults.Add(message.VaultModel);
62+
}
3663

3764
/// <inheritdoc/>
3865
protected override IServiceCollection ConfigureServices(IModifiableFolder settingsFolder)
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using Android.App;
2+
using Android.Content;
3+
using Android.Content.PM;
4+
using Android.OS;
5+
using AndroidX.Core.App;
6+
using CommunityToolkit.Mvvm.Messaging;
7+
using Microsoft.Maui.Platform;
8+
using SecureFolderFS.Sdk.Extensions;
9+
using SecureFolderFS.Sdk.Messages;
10+
using SecureFolderFS.Sdk.Models;
11+
12+
namespace SecureFolderFS.Maui.Platforms.Android.ServiceImplementation
13+
{
14+
[Service(
15+
ForegroundServiceType = ForegroundService.TypeDataSync,
16+
Exported = false)]
17+
public class VaultForegroundService : Service, IRecipient<VaultUnlockedMessage>, IRecipient<VaultLockedMessage>
18+
{
19+
private const string CHANNEL_ID = "securefolderfs_vault_foreground_service";
20+
private const int NOTIFICATION_ID = 4949;
21+
public const string LockAll = "securefolderfs.action.LOCK_ALL";
22+
23+
private static TaskCompletionSource<VaultForegroundService> InstanceTcs { get; } = new();
24+
25+
public List<IVaultModel> UnlockedVaults { get; } = new();
26+
27+
public static async Task<VaultForegroundService> GetInstanceAsync()
28+
{
29+
return await InstanceTcs.Task;
30+
}
31+
32+
/// <inheritdoc/>
33+
public override IBinder? OnBind(Intent? intent)
34+
{
35+
return null;
36+
}
37+
38+
/// <inheritdoc/>
39+
public override void OnCreate()
40+
{
41+
InstanceTcs.TrySetResult(this);
42+
43+
base.OnCreate();
44+
EnsureNotificationChannel();
45+
WeakReferenceMessenger.Default.Register<VaultLockedMessage>(this);
46+
WeakReferenceMessenger.Default.Register<VaultUnlockedMessage>(this);
47+
}
48+
49+
/// <inheritdoc/>
50+
public override StartCommandResult OnStartCommand(Intent? intent, StartCommandFlags flags, int startId)
51+
{
52+
if (intent?.Action == LockAll)
53+
{
54+
foreach (var vault in UnlockedVaults.ToList())
55+
WeakReferenceMessenger.Default.Send(new VaultLockRequestedMessage(vault));
56+
57+
return StartCommandResult.Sticky;
58+
}
59+
60+
var notification = BuildNotification();
61+
if (notification is null)
62+
return StartCommandResult.NotSticky;
63+
64+
StartForeground(NOTIFICATION_ID, notification, ForegroundService.TypeDataSync);
65+
return StartCommandResult.Sticky;
66+
}
67+
68+
/// <inheritdoc/>
69+
public override void OnDestroy()
70+
{
71+
WeakReferenceMessenger.Default.Unregister<VaultLockedMessage>(this);
72+
WeakReferenceMessenger.Default.Unregister<VaultUnlockedMessage>(this);
73+
base.OnDestroy();
74+
}
75+
76+
/// <inheritdoc/>
77+
public void Receive(VaultUnlockedMessage message)
78+
{
79+
UnlockedVaults.Add(message.VaultModel);
80+
UpdateNotification();
81+
}
82+
83+
/// <inheritdoc/>
84+
public void Receive(VaultLockedMessage message)
85+
{
86+
UnlockedVaults.Remove(message.VaultModel);
87+
UpdateNotification();
88+
}
89+
90+
private void UpdateNotification()
91+
{
92+
var manager = NotificationManagerCompat.From(this);
93+
if (manager is null)
94+
return;
95+
96+
var notification = BuildNotification();
97+
manager.Notify(NOTIFICATION_ID, notification);
98+
}
99+
100+
private Notification? BuildNotification()
101+
{
102+
var count = UnlockedVaults.Count;
103+
var title = count switch
104+
{
105+
0 => "VaultUnlocked".ToLocalized(),
106+
1 => "OneVaultIsUnlocked".ToLocalized(),
107+
_ => "MultipleVaultsAreUnlocked".ToLocalized(count)
108+
};
109+
110+
// Tapping the notification brings the app back to foreground
111+
var tapIntent = new Intent(this, typeof(MainActivity));
112+
tapIntent.SetFlags(ActivityFlags.SingleTop | ActivityFlags.ClearTop);
113+
var tapPendingIntent = PendingIntent.GetActivity(
114+
this, 0, tapIntent,
115+
PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);
116+
117+
// "Lock all" action
118+
var lockAllIntent = new Intent(this, typeof(VaultForegroundService));
119+
lockAllIntent.SetAction(LockAll);
120+
var lockAllPendingIntent = PendingIntent.GetService(
121+
this, 0, lockAllIntent,
122+
PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);
123+
124+
var iconRid = MauiApplication.Current.GetDrawableId("app_icon.png");
125+
if (iconRid == 0)
126+
iconRid = 0x0108002f; // ic_lock_lock
127+
128+
return new NotificationCompat.Builder(this, CHANNEL_ID)
129+
.SetContentTitle(title)
130+
?.SetContentText("TapToLockAll".ToLocalized())
131+
?.SetSmallIcon(iconRid)
132+
?.SetContentIntent(tapPendingIntent)
133+
?.AddAction(0, "LockAll".ToLocalized(), lockAllPendingIntent)
134+
?.SetOngoing(true)
135+
?.SetOnlyAlertOnce(true)
136+
?.Build();
137+
}
138+
139+
private void EnsureNotificationChannel()
140+
{
141+
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
142+
return;
143+
144+
var channel = new NotificationChannel(
145+
CHANNEL_ID,
146+
"Vault Status",
147+
NotificationImportance.Low) // Low = no sound, no heads-up
148+
{
149+
Description = "Shows which vaults are currently unlocked"
150+
};
151+
152+
var manager = GetSystemService(NotificationService) as NotificationManager;
153+
manager?.CreateNotificationChannel(channel);
154+
}
155+
}
156+
}

src/Platforms/SecureFolderFS.Maui/SecureFolderFS.Maui.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<MauiEnableXamlCBindingWithSourceCompilation>True</MauiEnableXamlCBindingWithSourceCompilation>
2828

2929
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">17.0</SupportedOSPlatformVersion>
30-
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">28.0</SupportedOSPlatformVersion>
30+
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">29.0</SupportedOSPlatformVersion>
3131
</PropertyGroup>
3232

3333
<PropertyGroup Condition="$(TargetFramework.Contains('-ios')) and '$(Configuration)' == 'Debug'">

src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,4 +1418,19 @@
14181418
<data name="ItemAlreadyExists" xml:space="preserve">
14191419
<value>Item with the same name already exists</value>
14201420
</data>
1421+
<data name="TapToLockAll" xml:space="preserve">
1422+
<value>Tap to lock all vaults</value>
1423+
</data>
1424+
<data name="OneVaultIsUnlocked" xml:space="preserve">
1425+
<value>One vault is unlocked</value>
1426+
</data>
1427+
<data name="MultipleVaultsAreUnlocked" xml:space="preserve">
1428+
<value>{0} vaults are unlocked</value>
1429+
</data>
1430+
<data name="ViewInApp" xml:space="preserve">
1431+
<value>View in app</value>
1432+
</data>
1433+
<data name="ExitApp" xml:space="preserve">
1434+
<value>Exit</value>
1435+
</data>
14211436
</root>

src/Platforms/SecureFolderFS.Uno/UserControls/InterfaceRoot/MainWindowRootControl.xaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
55
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
66
xmlns:hni="using:H.NotifyIcon"
7+
xmlns:l="using:SecureFolderFS.Uno.Localization"
78
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
89
xmlns:not_win="http://uno.ui/not_win"
910
xmlns:svfs="using:SecureFolderFS.Storage.VirtualFileSystem"
@@ -87,21 +88,21 @@
8788
ToolTipText="SecureFolderFS">
8889
<hni:TaskbarIcon.ContextFlyout>
8990
<MenuFlyout Placement="Top">
90-
<MenuFlyoutItem Click="MenuShowApp_Click" Text="View in app">
91+
<MenuFlyoutItem Click="MenuShowApp_Click" Text="{l:ResourceString Rid=ViewInApp}">
9192
<MenuFlyoutItem.Icon>
9293
<FontIcon Glyph="&#xE8A7;" />
9394
</MenuFlyoutItem.Icon>
9495
</MenuFlyoutItem>
9596
<MenuFlyoutItem
9697
Click="MenuLockAll_Click"
9798
IsEnabled="{x:Bind svfs:FileSystemManager.Instance.FileSystems.Count, Mode=OneWay, Converter={StaticResource CountToBoolConverter}}"
98-
Text="Lock all vaults">
99+
Text="{l:ResourceString Rid=LockAll}">
99100
<MenuFlyoutItem.Icon>
100101
<FontIcon Glyph="&#xE72E;" />
101102
</MenuFlyoutItem.Icon>
102103
</MenuFlyoutItem>
103104
<MenuFlyoutSeparator />
104-
<MenuFlyoutItem Click="MenuCloseApp_Click" Text="Exit">
105+
<MenuFlyoutItem Click="MenuCloseApp_Click" Text="{l:ResourceString Rid=ExitApp}">
105106
<MenuFlyoutItem.Icon>
106107
<FontIcon Glyph="&#xF78A;" />
107108
</MenuFlyoutItem.Icon>

0 commit comments

Comments
 (0)