fix: make cash drawer failures visible and service binding recoverable - #34
Merged
Merged
Conversation
FrenkyDema
force-pushed
the
fix/cash-drawer-and-service-binding
branch
from
September 9, 2026 15:54
524baf9 to
0dd4e06
Compare
The example app could not start on Android 11+ devices: BIND_SERVICE never
resolved, so `await bindingService()` before `runApp()` hung forever.
Two causes, verified on a Sunmi D3 (Android 14, targetSdk 36):
1. Package visibility. Without a <queries> entry the Sunmi service is
invisible to the app, so bindService() returns false:
AppsFilter: ...example -> woyou.aidlservice.jiuiv5 BLOCKED
Unable to start service Intent { act=...IWoyouService } U=0: not found
The declaration now lives in the plugin manifest, so host apps inherit it.
2. The pending Result was only completed from onServiceConnected, which
never fires when the bind is refused. Every exit path now settles it.
The same dead-binding state is what a stuck cash drawer looks like, and the
plugin hid it: openDrawer() reported success as soon as the AIDL call was
dispatched while discarding the firmware's callback, and drawerStatus() /
drawerTimesOpen() returned false / 0 when the service was unreachable,
indistinguishable from a drawer that is simply not attached.
BREAKING CHANGE: openDrawer(), drawerStatus() and drawerTimesOpen() now throw
a PlatformException instead of silently reporting success or absent hardware.
Also reconnects after onBindingDied, releases a bind that never completed,
cleans up on engine detach, and holds in-flight AIDL callbacks with a strong
reference so they are not collected before the printer replies.
FrenkyDema
force-pushed
the
fix/cash-drawer-and-service-binding
branch
from
September 9, 2026 15:56
0dd4e06 to
e01d454
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Il problema
L'app di esempio non si avviava sui dispositivi Android 11+:
BIND_SERVICEnon si risolveva mai, quindiawait bindingService()prima dirunApp()restava appeso all'infinito e la UI non compariva.Diagnosticato su un Sunmi D3 (Android 14, targetSdk 36). Due cause in cascata:
1. Package visibility. Senza una dichiarazione
<queries>il servizio Sunmi è invisibile all'app, quindibindService()ritornafalse:Il servizio è installato e funzionante sul device — semplicemente l'app non poteva vederlo. La dichiarazione ora sta nel manifest del plugin, così le app host la ereditano dal manifest merger senza doversene ricordare.
2. Il
Resultpendente non veniva mai completato. Era risolto solo daonServiceConnected, che non viene mai invocato se il bind è rifiutato. Ora ogni via d'uscita lo completa: ritornofalsedibindService(),SecurityException,onNullBinding,onBindingDied.Perché tocca il cassetto
Lo stesso stato di binding morto è esattamente ciò che si osserva quando il cassetto non si apre, e il plugin lo mascherava:
openDrawer()risolvevasuccess(true)appena la chiamata AIDL era stata dispatchata, passando un callback dummy che scartavaonRunResulteonRaiseException. Un kick rifiutato dal firmware veniva riportato come riuscito.drawerStatus()edrawerTimesOpen()ritornavanofalse/0quando il servizio era irraggiungibile — indistinguibile da un cassetto semplicemente non collegato, il che sposta la diagnosi sull'hardware invece che sul binding.Modifiche
openDrawer()si completa solo quando il firmware conferma il kick; in caso contrario lanciaPlatformExceptioncon codiceOPERATION_FAILED,PRINTER_EXCEPTION,UNAVAILABLEoTIMEOUT.drawerStatus()edrawerTimesOpen()lancianoPlatformExceptioncon codiceUNAVAILABLEinvece di riportare hardware assente.<queries>perwoyou.aidlservice.jiuiv5al manifest del plugin.BIND_SERVICEsi risolve sempre, su ogni percorso.onBindingDied, e i comandi che trovano il servizio mancante schedulano un reconnect. Prima un riavvio del servizio Sunmi lasciava il plugin rotto in modo permanente fino al riavvio dell'app host.unbindService()rilascia anche un bind richiesto ma mai completato (era un leak), e il plugin fa cleanup sul detach dell'engine.runApp()sul binding.Verifica
Sul D3 (
D402P5CCJ1651), dopo il fix:flutter test11/11 verdi, inclusi due nuovi test sui percorsi di errore del cassetto.flutter analyzepulito a parte ununnecessary_library_namepreesistente inlib/enums.dart.Note
getPrinterStatus()ritornaPrinterStatus.ERRORquando il servizio è irraggiungibile, senza distinguerlo da un errore reale della stampante.