From 8acbfef285eb468630e45c259a6151951615e97c Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:04:02 -0400 Subject: [PATCH] replace android default player icons with astra icons --- .../astracar/AstraCarCommandService.kt | 10 ++- .../AstraDesktopRemoteSessionModule.kt | 9 ++- plugins/withAstraAndroidRelease.js | 69 +++++++++++++++++++ plugins/withAstraAndroidRelease.test.mjs | 36 ++++++++++ 4 files changed, 122 insertions(+), 2 deletions(-) diff --git a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarCommandService.kt b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarCommandService.kt index c1a01ae..9b23b1b 100644 --- a/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarCommandService.kt +++ b/modules/astra-car/android/src/main/java/expo/modules/astracar/AstraCarCommandService.kt @@ -15,6 +15,14 @@ import com.facebook.react.HeadlessJsTaskService import com.facebook.react.bridge.Arguments import com.facebook.react.jstasks.HeadlessJsTaskConfig +private const val NOTIFICATION_ICON_RESOURCE = "astra_notification_icon" + +private fun notificationSmallIcon(context: Context): Int = + context.resources + .getIdentifier(NOTIFICATION_ICON_RESOURCE, "drawable", context.packageName) + .takeIf { it != 0 } + ?: android.R.drawable.ic_media_play + class AstraCarCommandService : HeadlessJsTaskService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // We're started via startForegroundService (so transport from the car works even when @@ -72,7 +80,7 @@ class AstraCarCommandService : HeadlessJsTaskService() { return NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Astra") .setContentText("Handling car controls") - .setSmallIcon(android.R.drawable.ic_media_play) + .setSmallIcon(notificationSmallIcon(this)) .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) .build() diff --git a/modules/astra-desktop-remote-session/android/src/main/java/expo/modules/astradesktopremotesession/AstraDesktopRemoteSessionModule.kt b/modules/astra-desktop-remote-session/android/src/main/java/expo/modules/astradesktopremotesession/AstraDesktopRemoteSessionModule.kt index 049f8c5..db5f8ed 100644 --- a/modules/astra-desktop-remote-session/android/src/main/java/expo/modules/astradesktopremotesession/AstraDesktopRemoteSessionModule.kt +++ b/modules/astra-desktop-remote-session/android/src/main/java/expo/modules/astradesktopremotesession/AstraDesktopRemoteSessionModule.kt @@ -35,6 +35,13 @@ private const val ACTION_NEXT = ACTION_PREFIX + "NEXT" private const val ACTION_TOGGLE_FAVORITE = ACTION_PREFIX + "TOGGLE_FAVORITE" private const val ACTION_STOP = ACTION_PREFIX + "STOP" private const val MAX_ART_EDGE = 512 +private const val NOTIFICATION_ICON_RESOURCE = "astra_notification_icon" + +private fun notificationSmallIcon(context: Context): Int = + context.resources + .getIdentifier(NOTIFICATION_ICON_RESOURCE, "drawable", context.packageName) + .takeIf { it != 0 } + ?: android.R.drawable.ic_media_play class AstraDesktopRemoteSessionState : Record { @Field @@ -240,7 +247,7 @@ object AstraDesktopRemoteSessionController { } val largeIcon = decodeDataUrlBitmap(state.artworkDataUrl) val notification = NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(android.R.drawable.ic_media_play) + .setSmallIcon(notificationSmallIcon(context)) .setContentTitle(state.title?.ifBlank { "Unknown track" } ?: "Unknown track") .setContentText(state.artist?.ifBlank { state.desktopName } ?: state.desktopName ?: "Astra Desktop") .setSubText(state.desktopName ?: "Desktop Remote") diff --git a/plugins/withAstraAndroidRelease.js b/plugins/withAstraAndroidRelease.js index d0180dd..9a75d86 100644 --- a/plugins/withAstraAndroidRelease.js +++ b/plugins/withAstraAndroidRelease.js @@ -1,5 +1,8 @@ +const fs = require('fs/promises'); +const path = require('path'); const { AndroidConfig, + withDangerousMod, withAndroidManifest, withAppBuildGradle, withProjectBuildGradle, @@ -10,6 +13,41 @@ const SETTINGS_MARKER = '// ASTRA VENDORED KOTLIN AUDIO'; const PROJECT_BUILD_MARKER = '// ASTRA KOTLIN AUDIO SUBSTITUTION'; const SIGNING_MARKER = '// ASTRA RELEASE SIGNING'; const CAMERA_FEATURE = 'android.hardware.camera'; +const NOTIFICATION_ICON_FILE = 'astra_notification_icon.xml'; +const NOTIFICATION_OVERRIDE_FILE = 'astra_notification_icon_overrides.xml'; + +// A notification small icon is an alpha mask: Android supplies the color for +// the status bar, lock screen and media controls. Keep only the main Astra mark +// here—launcher backgrounds, shadows and brand colors do not belong in it. +const NOTIFICATION_ICON_VECTOR = ` + + + + + + +`; + +// RNTP 4.1.2 uses ExoPlayer's generic circular-play drawable whenever no JS +// icon resolves. An app-level resource with the same name overrides that +// default in release, debug and the temporary foreground-service notification. +const NOTIFICATION_ICON_OVERRIDE = ` + + @drawable/astra_notification_icon + +`; const SETTINGS_BLOCK = ` @@ -158,9 +196,35 @@ function ensureOptionalCameraFeature(androidManifest) { manifest['uses-feature'] = features; } +async function writeNotificationIconResources(platformProjectRoot) { + const resourceRoot = path.join(platformProjectRoot, 'app', 'src', 'main', 'res'); + const drawableDir = path.join(resourceRoot, 'drawable'); + const valuesDir = path.join(resourceRoot, 'values'); + + await Promise.all([ + fs.mkdir(drawableDir, { recursive: true }), + fs.mkdir(valuesDir, { recursive: true }), + ]); + await Promise.all([ + fs.writeFile(path.join(drawableDir, NOTIFICATION_ICON_FILE), NOTIFICATION_ICON_VECTOR), + fs.writeFile(path.join(valuesDir, NOTIFICATION_OVERRIDE_FILE), NOTIFICATION_ICON_OVERRIDE), + ]); +} + +function withAstraNotificationIcon(config) { + return withDangerousMod(config, [ + 'android', + async (mod) => { + await writeNotificationIconResources(mod.modRequest.platformProjectRoot); + return mod; + }, + ]); +} + function withAstraAndroidRelease(config) { config = withVendoredKotlinAudio(config); config = withReleaseSigning(config); + config = withAstraNotificationIcon(config); return withProfileableRelease(config); } @@ -169,7 +233,12 @@ module.exports._internal = { PROJECT_BUILD_MARKER, SETTINGS_MARKER, SIGNING_MARKER, + NOTIFICATION_ICON_FILE, + NOTIFICATION_ICON_VECTOR, + NOTIFICATION_OVERRIDE_FILE, + NOTIFICATION_ICON_OVERRIDE, addReleaseSigning, appendBlock, ensureOptionalCameraFeature, + writeNotificationIconResources, }; diff --git a/plugins/withAstraAndroidRelease.test.mjs b/plugins/withAstraAndroidRelease.test.mjs index b95fa1b..d6a61ae 100644 --- a/plugins/withAstraAndroidRelease.test.mjs +++ b/plugins/withAstraAndroidRelease.test.mjs @@ -1,5 +1,8 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; import test from 'node:test'; const require = createRequire(import.meta.url); @@ -65,3 +68,36 @@ test('marks QR scanning camera hardware as optional without duplicating it', () }, ]); }); + +test('writes deterministic Astra and RNTP notification icon resources', async (t) => { + const projectRoot = await mkdtemp(path.join(tmpdir(), 'astra-notification-icon-')); + t.after(() => rm(projectRoot, { recursive: true, force: true })); + + await _internal.writeNotificationIconResources(projectRoot); + const drawablePath = path.join( + projectRoot, + 'app/src/main/res/drawable', + _internal.NOTIFICATION_ICON_FILE + ); + const overridePath = path.join( + projectRoot, + 'app/src/main/res/values', + _internal.NOTIFICATION_OVERRIDE_FILE + ); + const firstDrawable = await readFile(drawablePath, 'utf8'); + const firstOverride = await readFile(overridePath, 'utf8'); + + assert.equal(firstDrawable, _internal.NOTIFICATION_ICON_VECTOR); + assert.match(firstDrawable, /android:width="24dp"/); + assert.match(firstDrawable, /android:fillColor="#FFFFFFFF"/); + assert.doesNotMatch(firstDrawable, /background|logoShadow/); + assert.equal(firstOverride, _internal.NOTIFICATION_ICON_OVERRIDE); + assert.match( + firstOverride, + /name="exo_notification_small_icon" type="drawable">@drawable\/astra_notification_icon/ + ); + + await _internal.writeNotificationIconResources(projectRoot); + assert.equal(await readFile(drawablePath, 'utf8'), firstDrawable); + assert.equal(await readFile(overridePath, 'utf8'), firstOverride); +});