Skip to content
Merged
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
152 changes: 152 additions & 0 deletions .github/workflows/build-notifications-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
name: Build Notifications Plugin

# Builds plugins/Notifications (sokol_notifications, local scheduled notifications) on every change to the plugin,
# and commits the prebuilt Android and iOS libraries back to plugins/Notifications/libs/ on main — every shipped
# library is a CI build, never a local one. Android and iOS are the only native backends; the managed layer (a stub
# elsewhere) is compiled for every platform symbol set. A change is not done until every job is green.

on:
push:
branches: [ main ]
paths:
- 'plugins/Notifications/**'
- '!plugins/Notifications/libs/**'
- '.github/workflows/build-notifications-plugin.yml'
pull_request:
branches: [ main ]
paths:
- 'plugins/Notifications/**'
- '!plugins/Notifications/libs/**'
- '.github/workflows/build-notifications-plugin.yml'
workflow_dispatch:

permissions:
contents: write

jobs:
build-android:
name: Build Android (arm64-v8a, armeabi-v7a, x86_64)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
packages: ''
- name: Install Android NDK
run: |
sdkmanager --install "ndk;27.2.12479018"
echo "ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/27.2.12479018" >> $GITHUB_ENV
- name: Build
run: ./plugins/Notifications/scripts/build-android.sh
- name: Check 16 KB page alignment (Google Play, Android 15+ — 64-bit ABIs only, as the NDK flag)
run: |
READELF="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf"
for so in plugins/Notifications/libs/android/{arm64-v8a,x86_64}/release/libsokol_notifications.so; do
align=$("$READELF" -l "$so" | awk '/LOAD/ {print $NF; exit}')
echo "$so: LOAD align $align"
[ "$align" = "0x4000" ] || { echo "::error::$so is not 16 KB aligned ($align)"; exit 1; }
done
- uses: actions/upload-artifact@v4
with:
name: notifications-android
path: plugins/Notifications/libs/android/**/*
retention-days: 30

build-ios:
name: Build iOS (${{ matrix.target }})
runs-on: macos-latest
strategy:
matrix:
target: [device, simulator-arm64, simulator-x64]
steps:
- uses: actions/checkout@v4
- name: Setup CMake
uses: lukka/get-cmake@latest
- name: Build
run: ./plugins/Notifications/scripts/build-ios.sh ${{ matrix.target }}
- name: Determine upload path
id: upload_path
run: |
case "${{ matrix.target }}" in
device) echo "path=plugins/Notifications/libs/ios/arm64" >> $GITHUB_OUTPUT ;;
simulator-arm64) echo "path=plugins/Notifications/libs/ios/simulator-arm64" >> $GITHUB_OUTPUT ;;
simulator-x64) echo "path=plugins/Notifications/libs/ios/simulator-x64" >> $GITHUB_OUTPUT ;;
esac
- uses: actions/upload-artifact@v4
with:
name: notifications-ios-${{ matrix.target }}
path: ${{ steps.upload_path.outputs.path }}/**/*
retention-days: 30

build-managed:
name: Managed layer (every platform symbol set)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Compile the managed layer for every platform symbol set
run: |
set -e
mkdir -p /tmp/notifications-managed && cd /tmp/notifications-managed
cat > notifications.csproj <<'PROJ'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DefineConstants>$(DefineConstants);$(NotificationsSymbols)</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(GITHUB_WORKSPACE)/plugins/Notifications/managed/*.cs" />
</ItemGroup>
</Project>
PROJ
for SYMS in "" "__LINUX__" "__WINDOWS__" "__MACOS__" "__ANDROID__" "__IOS__" "WEB"; do
echo "=== DefineConstants: '${SYMS:-<none>}'"
dotnet build -c Release -p:NotificationsSymbols="$SYMS" --nologo -v q
done

commit-libs:
name: Commit the Android and iOS libraries
needs: [build-android, build-ios, build-managed]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
steps:
- name: Download the artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Commit the libraries
run: |
git clone https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git repo
cd repo
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
mkdir -p plugins/Notifications/libs/android plugins/Notifications/libs/ios/arm64 \
plugins/Notifications/libs/ios/simulator-arm64 plugins/Notifications/libs/ios/simulator-x64
cp -r ../artifacts/notifications-android/* plugins/Notifications/libs/android/
cp -r ../artifacts/notifications-ios-device/* plugins/Notifications/libs/ios/arm64/
cp -r ../artifacts/notifications-ios-simulator-arm64/* plugins/Notifications/libs/ios/simulator-arm64/
cp -r ../artifacts/notifications-ios-simulator-x64/* plugins/Notifications/libs/ios/simulator-x64/
LIBS="plugins/Notifications/libs"
if [ -z "$(git status --porcelain $LIBS)" ]; then
echo "No sokol_notifications library change to commit"
exit 0
fi
git add $LIBS
git commit -m "Auto-update built sokol_notifications libraries (Android, iOS) [skip ci]" \
-m "Generated from commit: ${{ github.sha }}" \
-m "Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
git pull --rebase origin main
git push origin main
echo "✓ Committed sokol_notifications libraries"
13 changes: 13 additions & 0 deletions docs/ANDROID_PROPERTIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@ Each request is bounded by the permission's own API window:
> app sees a plain denial. Any UI that waits for the user's answer then waits for an answer that can
> never arrive.

#### Asking later instead of at launch (`AndroidPermissionsNoAutoRequest`)

A permission the app requests itself, at a moment that makes sense to the user, is listed here too.
It stays in the manifest but is left out of the launch-time prompt:

```xml
<AndroidPermissions>android.permission.BLUETOOTH_SCAN;android.permission.POST_NOTIFICATIONS</AndroidPermissions>
<AndroidPermissionsNoAutoRequest>android.permission.POST_NOTIFICATIONS</AndroidPermissionsNoAutoRequest>
```

Typical use: notifications (`plugins/Notifications` asks for `POST_NOTIFICATIONS` when the app calls
`Notifications.RequestPermission()`).

### Bluetooth / BLE permissions

Android 12 (API 31) replaced the Bluetooth permission model outright, so an app supporting anything
Expand Down
71 changes: 71 additions & 0 deletions plugins/Notifications/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Notifications plugin

Local (on-device) scheduled notifications: the app schedules its own text for a time, and the OS shows it even when
the app isn't running. No push service, no server, no SDK account — nothing leaves the device.

| Platform | Backend |
|----------|---------|
| Android 8+ (API 26) | WorkManager one-time work posts the notification (survives reboots and app updates; time approximate) |
| iOS 15+ | `UNUserNotificationCenter` with a calendar trigger (fires on time, survives reboots) |
| Desktop, Web | none — `Notifications.Available == false`, permission reads `Denied`, calls complete as no-ops |

## API

```csharp
Notifications.Init(); // once at start
Notifications.Configure("Reminders", "icons/notify.png"); // Android: channel name + small icon (asset path)
Notifications.OnEvent += e => { /* Permission / Pending / PendingDone */ };
// every frame:
Notifications.Poll();

