replace android default player icons with astra icons

This commit is contained in:
Boof2015
2026-08-11 20:04:02 -04:00
parent 6fb2da03b2
commit 8acbfef285
4 changed files with 122 additions and 2 deletions
@@ -15,6 +15,14 @@ import com.facebook.react.HeadlessJsTaskService
import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Arguments
import com.facebook.react.jstasks.HeadlessJsTaskConfig 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() { class AstraCarCommandService : HeadlessJsTaskService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// We're started via startForegroundService (so transport from the car works even when // 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) return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Astra") .setContentTitle("Astra")
.setContentText("Handling car controls") .setContentText("Handling car controls")
.setSmallIcon(android.R.drawable.ic_media_play) .setSmallIcon(notificationSmallIcon(this))
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true) .setOngoing(true)
.build() .build()
@@ -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_TOGGLE_FAVORITE = ACTION_PREFIX + "TOGGLE_FAVORITE"
private const val ACTION_STOP = ACTION_PREFIX + "STOP" private const val ACTION_STOP = ACTION_PREFIX + "STOP"
private const val MAX_ART_EDGE = 512 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 { class AstraDesktopRemoteSessionState : Record {
@Field @Field
@@ -240,7 +247,7 @@ object AstraDesktopRemoteSessionController {
} }
val largeIcon = decodeDataUrlBitmap(state.artworkDataUrl) val largeIcon = decodeDataUrlBitmap(state.artworkDataUrl)
val notification = NotificationCompat.Builder(context, CHANNEL_ID) 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") .setContentTitle(state.title?.ifBlank { "Unknown track" } ?: "Unknown track")
.setContentText(state.artist?.ifBlank { state.desktopName } ?: state.desktopName ?: "Astra Desktop") .setContentText(state.artist?.ifBlank { state.desktopName } ?: state.desktopName ?: "Astra Desktop")
.setSubText(state.desktopName ?: "Desktop Remote") .setSubText(state.desktopName ?: "Desktop Remote")
+69
View File
@@ -1,5 +1,8 @@
const fs = require('fs/promises');
const path = require('path');
const { const {
AndroidConfig, AndroidConfig,
withDangerousMod,
withAndroidManifest, withAndroidManifest,
withAppBuildGradle, withAppBuildGradle,
withProjectBuildGradle, withProjectBuildGradle,
@@ -10,6 +13,41 @@ const SETTINGS_MARKER = '// ASTRA VENDORED KOTLIN AUDIO';
const PROJECT_BUILD_MARKER = '// ASTRA KOTLIN AUDIO SUBSTITUTION'; const PROJECT_BUILD_MARKER = '// ASTRA KOTLIN AUDIO SUBSTITUTION';
const SIGNING_MARKER = '// ASTRA RELEASE SIGNING'; const SIGNING_MARKER = '// ASTRA RELEASE SIGNING';
const CAMERA_FEATURE = 'android.hardware.camera'; 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 = `<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<group
android:scaleX="1.726813"
android:scaleY="1.726813"
android:translateX="-660.505902"
android:translateY="-373.7">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M526.083,500.65C529.86,496.662 535.112,494.402 540.605,494.402C553.071,494.402 576.056,494.402 588.831,494.402C594.652,494.402 600.185,496.939 603.984,501.35C610.054,508.396 619.61,519.49 627.207,528.31C633.905,536.085 633.631,547.668 626.573,555.117C603.295,579.689 553.937,631.788 536.916,649.755C533.139,653.742 527.889,656 522.397,656L452,656C440.954,656 432,647.046 432,636C432,626.32 432,615.247 432,607.967C432,602.851 433.96,597.93 437.478,594.215C454.783,575.942 508.184,519.551 526.083,500.65Z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M580,389.237C580,378.578 588.641,369.937 599.3,369.937C625.097,369.937 669.782,369.937 688.899,369.937C694.682,369.937 700.183,372.436 703.987,376.792C736.676,414.222 893.163,593.401 921.571,625.929C924.427,629.198 926,633.392 926,637.733C926,637.733 926,637.734 926,637.734C926,648.379 917.371,657.008 906.726,657.008L817.1,657.008C811.318,657.008 805.817,654.51 802.013,650.155C769.332,612.742 612.909,433.673 584.448,401.092C581.58,397.809 580,393.598 580,389.239C580,389.238 580,389.237 580,389.237Z" />
</group>
</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 = `<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="exo_notification_small_icon" type="drawable">@drawable/astra_notification_icon</item>
</resources>
`;
const SETTINGS_BLOCK = ` const SETTINGS_BLOCK = `
@@ -158,9 +196,35 @@ function ensureOptionalCameraFeature(androidManifest) {
manifest['uses-feature'] = features; 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) { function withAstraAndroidRelease(config) {
config = withVendoredKotlinAudio(config); config = withVendoredKotlinAudio(config);
config = withReleaseSigning(config); config = withReleaseSigning(config);
config = withAstraNotificationIcon(config);
return withProfileableRelease(config); return withProfileableRelease(config);
} }
@@ -169,7 +233,12 @@ module.exports._internal = {
PROJECT_BUILD_MARKER, PROJECT_BUILD_MARKER,
SETTINGS_MARKER, SETTINGS_MARKER,
SIGNING_MARKER, SIGNING_MARKER,
NOTIFICATION_ICON_FILE,
NOTIFICATION_ICON_VECTOR,
NOTIFICATION_OVERRIDE_FILE,
NOTIFICATION_ICON_OVERRIDE,
addReleaseSigning, addReleaseSigning,
appendBlock, appendBlock,
ensureOptionalCameraFeature, ensureOptionalCameraFeature,
writeNotificationIconResources,
}; };
+36
View File
@@ -1,5 +1,8 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createRequire } from 'node:module'; 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'; import test from 'node:test';
const require = createRequire(import.meta.url); 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);
});