Skip to content

chore: baseline main con los 4 PRs abiertos a upstream + build type personal - #27

Merged
PonceGL merged 26 commits into
mainfrom
chore/main-baseline
Aug 27, 2026
Merged

chore: baseline main con los 4 PRs abiertos a upstream + build type personal#27
PonceGL merged 26 commits into
mainfrom
chore/main-baseline

Conversation

@PonceGL

@PonceGL PonceGL commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Qué integra

Puebla main (hasta ahora igual a upstream/master) con todo el trabajo propio ya abierto como PR contra PixelPlayerHQ:master, más lo que es exclusivamente de este fork.

Conflicto resuelto

PlaylistDetailScreen.kt: el PR de búsqueda envuelve la fila de reproducción y la fila de acciones en if (searchQuery.isBlank()); el PR de Wear consolidó el espaciado de esas mismas filas en una variable sectionSpacing. Se combinaron ambos cambios conservando el comportamiento de las dos features.

Verificación

  • :app:assembleDebug, :wear:assembleDebug: BUILD SUCCESSFUL
  • :wear:testDebugUnitTest, :shared:testDebugUnitTest: en verde (rerun forzado)
  • :app:testDebugUnitTest: 5 fallos, confirmados idénticos en upstream/master limpio (worktree aislado) — preexistentes, no introducidos aquí
  • :wear:lintDebug: en verde
  • :app:lintDebug: 3722 errores, primer error confirmado idéntico en upstream/master — sin baseline configurado y sin paso de lint en ninguno de los 5 workflows de CI del proyecto. Deuda preexistente, no introducida aquí

Tags de respaldo antes de tocar nada: backup/dev-20260827, backup/dev-personal-20260827.

PonceGL added 26 commits July 21, 2026 19:00
Pulls the inline search field out of SongPickerBottomSheet into a
standalone SearchFilterTextField composable so it can be reused by
other song-list screens without duplicating the input styling.
Pure, testable predicate for filtering a song by title or artist,
using the same field scope as the existing global song search.
Will back the upcoming in-playlist search filter.
Adds a persistent SearchFilterTextField above the song list that
filters the currently displayed songs by title/artist, scoped to the
songs already loaded for this playlist (local or streaming-sourced).

Reorder mode is not yet guarded against an active filter; that lands
in the next commit.
Hides the Play/Shuffle/Add/Remove/Reorder action row while a search
filter is active, and force-exits reorder/remove mode as soon as the
user starts typing. This keeps drag-to-reorder (index-based) from
ever operating on a filtered view of the playlist.

Also adds a dedicated "no results for query" empty state, distinct
from the existing "playlist has no songs" state.
Encapsulates the displayedSongs filtering block from PlaylistDetailScreen
into a pure, list-level utility so it's unit-testable in isolation, on
top of the existing per-song matchesTitleOrArtist predicate.

No functional change to PlaylistDetailScreen; same filtering behavior,
now backed by 5 additional unit tests covering blank query, ordering,
no-match, empty-list, and mixed title/artist matches.
Exercises the real PlaylistDetailScreen composable end-to-end with
relaxed mocks for PlaylistViewModel/PlayerViewModel, covering:
- typing a query filters the visible songs
- a non-matching query shows the "no results" empty state
- clearing the query restores the full list and the actions row
- the actions row (Play it/Shuffle/Add/Remove/Reorder) is hidden while
  a search query is active
- reorder mode is force-disabled when search starts and stays disabled
  after clearing the query (not just hidden momentarily)
- tapping a filtered song plays the FULL unfiltered playlist starting
  from that song (the highest-risk behavior of this feature)
- an empty playlist never shows the search field

