android: the icon where it moves — the glyph, the lock screen and a splash that is not white
Both notification glyphs are the same falcon now, the logo kit's monochrome path verbatim inside a group that re-frames it for a 24dp box — the playback card's and the transfers notice's, because a note beside an arrow does not read as one app, and the transfers one keeps the tray bar it always had so the falcon dives into it. The fill stays white on transparent: the shade composites a small icon from its alpha channel alone and tints it itself, so a coloured export would arrive as a white blob. The lock screen falls back to the app's own icon instead of a grey hole. `MediaHub.art` is null from the moment a track changes until the frontend has finished decoding its cover, and null forever for a track with none, so `appIcon()` decodes the launcher icon ONCE — through the PackageManager, because a library module's R has no `ic_launcher` — and hands it to both the card and `METADATA_KEY_ALBUM_ART`. The app manifest lives in gitignored `gen/` and declares no category, so the media-session plugin's own library manifest contributes `android:appCategory="audio"` through the merger, and a `Theme.App.Starting` on the brand ground replaces Android 12+'s white flash — in `values-v31` and `values-night-v31` both, because the qualifier order puts night mode ahead of platform version and a splash written only into `values-v31` is silently ignored on a dark-mode phone. The walk gains `media-card`: after the session block, the card is still posted on the `playback` channel with the playing track's title on it, and `media-keys` below it still gets the transport back — which is what a drawable the shade cannot inflate would actually break. The glyph itself and the splash are screenshots; `dumpsys notification` prints a resource id and never a pixel, and the splash is gone before the walk's first frame.
This commit is contained in:
1 parent
4ebc38d5f3
commit
d42645d2a0
8 files changed
+270
-17
No files matched your search
+67
@@ -786,6 +786,68 @@ and headphones coming out pause. The app's capability grants
|
|||||||
permission resolves everywhere, and on the desktop its commands answer
|
permission resolves everywhere, and on the desktop its commands answer
|
||||||
"not here" (the desktop has `smtc.rs`).
|
"not here" (the desktop has `smtc.rs`).
|
||||||
|
|
||||||
|
The glyph in the status bar is the falcon, not a note.
|
||||||
|
`ic_soulstream_note.xml` is the logo kit's monochrome path verbatim
|
||||||
|
(`branding/soulstream-logo-kit/android/adaptive/ic_launcher_monochrome.svg`),
|
||||||
|
wrapped in a `<group>` that re-frames it for a 24dp box — an Android path
|
||||||
|
element has no `transform` attribute, so the SVG's own
|
||||||
|
`translate/scale/translate` chain is folded into the group's numbers and the
|
||||||
|
falcon lands x 4.91→19.09, y 2→22. The fill stays `#FFFFFFFF` on a
|
||||||
|
transparent viewport because Android composites a notification's small icon
|
||||||
|
from its ALPHA CHANNEL and tints it itself: a coloured export renders as a
|
||||||
|
white blob. Nothing in the walk can tell those two apart — `dumpsys
|
||||||
|
notification` prints the icon's RESOURCE ID and never a pixel — so the glyph
|
||||||
|
is a screenshot, and the only thing the walk can prove is the thing that
|
||||||
|
actually breaks. `media-card` proves it: after `resumed`, the card is still
|
||||||
|
posted, on the `playback` channel, carrying the playing track's own title,
|
||||||
|
and `media-keys` a line below still gets the transport back.
|
||||||
|
|
||||||
|
The card's large icon falls back to the app's own icon. `MediaHub.art` is
|
||||||
|
cleared the moment `artKey` changes and only refilled once the frontend has
|
||||||
|
decoded a cover into bytes, and a track with no cover never refills it at
|
||||||
|
all — so without a fallback every track change has a window where the lock
|
||||||
|
screen shows a grey hole. `MediaHub.appIcon()` decodes the icon ONCE and
|
||||||
|
keeps it (`refresh()` runs on every position tick, so a per-update decode
|
||||||
|
would be a real cost), through the PackageManager rather than a resource id
|
||||||
|
because a library module's own R has no `ic_launcher`. It is drawn at
|
||||||
|
`setBounds(0, 0, 512, 512)`, and that is less obvious than it looks: an
|
||||||
|
`AdaptiveIconDrawable`'s bounds are the **72** units the launcher shows, not
|
||||||
|
the 108 its layers are drawn in — it scales its own layers up by 3/2 and
|
||||||
|
masks them inside whatever box it is handed, which is why its intrinsic width
|
||||||
|
is two thirds of the layer PNG. So the plain call already IS the launcher's
|
||||||
|
view, and a box set outside the tile to "reveal the 18% bleed" does the
|
||||||
|
opposite: it crops into the safe zone. The kit's foreground inks y 20.5→86.5
|
||||||
|
of 108, so a box 128 units oversize on each side would have shown 30→78 —
|
||||||
|
the head and the tail cut off. The same bitmap serves
|
||||||
|
`METADATA_KEY_ALBUM_ART`.
|
||||||
|
|
||||||
|
`android:appCategory="audio"` is contributed by the media-session plugin's
|
||||||
|
own library manifest. The app manifest lives in gitignored `gen/` and cannot
|
||||||
|
be edited from the repo, and it declares no category — so the merger takes
|
||||||
|
the library's, which is the one route there is.
|
||||||
|
|
||||||
|
Android 12+ draws a splash whether we ask or not, and the platform's own is a
|
||||||
|
white flash; `Theme.App.Starting` puts the brand ground (`#05080B`, the kit's
|
||||||
|
`night`) and `@mipmap/ic_launcher` there instead, and takes the window
|
||||||
|
background with it so the frame before the WebView's first paint is not white
|
||||||
|
either. It is delivered as a tracked res overlay, `src-tauri/android/res/`,
|
||||||
|
laid over the generated project by `build.sh` and `dev.sh` — the same move
|
||||||
|
`build.sh` already makes for `proguard-soulstream.pro`, and for the same
|
||||||
|
reason: `gen/` is regenerated and gitignored, so nothing of ours survives in
|
||||||
|
it. The overlay carries BOTH `values-v31/themes.xml` and
|
||||||
|
`values-night-v31/themes.xml`, and that pair is not belt and braces: the
|
||||||
|
resource qualifier order puts NIGHT MODE ahead of PLATFORM VERSION, so on a
|
||||||
|
dark-mode Android 14 phone the generated `values-night/themes.xml` beats a
|
||||||
|
lone `values-v31/` and the splash is silently ignored — on this phone, most
|
||||||
|
likely always. The style IS `Theme.app` (its parent), not a theme nothing
|
||||||
|
references, because `androidx.core:core-splashscreen` is not a dependency of
|
||||||
|
the app module: the platform attributes are the whole mechanism, there is no
|
||||||
|
`postSplashScreenTheme`, and `Theme.app` already is the post-splash theme.
|
||||||
|
Both overlay files replace `Theme.app` outright on API 31+, so anything a
|
||||||
|
later round adds to the generated `values/themes.xml` has to be added to them
|
||||||
|
too. The splash is a screenshot as well — it is gone before the walk's first
|
||||||
|
frame and `dumpsys` has no word for it.
|
||||||
|
|
||||||
A trap: tauri-build regenerates `gen/android/tauri.settings.gradle` and
|
A trap: tauri-build regenerates `gen/android/tauri.settings.gradle` and
|
||||||
`app/tauri.build.gradle.kts` — the lines that include every plugin's Android
|
`app/tauri.build.gradle.kts` — the lines that include every plugin's Android
|
||||||
module — ONLY when `TAURI_ANDROID_PROJECT_PATH` names the project, which the
|
module — ONLY when `TAURI_ANDROID_PROJECT_PATH` names the project, which the
|
||||||
@@ -1011,6 +1073,11 @@ callback returns, so `onTimeout` takes the notice down rather than being
|
|||||||
taken down with it; the fetches are the Rust core's own and Transfers still
|
taken down with it; the fetches are the Rust core's own and Transfers still
|
||||||
lists them.
|
lists them.
|
||||||
|
|
||||||
|
The notice's glyph is the same falcon as the playback card's, diving into the
|
||||||
|
same tray bar it always had, so the two notices read as one app — a second
|
||||||
|
copy of the path on purpose, because `R.drawable.ic_soulstream_note` belongs
|
||||||
|
to the media-session module's R class and is not visible from here.
|
||||||
|
|
||||||
The buttons come back as the plugin event `action`
|
The buttons come back as the plugin event `action`
|
||||||
(`addPluginListener("transfer-notice", "action", …)`). **Stop all** runs
|
(`addPluginListener("transfer-notice", "action", …)`). **Stop all** runs
|
||||||
`cancelAll()` in `transfers.svelte.js`, which is the same `cancel(id)` a
|
`cancelAll()` in `transfers.svelte.js`, which is the same `cancel(id)` a
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
<!-- the card is a notification; asked for at runtime on Android 13+ -->
|
<!-- the card is a notification; asked for at runtime on Android 13+ -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application>
|
<!-- The app's own manifest lives in gitignored gen/ and declares no
|
||||||
|
android:appCategory, so the merger takes this one's: an application
|
||||||
|
attribute from a library merges up when the app does not set it.
|
||||||
|
API 26+, and minSdk here is 26. -->
|
||||||
|
<application android:appCategory="audio">
|
||||||
<service
|
<service
|
||||||
android:name="com.soulstream.mediasession.MediaService"
|
android:name="com.soulstream.mediasession.MediaService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|||||||
+45
-2
@@ -12,6 +12,7 @@ import android.content.IntentFilter
|
|||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioFocusRequest
|
import android.media.AudioFocusRequest
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
@@ -39,6 +40,8 @@ object MediaHub {
|
|||||||
@Volatile var art: Bitmap? = null
|
@Volatile var art: Bitmap? = null
|
||||||
@Volatile var artKey: String? = null
|
@Volatile var artKey: String? = null
|
||||||
@Volatile var service: MediaService? = null
|
@Volatile var service: MediaService? = null
|
||||||
|
/** the app's own icon, decoded once — see appIcon() */
|
||||||
|
@Volatile private var icon: Bitmap? = null
|
||||||
|
|
||||||
fun update(ctx: Context, np: NowPlayingArgs) {
|
fun update(ctx: Context, np: NowPlayingArgs) {
|
||||||
if (np.artKey != artKey) {
|
if (np.artKey != artKey) {
|
||||||
@@ -74,6 +77,46 @@ object MediaHub {
|
|||||||
if (running != null) running.stopSelf() else ctx.stopService(Intent(ctx, MediaService::class.java))
|
if (running != null) running.stopSelf() else ctx.stopService(Intent(ctx, MediaService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The app's own icon, decoded ONCE and kept, as the card's large icon
|
||||||
|
* when there is no cover yet. `art` is cleared the moment `artKey`
|
||||||
|
* changes and only refilled when the frontend has finished decoding a
|
||||||
|
* cover into bytes — and a track with no cover never refills it at all —
|
||||||
|
* so every track change has a real window with a null large icon and the
|
||||||
|
* lock screen shows a grey hole. This is what stands in it.
|
||||||
|
*
|
||||||
|
* Through the PackageManager rather than a resource id on purpose: this
|
||||||
|
* is a library module and its own R has no `ic_launcher`, so from in here
|
||||||
|
* the app's icon is only reachable this way.
|
||||||
|
*
|
||||||
|
* Drawn at the tile's own bounds, which is the whole trick. On API 26+
|
||||||
|
* what comes back is an AdaptiveIconDrawable, and an adaptive icon's
|
||||||
|
* BOUNDS are the 72 units the launcher shows — not the 108 its layers
|
||||||
|
* are drawn in. The drawable scales its own layers up by 3/2 and masks
|
||||||
|
* them inside whatever box it is handed, which is why its intrinsic
|
||||||
|
* width is two thirds of the layer PNG. So setBounds(0, 0, 512, 512) IS
|
||||||
|
* the launcher's view; a box set outside the tile to "reveal the 18%
|
||||||
|
* bleed" does the opposite and crops into the safe zone. Every other
|
||||||
|
* kind of drawable simply fills the tile.
|
||||||
|
*
|
||||||
|
* Decoded on the first ask and never again — `refresh()` runs on every
|
||||||
|
* position tick, and this must not. A failure caches nothing and answers
|
||||||
|
* null, which is the card exactly as it was before this existed. */
|
||||||
|
fun appIcon(ctx: Context): Bitmap? {
|
||||||
|
val cached = icon
|
||||||
|
if (cached != null) return cached
|
||||||
|
return try {
|
||||||
|
val drawable = ctx.packageManager.getApplicationIcon(ctx.packageName)
|
||||||
|
val bitmap = Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888)
|
||||||
|
drawable.setBounds(0, 0, 512, 512)
|
||||||
|
drawable.draw(Canvas(bitmap))
|
||||||
|
icon = bitmap
|
||||||
|
bitmap
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.w("SoulStream", "app icon could not be decoded: $e")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun act(action: String, position: Double = -1.0) {
|
fun act(action: String, position: Double = -1.0) {
|
||||||
onAction?.invoke(action, position)
|
onAction?.invoke(action, position)
|
||||||
}
|
}
|
||||||
@@ -202,7 +245,7 @@ class MediaService : Service() {
|
|||||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, np.artist ?: "")
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, np.artist ?: "")
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, np.album ?: "")
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, np.album ?: "")
|
||||||
np.duration?.let { meta.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, (it * 1000).toLong()) }
|
np.duration?.let { meta.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, (it * 1000).toLong()) }
|
||||||
MediaHub.art?.let {
|
(MediaHub.art ?: MediaHub.appIcon(this))?.let {
|
||||||
meta.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, it)
|
meta.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, it)
|
||||||
meta.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, it)
|
meta.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, it)
|
||||||
}
|
}
|
||||||
@@ -258,7 +301,7 @@ class MediaService : Service() {
|
|||||||
.setContentTitle(np.title ?: "SoulStream")
|
.setContentTitle(np.title ?: "SoulStream")
|
||||||
.setContentText(np.artist ?: "")
|
.setContentText(np.artist ?: "")
|
||||||
.setSubText(np.album)
|
.setSubText(np.album)
|
||||||
.setLargeIcon(MediaHub.art)
|
.setLargeIcon(MediaHub.art ?: MediaHub.appIcon(this))
|
||||||
.setContentIntent(open)
|
.setContentIntent(open)
|
||||||
.setDeleteIntent(stop)
|
.setDeleteIntent(stop)
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
|||||||
+24
-4
@@ -1,11 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- the status-bar glyph: a note, white on transparent, as the shade wants -->
|
<!-- the status-bar glyph: the falcon, white on transparent, as the shade
|
||||||
|
wants — Android composites a notification's small icon from its ALPHA
|
||||||
|
CHANNEL and tints it itself, so a coloured export renders as a white blob
|
||||||
|
and only a screenshot can tell the difference.
|
||||||
|
|
||||||
|
The path is the logo kit's monochrome layer verbatim
|
||||||
|
(branding/soulstream-logo-kit/android/adaptive/ic_launcher_monochrome.svg),
|
||||||
|
drawn in its own large source space. An Android path element has no
|
||||||
|
transform attribute, so the SVG's
|
||||||
|
translate(9.814 9.814) scale(0.0863) translate(-40.740741 20.370370)
|
||||||
|
is folded into the group below and re-framed for a 24dp box: the falcon
|
||||||
|
lands x 4.91 to 19.09, y 2 to 22. It is a narrow mark (0.709:1), so the
|
||||||
|
side margins are generous and the top and bottom are not, which is what a
|
||||||
|
status bar wants. The transfers notice carries the same falcon. -->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<group
|
||||||
android:fillColor="#FFFFFFFF"
|
android:translateX="-1.6606"
|
||||||
android:pathData="M12,3v10.55A4,4 0,1 0,14 17V7h4V3H12zM10,19a2,2 0,1 1,0 -4a2,2 0,0 1,0 4z" />
|
android:translateY="-0.7287"
|
||||||
|
android:scaleX="0.026185"
|
||||||
|
android:scaleY="0.026185">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M 471.889 863.531 C 470.033 861.073 467.485 856.812 466.226 854.062 C 464.398 850.070 463.857 847.418 463.544 840.920 C 463.249 834.800 463.512 831.540 464.601 827.793 C 466.399 821.609 469.407 815.555 473.409 810.066 C 476.304 806.095 476.482 805.474 476.562 799.067 C 476.630 793.639 477.162 790.964 479.217 785.732 C 483.406 775.062 489.001 766.958 499.269 756.689 C 504.437 751.520 515.049 742.428 522.967 736.383 C 530.854 730.363 540.573 722.201 544.565 718.246 C 548.557 714.290 553.986 707.799 556.630 703.821 C 566.285 689.294 570.061 677.926 570.109 663.251 C 570.167 645.247 564.288 631.978 550.406 618.778 C 543.711 612.413 537.141 607.939 527.046 602.871 C 511.185 594.907 499.224 590.830 464.632 581.594 C 429.650 572.254 407.140 565.108 384.291 556.087 C 369.696 550.324 349.615 540.740 338.462 534.213 C 320.047 523.435 302.208 509.772 290.291 497.319 C 282.487 489.163 272.391 476.452 268.043 469.309 C 260.079 456.225 252.284 438.708 251.281 431.640 C 250.907 429.006 251.062 428.044 251.860 428.044 C 252.460 428.044 257.063 430.702 262.090 433.950 C 279.955 445.495 298.689 456.033 313.781 463.025 L 321.990 466.828 L 315.175 459.916 C 306.482 451.099 297.522 440.054 290.373 429.342 C 283.846 419.562 275.156 402.180 271.326 391.240 C 268.303 382.605 266.682 375.526 264.934 363.320 C 263.614 354.107 263.278 340.053 264.377 340.053 C 264.755 340.053 271.343 346.050 279.018 353.379 C 305.154 378.337 323.767 394.790 344.545 411.305 C 357.025 421.223 379.468 437.524 391.447 445.369 C 421.469 465.031 452.755 481.034 483.387 492.398 C 493.465 496.137 514.890 502.670 542.833 510.526 C 553.736 513.591 567.823 518.023 574.138 520.375 C 589.578 526.124 605.702 534.093 616.621 541.371 C 626.525 547.973 639.041 559.257 644.889 566.856 C 650.348 573.951 654.995 583.053 657.601 591.759 C 659.560 598.303 659.892 600.978 659.957 610.795 C 660.037 622.827 659.257 627.864 655.632 638.715 C 653.007 646.577 649.606 653.442 644.525 661.136 C 642.375 664.394 640.265 667.630 639.838 668.328 C 639.410 669.026 642.266 666.701 646.183 663.161 C 658.976 651.602 669.377 635.863 674.360 620.525 C 680.887 600.431 681.418 582.436 676.073 562.442 C 672.127 547.683 662.390 530.337 650.734 517.305 C 640.568 505.936 625.125 494.022 607.956 484.301 C 589.833 474.040 573.381 467.503 530.216 453.412 C 512.773 447.718 493.356 440.996 487.066 438.473 C 474.757 433.536 464.611 428.344 455.790 422.469 C 449.119 418.025 438.217 408.277 432.454 401.601 C 422.380 389.933 413.615 372.247 410.336 356.975 C 409.168 351.535 408.804 346.280 408.840 335.400 C 408.881 323.303 409.202 319.739 410.863 312.979 C 414.453 298.361 419.429 287.287 428.148 274.505 C 434.444 265.277 446.706 252.323 456.519 244.535 C 483.937 222.774 516.119 207.844 564.045 194.653 C 579.471 190.408 592.384 187.409 620.816 181.469 C 654.191 174.497 674.652 168.373 699.416 157.943 C 728.416 145.729 764.550 125.102 787.619 107.593 C 791.507 104.642 792.482 104.208 792.476 105.431 C 792.471 106.273 791.534 108.965 790.394 111.414 C 785.297 122.364 772.485 139.362 759.953 151.803 C 750.130 161.555 738.729 170.675 725.744 179.167 C 715.779 185.684 694.819 196.809 683.162 201.768 C 680.040 203.097 677.586 204.285 677.710 204.409 C 678.135 204.834 706.608 197.159 714.223 194.567 C 728.618 189.666 742.169 183.962 756.021 176.972 C 763.653 173.121 770.077 170.148 770.294 170.366 C 771.203 171.274 761.797 182.974 752.317 192.729 C 741.008 204.364 729.933 213.624 716.943 222.304 C 698.650 234.528 680.678 243.412 657.959 251.461 C 642.229 257.034 624.511 261.607 598.311 266.858 C 587.608 269.003 574.363 271.878 568.878 273.248 C 555.300 276.639 537.612 282.530 526.395 287.397 C 507.859 295.440 490.988 307.165 482.040 318.224 C 479.729 321.080 476.651 325.830 475.199 328.779 C 473.747 331.728 471.958 337.085 471.225 340.682 C 470.121 346.096 470.058 348.298 470.860 353.458 C 472.774 365.768 478.100 375.203 489.472 386.432 C 502.963 399.753 527.921 414.250 558.969 426.799 C 566.647 429.902 581.496 435.667 591.966 439.609 C 638.284 457.049 660.959 467.018 682.072 479.225 C 693.287 485.709 708.151 495.840 717.893 503.640 C 729.475 512.914 743.275 526.023 750.422 534.542 C 762.716 549.194 773.237 567.193 778.922 583.298 C 782.840 594.397 785.607 607.564 786.657 620.110 C 787.335 628.206 787.324 633.770 786.618 641.059 C 783.472 673.507 771.319 700.824 748.857 725.937 C 745.773 729.385 740.336 734.824 736.774 738.023 C 710.587 761.547 675.649 780.088 633.589 792.780 C 625.820 795.125 606.899 800.277 591.543 804.231 C 558.928 812.627 544.532 816.705 530.869 821.417 C 510.985 828.275 497.176 835.191 487.628 843.074 C 480.759 848.745 477.646 854.394 477.088 862.205 C 476.860 865.392 476.356 868.000 475.968 868.000 C 475.580 868.000 473.745 865.989 471.889 863.531 Z M 507.752 803.633 C 514.224 802.436 520.624 799.550 524.691 795.993 C 526.496 794.414 530.427 789.316 533.427 784.662 C 536.427 780.009 539.690 775.298 540.676 774.193 L 542.471 772.184 L 540.355 772.833 C 535.533Line truncated
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
+20
-6
@@ -1,15 +1,29 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- the status-bar glyph: an arrow into a tray, white on transparent, as
|
<!-- the status-bar glyph: the falcon diving into a tray, white on transparent,
|
||||||
the shade wants. Its own copy on purpose — R.drawable.ic_soulstream_note
|
as the shade wants. Its own copy of the path on purpose —
|
||||||
belongs to the media-session module's R class and is not visible here -->
|
R.drawable.ic_soulstream_note belongs to the media-session module's R
|
||||||
|
class and is not visible here — and deliberately the SAME falcon, so the
|
||||||
|
two notices read as one app.
|
||||||
|
|
||||||
|
Same kit source and same fold as the playback glyph
|
||||||
|
(branding/soulstream-logo-kit/android/adaptive/ic_launcher_monochrome.svg),
|
||||||
|
sized to clear the tray: the falcon lands x 7.04 to 16.96, y 2 to 16, and
|
||||||
|
the tray bar below it is unchanged at y 17.5 to 20. -->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<group
|
||||||
android:fillColor="#FFFFFFFF"
|
android:translateX="2.4375"
|
||||||
android:pathData="M11,3h2v7h3.5L12,15.5 7.5,10H11V3z" />
|
android:translateY="0.0899"
|
||||||
|
android:scaleX="0.018330"
|
||||||
|
android:scaleY="0.018330">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M 471.889 863.531 C 470.033 861.073 467.485 856.812 466.226 854.062 C 464.398 850.070 463.857 847.418 463.544 840.920 C 463.249 834.800 463.512 831.540 464.601 827.793 C 466.399 821.609 469.407 815.555 473.409 810.066 C 476.304 806.095 476.482 805.474 476.562 799.067 C 476.630 793.639 477.162 790.964 479.217 785.732 C 483.406 775.062 489.001 766.958 499.269 756.689 C 504.437 751.520 515.049 742.428 522.967 736.383 C 530.854 730.363 540.573 722.201 544.565 718.246 C 548.557 714.290 553.986 707.799 556.630 703.821 C 566.285 689.294 570.061 677.926 570.109 663.251 C 570.167 645.247 564.288 631.978 550.406 618.778 C 543.711 612.413 537.141 607.939 527.046 602.871 C 511.185 594.907 499.224 590.830 464.632 581.594 C 429.650 572.254 407.140 565.108 384.291 556.087 C 369.696 550.324 349.615 540.740 338.462 534.213 C 320.047 523.435 302.208 509.772 290.291 497.319 C 282.487 489.163 272.391 476.452 268.043 469.309 C 260.079 456.225 252.284 438.708 251.281 431.640 C 250.907 429.006 251.062 428.044 251.860 428.044 C 252.460 428.044 257.063 430.702 262.090 433.950 C 279.955 445.495 298.689 456.033 313.781 463.025 L 321.990 466.828 L 315.175 459.916 C 306.482 451.099 297.522 440.054 290.373 429.342 C 283.846 419.562 275.156 402.180 271.326 391.240 C 268.303 382.605 266.682 375.526 264.934 363.320 C 263.614 354.107 263.278 340.053 264.377 340.053 C 264.755 340.053 271.343 346.050 279.018 353.379 C 305.154 378.337 323.767 394.790 344.545 411.305 C 357.025 421.223 379.468 437.524 391.447 445.369 C 421.469 465.031 452.755 481.034 483.387 492.398 C 493.465 496.137 514.890 502.670 542.833 510.526 C 553.736 513.591 567.823 518.023 574.138 520.375 C 589.578 526.124 605.702 534.093 616.621 541.371 C 626.525 547.973 639.041 559.257 644.889 566.856 C 650.348 573.951 654.995 583.053 657.601 591.759 C 659.560 598.303 659.892 600.978 659.957 610.795 C 660.037 622.827 659.257 627.864 655.632 638.715 C 653.007 646.577 649.606 653.442 644.525 661.136 C 642.375 664.394 640.265 667.630 639.838 668.328 C 639.410 669.026 642.266 666.701 646.183 663.161 C 658.976 651.602 669.377 635.863 674.360 620.525 C 680.887 600.431 681.418 582.436 676.073 562.442 C 672.127 547.683 662.390 530.337 650.734 517.305 C 640.568 505.936 625.125 494.022 607.956 484.301 C 589.833 474.040 573.381 467.503 530.216 453.412 C 512.773 447.718 493.356 440.996 487.066 438.473 C 474.757 433.536 464.611 428.344 455.790 422.469 C 449.119 418.025 438.217 408.277 432.454 401.601 C 422.380 389.933 413.615 372.247 410.336 356.975 C 409.168 351.535 408.804 346.280 408.840 335.400 C 408.881 323.303 409.202 319.739 410.863 312.979 C 414.453 298.361 419.429 287.287 428.148 274.505 C 434.444 265.277 446.706 252.323 456.519 244.535 C 483.937 222.774 516.119 207.844 564.045 194.653 C 579.471 190.408 592.384 187.409 620.816 181.469 C 654.191 174.497 674.652 168.373 699.416 157.943 C 728.416 145.729 764.550 125.102 787.619 107.593 C 791.507 104.642 792.482 104.208 792.476 105.431 C 792.471 106.273 791.534 108.965 790.394 111.414 C 785.297 122.364 772.485 139.362 759.953 151.803 C 750.130 161.555 738.729 170.675 725.744 179.167 C 715.779 185.684 694.819 196.809 683.162 201.768 C 680.040 203.097 677.586 204.285 677.710 204.409 C 678.135 204.834 706.608 197.159 714.223 194.567 C 728.618 189.666 742.169 183.962 756.021 176.972 C 763.653 173.121 770.077 170.148 770.294 170.366 C 771.203 171.274 761.797 182.974 752.317 192.729 C 741.008 204.364 729.933 213.624 716.943 222.304 C 698.650 234.528 680.678 243.412 657.959 251.461 C 642.229 257.034 624.511 261.607 598.311 266.858 C 587.608 269.003 574.363 271.878 568.878 273.248 C 555.300 276.639 537.612 282.530 526.395 287.397 C 507.859 295.440 490.988 307.165 482.040 318.224 C 479.729 321.080 476.651 325.830 475.199 328.779 C 473.747 331.728 471.958 337.085 471.225 340.682 C 470.121 346.096 470.058 348.298 470.860 353.458 C 472.774 365.768 478.100 375.203 489.472 386.432 C 502.963 399.753 527.921 414.250 558.969 426.799 C 566.647 429.902 581.496 435.667 591.966 439.609 C 638.284 457.049 660.959 467.018 682.072 479.225 C 693.287 485.709 708.151 495.840 717.893 503.640 C 729.475 512.914 743.275 526.023 750.422 534.542 C 762.716 549.194 773.237 567.193 778.922 583.298 C 782.840 594.397 785.607 607.564 786.657 620.110 C 787.335 628.206 787.324 633.770 786.618 641.059 C 783.472 673.507 771.319 700.824 748.857 725.937 C 745.773 729.385 740.336 734.824 736.774 738.023 C 710.587 761.547 675.649 780.088 633.589 792.780 C 625.820 795.125 606.899 800.277 591.543 804.231 C 558.928 812.627 544.532 816.705 530.869 821.417 C 510.985 828.275 497.176 835.191 487.628 843.074 C 480.759 848.745 477.646 854.394 477.088 862.205 C 476.860 865.392 476.356 868.000 475.968 868.000 C 475.580 868.000 473.745 865.989 471.889 863.531 Z M 507.752 803.633 C 514.224 802.436 520.624 799.550 524.691 795.993 C 526.496 794.414 530.427 789.316 533.427 784.662 C 536.427 780.009 539.690 775.298 540.676 774.193 L 542.471 772.184 L 540.355 772.833 C 535.533Line truncated
|
||||||
|
</group>
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M5,17.5h14v2.5H5z" />
|
android:pathData="M5,17.5h14v2.5H5z" />
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- The night half of the API 31+ splash, and it is not a duplicate for
|
||||||
|
tidiness. The resource qualifier order puts NIGHT MODE ahead of PLATFORM
|
||||||
|
VERSION, so on a dark-mode Android 12+ phone the generated
|
||||||
|
values-night/themes.xml beats values-v31/themes.xml, Theme.app comes back
|
||||||
|
with nothing on it and the splash is silently ignored — which on this
|
||||||
|
phone means always. Same parent, so the same items.
|
||||||
|
|
||||||
|
ANYTHING ADDED TO THE GENERATED values/themes.xml MUST BE ADDED HERE TOO.
|
||||||
|
|
||||||
|
Theme.App.Starting itself is declared once, in values-v31/themes.xml: it
|
||||||
|
carries no night qualifier, so it matches a night device too. -->
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.app" parent="Theme.App.Starting" />
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- The Android 12+ splash, and a full replacement of Theme.app for API 31+.
|
||||||
|
|
||||||
|
The platform draws a splash whether we ask for one or not, and its own is
|
||||||
|
a white flash. These attributes put the brand ground and the app's icon
|
||||||
|
there instead. They have to sit on the theme the ACTIVITY resolves, which
|
||||||
|
is Theme.app (declared on the application, inherited by MainActivity), so
|
||||||
|
Theme.App.Starting is its PARENT rather than a style nothing references:
|
||||||
|
androidx.core:core-splashscreen is not a dependency of the app module, so
|
||||||
|
the platform attributes are the whole mechanism and there is no
|
||||||
|
postSplashScreenTheme to hand back to. Theme.app already is the
|
||||||
|
post-splash theme.
|
||||||
|
|
||||||
|
ANYTHING ADDED TO THE GENERATED values/themes.xml MUST BE ADDED HERE TOO.
|
||||||
|
This file replaces Theme.app outright on API 31+; it does not extend it.
|
||||||
|
|
||||||
|
Its night twin, values-night-v31/themes.xml, is not optional. The resource
|
||||||
|
qualifier order puts NIGHT MODE ahead of PLATFORM VERSION, so on a
|
||||||
|
dark-mode Android 12+ phone the generated values-night/themes.xml beats
|
||||||
|
this file and a splash written only here is silently ignored.
|
||||||
|
|
||||||
|
gen/ is regenerated and gitignored, so this lives under src-tauri/android/
|
||||||
|
res/ and build.sh and dev.sh lay it over the generated project. -->
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.App.Starting" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- the kit's `night`, source/brand-tokens.json. A literal rather than
|
||||||
|
a colour resource so it cannot collide with a name the
|
||||||
|
launcher-icon round chooses. -->
|
||||||
|
<item name="android:windowSplashScreenBackground">#FF05080B</item>
|
||||||
|
<!-- the app's own icon resource, whatever it is: this follows the
|
||||||
|
launcher icon rather than naming one of its layers, so it cannot
|
||||||
|
fail to resolve, and on API 26+ it is the adaptive icon, which is
|
||||||
|
what the splash slot is shaped for -->
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
|
||||||
|
<!-- and the frame between the splash dismissing and the WebView's
|
||||||
|
first paint, which is otherwise the same white. This is the one
|
||||||
|
item here that is not strictly "a splash": deleting it leaves the
|
||||||
|
rest of this file working. -->
|
||||||
|
<item name="android:windowBackground">#FF05080B</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.app" parent="Theme.App.Starting" />
|
||||||
|
</resources>
|
||||||
+51
-4
@@ -2439,7 +2439,7 @@ function shadeNotice() {
|
|||||||
const dump = adb("shell", "dumpsys", "notification", "--noredact").toString();
|
const dump = adb("shell", "dumpsys", "notification", "--noredact").toString();
|
||||||
const blocks = dump.split("NotificationRecord(").slice(1);
|
const blocks = dump.split("NotificationRecord(").slice(1);
|
||||||
const chans = [];
|
const chans = [];
|
||||||
let title = "", text = "";
|
let title = "", text = "", card = "";
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
const head = block.slice(0, 6000);
|
const head = block.slice(0, 6000);
|
||||||
if (!/pkg=com\.soulstream\.app\b/.test(head)) continue;
|
if (!/pkg=com\.soulstream\.app\b/.test(head)) continue;
|
||||||
@@ -2448,7 +2448,6 @@ function shadeNotice() {
|
|||||||
let ch = (head.match(/channel=([\w.\-]+)/) || [])[1] || "";
|
let ch = (head.match(/channel=([\w.\-]+)/) || [])[1] || "";
|
||||||
if (!ch || ch === "NotificationChannel") ch = (head.match(/mId='([^']*)'/) || [])[1] || ch;
|
if (!ch || ch === "NotificationChannel") ch = (head.match(/mId='([^']*)'/) || [])[1] || ch;
|
||||||
if (ch) chans.push(ch);
|
if (ch) chans.push(ch);
|
||||||
if (ch !== "downloads") continue;
|
|
||||||
/* the extras print as `<key>=<class> (<value>)`, and the class is
|
/* the extras print as `<key>=<class> (<value>)`, and the class is
|
||||||
whatever CharSequence the builder kept — String on this one,
|
whatever CharSequence the builder kept — String on this one,
|
||||||
but a Spannable on a device that styles it, so the class is
|
but a Spannable on a device that styles it, so the class is
|
||||||
@@ -2457,12 +2456,17 @@ function shadeNotice() {
|
|||||||
const m = head.match(new RegExp(key.replace(/\./g, "\\.") + "=[A-Za-z]+ \\(([^\\n]*?)\\)\\s*\\n"));
|
const m = head.match(new RegExp(key.replace(/\./g, "\\.") + "=[A-Za-z]+ \\(([^\\n]*?)\\)\\s*\\n"));
|
||||||
return m ? m[1].replace(/^"|"$/g, "") : "";
|
return m ? m[1].replace(/^"|"$/g, "") : "";
|
||||||
};
|
};
|
||||||
|
/* the media card's own title, for `media-card` in the phone
|
||||||
|
tail. This is why the `continue` below now sits under the
|
||||||
|
`extra` declaration rather than over it. */
|
||||||
|
if (ch === "playback") card = extra("android.title");
|
||||||
|
if (ch !== "downloads") continue;
|
||||||
title = extra("android.title");
|
title = extra("android.title");
|
||||||
text = extra("android.text");
|
text = extra("android.text");
|
||||||
}
|
}
|
||||||
return { shadeSeen: blocks.length, shadeChans: chans.join("|"), shadeTitle: title, shadeText: text };
|
return { shadeSeen: blocks.length, shadeChans: chans.join("|"), shadeTitle: title, shadeText: text, shadeCard: card };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { shadeSeen: 0, shadeChans: "ERR " + e.message, shadeTitle: "", shadeText: "" };
|
return { shadeSeen: 0, shadeChans: "ERR " + e.message, shadeTitle: "", shadeText: "", shadeCard: "" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2946,6 +2950,49 @@ async function main() {
|
|||||||
if (!sessOk) problems.push("EXPECT media-session " + JSON.stringify(sess));
|
if (!sessOk) problems.push("EXPECT media-session " + JSON.stringify(sess));
|
||||||
console.log((sessOk ? " ok " : " FAIL ") + "media-session".padEnd(16) + " " + JSON.stringify(sess));
|
console.log((sessOk ? " ok " : " FAIL ") + "media-session".padEnd(16) + " " + JSON.stringify(sess));
|
||||||
|
|
||||||
|
/* ── the card is still in the shade ───────────────────────
|
||||||
|
What a small icon the shade cannot inflate, or a large icon
|
||||||
|
that throws while the notification is built, actually does:
|
||||||
|
the card is never posted at all. So read it back out of the
|
||||||
|
same dump the downloads notice uses — our package, on the
|
||||||
|
`playback` channel, with the playing track's own title on it.
|
||||||
|
The title is taken from the player on BOTH sides of the dump
|
||||||
|
because a track may cross while it runs, and either answer is
|
||||||
|
a correct one; both sides come from the live app at the same
|
||||||
|
moment, so a real core answering faster than the mock cannot
|
||||||
|
make this disagree with itself.
|
||||||
|
|
||||||
|
It runs after `resumed` has asserted status === "playing", so
|
||||||
|
the ongoing card is settled state rather than a race, and it
|
||||||
|
sits above `media-keys`, which proves the transport still
|
||||||
|
answers through the session.
|
||||||
|
|
||||||
|
What this canNOT prove: the GLYPH. `dumpsys notification`
|
||||||
|
prints the icon's resource id and never a pixel, so a correct
|
||||||
|
falcon and a white rectangle read identically here. That one
|
||||||
|
is a screenshot. */
|
||||||
|
let cardBefore = await cdp.evaluate(PLAYER);
|
||||||
|
let cardNote = shadeNotice();
|
||||||
|
let cardAfter = await cdp.evaluate(PLAYER);
|
||||||
|
const cardAgrees = () => /playback/.test(cardNote.shadeChans) && !!cardNote.shadeCard
|
||||||
|
&& (cardNote.shadeCard === cardBefore?.title || cardNote.shadeCard === cardAfter?.title);
|
||||||
|
if (!cardAgrees()) {
|
||||||
|
/* the one race the two reads do not cover: `push()` in
|
||||||
|
media.svelte.js is debounced, so the card trails the
|
||||||
|
player by a moment, and a track crossing exactly here
|
||||||
|
shows the player two NEW titles and the shade the old
|
||||||
|
one. Let it settle and look once more. A card that is
|
||||||
|
not posted at all fails this the second time too. */
|
||||||
|
await sleep(2000);
|
||||||
|
cardBefore = await cdp.evaluate(PLAYER);
|
||||||
|
cardNote = shadeNotice();
|
||||||
|
cardAfter = await cdp.evaluate(PLAYER);
|
||||||
|
}
|
||||||
|
const cardOk = cardAgrees();
|
||||||
|
const cardSeen = { chans: cardNote.shadeChans, card: cardNote.shadeCard, title: cardAfter?.title ?? null };
|
||||||
|
if (!cardOk) problems.push("EXPECT media-card " + JSON.stringify(cardSeen));
|
||||||
|
console.log((cardOk ? " ok " : " FAIL ") + "media-card".padEnd(16) + " " + JSON.stringify(cardSeen));
|
||||||
|
|
||||||
adb("shell", "input", "keyevent", "KEYCODE_MEDIA_PAUSE");
|
adb("shell", "input", "keyevent", "KEYCODE_MEDIA_PAUSE");
|
||||||
await sleep(1500);
|
await sleep(1500);
|
||||||
const keyPaused = await cdp.evaluate(PLAYER);
|
const keyPaused = await cdp.evaluate(PLAYER);
|
||||||
|
|||||||
Reference in new issue
Block a user