diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bafccd40..b2c2c021 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -36,6 +36,17 @@ android:grantUriPermissions="true"> + + + + diff --git a/android/app/src/main/java/social/flotilla/MainActivity.java b/android/app/src/main/java/social/flotilla/MainActivity.java index e86ef463..e1fca734 100644 --- a/android/app/src/main/java/social/flotilla/MainActivity.java +++ b/android/app/src/main/java/social/flotilla/MainActivity.java @@ -1,5 +1,8 @@ package social.flotilla; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.os.Build; import android.os.Bundle; import com.getcapacitor.BridgeActivity; @@ -10,6 +13,28 @@ public class MainActivity extends BridgeActivity { @Override public void onCreate(Bundle savedInstanceState) { registerPlugin(AndroidPushFallbackPlugin.class); + createPushNotificationChannel(); super.onCreate(savedInstanceState); } + + // Ensures the channel named by the FCM default_notification_channel_id metadata + // exists. FCM does not create it for us; if it's missing when a notification + // arrives, Android falls back to a generic "Miscellaneous" channel. Channels + // persist once created, so doing this on launch is sufficient. + private void createPushNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationManager manager = getSystemService(NotificationManager.class); + String id = getString(R.string.default_notification_channel_id); + + if (manager != null && manager.getNotificationChannel(id) == null) { + NotificationChannel channel = new NotificationChannel( + id, + "Notifications", + NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription("New messages and activity"); + manager.createNotificationChannel(channel); + } + } + } } diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 4cb42b63..088e07bf 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -4,4 +4,5 @@ Flotilla social.flotilla social.flotilla + flotilla_push diff --git a/build.sh b/build.sh index b9229dc3..3a8b6d5e 100755 --- a/build.sh +++ b/build.sh @@ -40,3 +40,6 @@ npx @capacitor/assets generate \ --iconBackgroundColorDark '#222222' \ --splashBackgroundColor '#ffffff' \ --splashBackgroundColorDark '#191E24' + +# @capacitor/assets doesn't generate Android notification icons +node scripts/generate-notification-icon.mjs diff --git a/package.json b/package.json index 78956a9f..1768be24 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "postcss": "^8.5.15", "prettier": "^3.8.1", "prettier-plugin-svelte": "^3.4.1", + "sharp": "0.35.0-rc.0", "svelte": "^5.55.9", "svelte-check": "^4.3.5", "tailwindcss": "^4.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e09364f..53fb07dd 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ diff --git a/scripts/generate-notification-icon.mjs b/scripts/generate-notification-icon.mjs new file mode 100644 index 00000000..789cc3f7 --- /dev/null +++ b/scripts/generate-notification-icon.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +// Generates the Android notification (status-bar) small icon from the app's +// source logo. @capacitor/assets does NOT produce notification icons, so the +// build pipeline calls this after `@capacitor/assets generate` to keep the +// notification icon in sync with a custom platform logo (VITE_PLATFORM_LOGO). +// +// A notification small icon must be a monochrome silhouette: Android renders +// only its alpha channel and tints it. We take the logo's alpha channel as the +// shape and fill it white, with a little transparent padding so it isn't +// edge-to-edge in the status bar. +import sharp from "sharp" +import {mkdir} from "node:fs/promises" +import {dirname, join} from "node:path" + +const source = process.argv[2] || "assets/logo.png" +const resDir = process.argv[3] || "android/app/src/main/res" + +// density bucket -> small-icon size in px +const densities = {mdpi: 24, hdpi: 36, xhdpi: 48, xxhdpi: 72, xxxhdpi: 96} + +const generate = async (size, out) => { + const pad = Math.round(size * 0.14) + const inner = size - pad * 2 + + // The logo's alpha channel is the shape we want to render. + const alpha = await sharp(source) + .resize(inner, inner, {fit: "contain", background: {r: 0, g: 0, b: 0, alpha: 0}}) + .ensureAlpha() + .extractChannel(3) + .toColourspace("b-w") + .raw() + .toBuffer() + + await sharp({ + create: {width: inner, height: inner, channels: 3, background: {r: 255, g: 255, b: 255}}, + }) + .joinChannel(alpha, {raw: {width: inner, height: inner, channels: 1}}) + .extend({top: pad, bottom: pad, left: pad, right: pad, background: {r: 0, g: 0, b: 0, alpha: 0}}) + .png() + .toFile(out) + + console.log(`wrote ${out} (${size}px)`) +} + +for (const [density, size] of Object.entries(densities)) { + const out = join(resDir, `drawable-${density}`, "ic_stat_notify.png") + await mkdir(dirname(out), {recursive: true}) + await generate(size, out) +}