if (Notifications.Permission == NotificationPermission.NotDetermined)
Notifications.RequestPermission(); // → Permission event (Granted / Denied)

Notifications.Schedule(id: 1, at: DateTimeOffset.Now.AddDays(2), expires: null, "My App", "Time for a game?");
Notifications.Cancel(1);
Notifications.CancelAll();

int tapped = Notifications.TakeOpened(); // id the user tapped to open the app, once (0 = none)
Notifications.OpenSettings(); // after a denial
```

- **Ids** are chosen by the app, 1..63 (iOS keeps at most 64 pending requests per app). Scheduling a pending id
replaces it. `Cancel` / `CancelAll` also remove a notification already shown.
- **Text** comes from the app, already localized — the plugin holds none.
- **Android timing:** Doze and battery savers can run the work late. Pass `expires` to drop a notification that
would arrive too late (e.g. at night) instead of showing it.
- **The app in front:** iOS shows nothing while the app is in the foreground; Android shows it.
- **`TakeOpened`:** check it at start and on every return to the foreground (`SAPP_EVENTTYPE_RESUMED`). Android
delivers a tap on a running app through `onNewIntent`, which needs the `singleTask` `SokolNativeActivity`
(`AndroidFullscreen`, the default).

## Permission

- **Android 13+** needs the `POST_NOTIFICATIONS` runtime permission. Declare it, and keep it out of the generic
launch-time prompt so the app can ask at a moment that makes sense:
```xml
<AndroidPermissions>…;android.permission.POST_NOTIFICATIONS</AndroidPermissions>
<AndroidPermissionsNoAutoRequest>android.permission.POST_NOTIFICATIONS</AndroidPermissionsNoAutoRequest>
```
Android shows no dialog at all after two refusals; `RequestPermission` then answers `Denied` and the app should
offer `OpenSettings()`.
- **Android 8–12:** no prompt; notifications are on unless the user switched them off.
- **iOS:** `requestAuthorization(.alert, .sound)`. No entitlement, no APNs, no `UIBackgroundModes`.

## Opt-in (Directory.Build.props)

```xml
<AndroidNativeLibrary_sokol_notificationsPath>../../plugins/Notifications/libs/android</AndroidNativeLibrary_sokol_notificationsPath>
<AndroidJavaSource_sokolnotificationsPath>../../plugins/Notifications/platform/android/java</AndroidJavaSource_sokolnotificationsPath>
<IOSNativeLibrary_sokol_notificationsPath>../../plugins/Notifications/libs/ios/arm64/release</IOSNativeLibrary_sokol_notificationsPath>
```
```xml
<Compile Include="$(SokolNetHome)/plugins/Notifications/managed/*.cs" />
```
`androidx.work:work-runtime` comes in through `platform/android/gradle-deps.txt` automatically.

## Building

The shipped libraries are built by CI (`.github/workflows/build-notifications-plugin.yml`). Local builds, for
development only: `scripts/build-android.sh` (needs `ANDROID_NDK` and the prebuilt `libsokol.so`) and
`scripts/build-ios.sh [device|simulator-arm64|simulator-x64|all]`.
Loading
Loading