Swaps androidTestImplementation from io.mockk:mockk to
io.mockk:mockk-android, required for mocking concrete classes
(PlaylistViewModel/PlayerViewModel) on-device; plain mockk only ships
a JVM instrumentation agent that ART can't load.
Search filter in the playlist detail screen (PR PixelPlayerHQ#2641 against upstream/master).
Release-like build type (initWith release: minified, shrunk resources,
signed with the release keystore) for day-to-day development builds,
so they're representative of real performance instead of running
unoptimized debug builds.

applicationIdSuffix ".dev" + versionNameSuffix "-dev" + a distinct
app_name ("PixelPlayer [DEV]", same override pattern already used by
debug's "PixelPlayer [D]") let it install side by side with both the
official release build and debug, without applicationId collisions.

Not intended for upstream; this build type only lives on this fork.
Needed because wear shares applicationId with app and must be able to
coexist with/pair to the .dev phone build. matchingFallbacks falls
back to :shared's release variant, same mechanism app's benchmark
build type already relies on for the same dependency.
Personal (release-like) build type for both app and wear, fork-only,
not intended for an upstream PR.
app/src/debug/res only overrode app_name for de/fr/ko/nb/ru (+ the
default bucket). It missed ar/es/in/it/tr/zh-rCN, which is a problem
because those locales' main/values-<locale>/strings.xml files also
define app_name (untranslated, same 'PixelPlayer' value) — and a
locale-qualified resource always wins over a less-specific one
regardless of which source set (main vs debug) contributed it.

Net effect: on a device set to any of those 6 locales, a debug build
silently loses its '[D]' name and looks identical to the release
build. Verified with a real device set to es-MX before this fix
(showed plain 'PixelPlayer') and confirmed via aapt2 badging on the
rebuilt APK after.
fix(debug): add missing locale overrides for the [D] app name suffix
Same root cause fixed in debug (PR PixelPlayerHQ#2705 upstream): every locale whose
main/values-<locale>/strings.xml defines its own app_name wins over an
unqualified override from a build-type source set on a device set to
that locale. Adds app/src/personal/res/values-<locale>/strings.xml for
all 11 locales that override app_name, so 'PixelPlayer [DEV]' shows
reliably regardless of device language.

Verified via aapt2 dump badging on the rebuilt personal APK: every
locale bucket now resolves to 'PixelPlayer [DEV]'.

Fork-only, same as the rest of the personal build type.
matchesTitleOrArtist only ignored case, not accents, so searching
"que gan" inside a playlist never matched a song titled
"Qué ganas de...". Adds String.foldDiacritics() (NFD normalize +
strip combining marks) in Extensions.kt as a general-purpose reusable
utility, and uses it on both the query and the title/artist before
comparing.

Verified this is unrelated to the main library search: that path goes
through SQLite FTS4 (MusicDao.searchSongsMatch) with the unicode61
tokenizer, which already folds diacritics by default — confirmed with
a standalone sqlite3 repro (MATCH 'que* AND gan*' already finds
'Qué ganas de bailar' pre-existing, no code change needed there).
buildSongSearchMatchQuery/buildSongTitleSearchMatchQuery joined query
tokens with the literal " AND " keyword (e.g. "que* AND ganas*").
That keyword only behaves as a boolean operator on SQLite builds
compiled with SQLITE_ENABLE_FTS3_PARENTHESIS - not guaranteed on every
Android device. Without it, "AND" is parsed as an ordinary search
term, so a multi-word query would only match rows that literally
contained the word "and", silently breaking multi-word search
entirely on affected devices.

Confirmed on a real device (Galaxy S25 Ultra, SQLite 3.44.5, no
FTS3_PARENTHESIS support): searching a two-word query against a title
that doesn't contain "and" returned zero results, regardless of case
or accents, while the exact same content was trivially findable via a
single-token query or via the always-available implicit-AND syntax
(space-separated terms, no keyword).

Fix: join tokens with a plain space instead. Implicit AND is base FTS3/4
syntax, supported unconditionally on every SQLite build. Also extracts
the shared tokenization logic into one helper (buildFtsMatchQuery) to
remove the duplication between the two query builders.

Adds MusicDaoQueryBuilderTest (unit, verifies the query string shape)
and a MusicDaoTest regression case that runs the real query against a
real FTS4 table - which is what actually caught this, since a plain
string-comparison test wouldn't exercise the SQLite engine at all.
…, reliability hardening

Adds a full Wear OS companion experience for offline listening: transfer
playlists from the phone to the watch (transcode, chunked transfer, resumable
across process death), play them locally on the watch without the phone
nearby, and control the phone remotely when it's in range — plus several
rounds of reliability and performance hardening driven by real hardware
testing (audio offload with HAL-reset fallback, mid-song stall recovery,
playlist-sync and transfer-outcome acks so the phone never assumes success
without watch confirmation, and user-facing performance toggles — album art,
dynamic color, play-button animation — for constrained watch hardware).

This is a squashed extraction of ~55 commits (PR8, PR24, PR25, PR26) built up
across a longer development history on a personal fork, isolated onto master
here as a single self-contained changeset for review. Excludes everything
from that fork unrelated to this feature (a playlist search filter, an FTS
search fix, personal app branding) — none of it touched.

Known gap carried over from the fork: FAVORITES_SYNC_REQUEST/STATE are
declared but not fully wired (no manifest registration, no phone-side
handler) — pre-existing, unrelated to this feature, left as-is rather than
silently finishing it inside an unrelated PR.
Nueve defectos encontrados al revisar upstream-proposal/wear-companion-app
contra master, todos verificados contra el código antes de corregirse.

Altos:
- Una transferencia guardada en biblioteca podía quedarse para siempre en
  STATUS_AWAITING_WATCH_ACK si el reporte del reloj nunca llegaba, dejando
  clavada la notificación en primer plano y el indicador de "enviando". El
  timeout vive en PhoneWatchTransferStateStore para cubrir también el camino
  de canción suelta, que no tiene ningún await propio del que colgar uno.
- El interruptor de tema dinámico no hacía nada mientras la carátula
  estuviera activada: el tema caía a derivar la paleta del bitmap, por la
  ruta más cara que el propio interruptor existe para evitar.

Medios:
- Un batch reanudado tras la muerte del proceso perdía su intent para
  siempre si el reloj no estaba al alcance en ese arranque. Ahora sobrevive
  y caduca a los 7 días, arrastrando la marca de tiempo original.
- Con bitrate desconocido (el caso normal: sólo el escaneo profundo lo
  captura) se re-codificaba toda la biblioteca a AAC 128k. Ahora se sondea
  el archivo antes de decidir.
- Un stream cerrado por el watchdog podía confirmar como completo un
  archivo truncado, que quedaba en Room como reproducible y el teléfono no
  volvía a intentar.
- ERROR_ALREADY_ON_WATCH se contaba como fallo: se re-transcodificaba y se
  reofrecía la misma canción para reportarla luego como fallida.
- Un Transformer atascado colgaba el batch indefinidamente.

Bajos:
- Un batch cancelado antes de arrancar se reportaba como "Completado".
- Al restaurar la reproducción, la posición guardada se aplicaba a otra
  canción si la original había sido borrada del reloj.
Reportado en dispositivo: con "mostrar carátula", "color dinámico" y
"animación del botón" desactivados en el teléfono, la reproducción local
del reloj seguía mostrando las tres cosas.

Los gates estaban bien —los verifiqué de punta a punta: el repositorio
local, el ViewModel y PlayerScreen aplican los tres flags correctamente.
Lo que fallaba era que el reloj nunca se enteraba del valor:

- El reloj sólo aprendía los ajustes por push (onDataChanged), y un
  DataItem con contenido idéntico no genera evento. Reinstalar la app del
  reloj —cada build de depuración— borra su copia local y la deja en los
  valores por defecto (todo activado), y el reanuncio del teléfono al
  abrir la pantalla de ajustes es byte a byte el mismo DataItem, así que
  se descarta y el reloj se queda desincronizado para siempre. Ahora el
  reloj lee el DataItem publicado al arrancar el proceso.
- Además, la escritura del ajuste corría en el scope del
  WearableListenerService, que onDestroy cancela en cuanto el callback
  retorna: la escritura en DataStore competía con ese teardown y podía
  perderse. Al ser un evento único, perderla es permanente. Ahora se
  escribe en línea sobre el hilo del callback.
… en dispositivo

Del reporte de pruebas físicas:

- La carátula y los colores aparecían al iniciar la reproducción y se
  quitaban uno o dos segundos después. La lectura de los ajustes al
  arrancar tarda lo suyo (llamada a Play Services), y hasta entonces se
  actuaba con los valores cacheados. Ahora hay un estado "resuelto": los
  consumidores tratan "aún no lo sé" como desactivado, así que el trabajo
  caro se difiere en vez de hacerse y deshacerse. Con red de seguridad
  para que un fallo no deje la carátula suprimida para siempre.

- El teléfono nunca procesaba la respuesta de biblioteca del reloj: el
  manifiesto enruta /watch_library_state a WearCommandReceiver, pero su
  `when` no tenía rama y caía en "Unknown message path". La consecuencia
  es que el teléfono sólo sabía lo que él mismo había transferido y nunca
  se corregía —canciones borradas en el reloj, o el reloj reinstalado con
  su biblioteca en blanco, seguían contando como presentes—, y también
  que isWatchLibraryResolved nunca se ponía a true, así que el resume
  siempre agotaba sus 10 s de espera.

- "Actualizar en el reloj" salía para playlists nunca enviadas, porque se
  decidía con "alguna canción de la playlist está en el reloj". Ahora el
  reloj informa qué playlists tiene y la etiqueta usa esa señal.

- El chip de error del reloj no decía qué canción falló, y sobrevivía a
  un reintento exitoso. Ahora recupera el título y una transferencia
  completada descarta los errores previos de esa misma canción.
…dispositivo

Revisión de código de la rama contra master (9 hallazgos, todos con
escenario de fallo verificado) más lo que salió de la prueba física:

- Cuelgue permanente en STATUS_AWAITING_WATCH_ACK y notificación clavada.
- El interruptor de tema dinámico no hacía nada con la carátula activada.
- Intent de batch perdido al reanudar sin reloj al alcance; ahora caduca
  a los 7 días en vez de descartarse.
- Todo se re-codificaba a AAC 128k por bitrate desconocido; ahora se
  sondea el archivo.
- Un archivo truncado podía confirmarse como completo.
- ERROR_ALREADY_ON_WATCH se contaba como fallo y disparaba un reintento.
- Un Transformer atascado colgaba el batch indefinidamente.
- Batch cancelado antes de arrancar reportado como "Completado".
- Posición restaurada sobre la canción equivocada.

Y del dispositivo:

- Los tres ajustes de rendimiento nunca llegaban al reloj (sólo push, que
  el Data Layer descarta si el DataItem no cambia, más una escritura que
  competía con el teardown del servicio).
- El teléfono no procesaba /watch_library_state, así que nunca conocía la
  biblioteca real del reloj.
- "Actualizar en el reloj" para playlists nunca enviadas.
- Carátula y colores aplicándose y quitándose al iniciar la reproducción.
- Chip de error sin título de canción y persistente tras un reintento
  exitoso.
…-app' into chore/main-baseline

# Conflicts:
#	app/src/main/java/com/theveloper/pixelplay/presentation/screens/PlaylistDetailScreen.kt
@PonceGL
PonceGL merged commit 95010f6 into main Aug 27, 2026
@PonceGL
PonceGL deleted the chore/main-baseline branch August 27, 2026 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant