1- // Query battery health from the hardware HAL: capacity, charge status, current.
1+ // Query battery health from the system battery properties service.
2+ //
3+ // Uses IBatteryPropertiesRegistrar via the "batteryproperties" service to query
4+ // battery properties via raw binder transact. Falls back to reading sysfs
5+ // if binder access is denied.
26//
37// Build:
48//
5- // GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o build/battery_health ./examples/battery_health/
9+ // GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o build/battery_health ./examples/battery_health/
610// adb push battery_health /data/local/tmp/ && adb shell /data/local/tmp/battery_health
711package main
812
913import (
1014 "context"
1115 "fmt"
1216 "os"
17+ "strings"
1318
1419 "github.com/xaionaro-go/binder/binder"
1520 "github.com/xaionaro-go/binder/binder/versionaware"
16- "github.com/xaionaro-go/binder/android/hardware/health"
1721 "github.com/xaionaro-go/binder/kernelbinder"
22+ "github.com/xaionaro-go/binder/parcel"
1823 "github.com/xaionaro-go/binder/servicemanager"
1924)
2025
26+ // Android BatteryManager property IDs (from android.os.BatteryManager).
27+ const (
28+ batteryPropertyChargeCounter = 1 // BATTERY_PROPERTY_CHARGE_COUNTER (µAh)
29+ batteryPropertyCurrentNow = 2 // BATTERY_PROPERTY_CURRENT_NOW (µA)
30+ batteryPropertyCurrentAvg = 3 // BATTERY_PROPERTY_CURRENT_AVERAGE (µA)
31+ batteryPropertyCapacity = 4 // BATTERY_PROPERTY_CAPACITY (%)
32+ batteryPropertyStatus = 6 // BATTERY_PROPERTY_STATUS
33+ )
34+
35+ const descriptorBatteryProps = "android.os.IBatteryPropertiesRegistrar"
36+
2137func main () {
2238 ctx := context .Background ()
2339
@@ -36,53 +52,159 @@ func main() {
3652
3753 sm := servicemanager .New (transport )
3854
39- svc , err := sm .GetService (ctx , servicemanager .ServiceName ("android.hardware.health.IHealth/default " ))
55+ svc , err := sm .GetService (ctx , servicemanager .ServiceName ("batteryproperties " ))
4056 if err != nil {
41- fmt .Fprintf (os .Stderr , "get health HAL: %v\n " , err )
42- fmt .Fprintf (os .Stderr , "(health HAL may not be available on this device, or access may be blocked by SELinux)\n " )
43- os .Exit (1 )
57+ fmt .Fprintf (os .Stderr , "get batteryproperties service: %v\n " , err )
58+ fmt .Fprintln (os .Stderr , "Falling back to sysfs..." )
59+ printSysfs ()
60+ os .Exit (0 )
61+ }
62+
63+ binderOK := false
64+
65+ // Query each property using raw binder transact to properly read
66+ // the BatteryProperty out-parameter.
67+ type propQuery struct {
68+ name string
69+ id int32
70+ unit string
71+ }
72+
73+ queries := []propQuery {
74+ {"Battery level" , batteryPropertyCapacity , "%" },
75+ {"Charge counter" , batteryPropertyChargeCounter , " uAh" },
76+ {"Current draw" , batteryPropertyCurrentNow , " uA" },
77+ {"Current average" , batteryPropertyCurrentAvg , " uA" },
78+ {"Battery status" , batteryPropertyStatus , "" },
4479 }
4580
46- h := health .NewHealthProxy (svc )
81+ for _ , q := range queries {
82+ val , status , err := getProperty (ctx , svc , q .id )
83+ if err != nil {
84+ fmt .Fprintf (os .Stderr , "GetProperty(%s): %v\n " , q .name , err )
85+ continue
86+ }
87+ if status != 0 {
88+ fmt .Fprintf (os .Stderr , "GetProperty(%s): status=%d\n " , q .name , status )
89+ continue
90+ }
91+ binderOK = true
92+ if q .id == batteryPropertyStatus {
93+ fmt .Printf (" %-20s %s (%d)\n " , q .name + ":" , statusToString (int32 (val )), val )
94+ } else {
95+ fmt .Printf (" %-20s %d%s\n " , q .name + ":" , val , q .unit )
96+ }
97+ }
98+
99+ if ! binderOK {
100+ fmt .Fprintln (os .Stderr , "\n Binder calls failed; falling back to sysfs..." )
101+ printSysfs ()
102+ }
103+ }
104+
105+ // getProperty performs a raw binder transact to IBatteryPropertiesRegistrar.getProperty,
106+ // properly reading both the BatteryProperty out-parameter and the status code.
107+ func getProperty (
108+ ctx context.Context ,
109+ remote binder.IBinder ,
110+ propertyID int32 ,
111+ ) (value int64 , status int32 , err error ) {
112+ data := parcel .New ()
113+ defer data .Recycle ()
114+ data .WriteInterfaceToken (descriptorBatteryProps )
115+ data .WriteInt32 (propertyID )
47116
48- capacity , err := h . GetCapacity (ctx )
117+ code , err := remote . ResolveCode (ctx , descriptorBatteryProps , "getProperty" )
49118 if err != nil {
50- fmt .Fprintf (os .Stderr , "GetCapacity: %v (health HAL may have died or returned an error)\n " , err )
51- } else {
52- fmt .Printf ("Battery level: %d%%\n " , capacity )
119+ return 0 , 0 , fmt .Errorf ("resolving getProperty: %w" , err )
53120 }
54121
55- status , err := h . GetChargeStatus (ctx )
122+ reply , err := remote . Transact (ctx , code , 0 , data )
56123 if err != nil {
57- fmt .Fprintf (os .Stderr , "GetChargeStatus: %v (health HAL may have died or returned an error)\n " , err )
58- } else {
59- statusName := "unknown"
60- switch status {
61- case health .BatteryStatusUNKNOWN :
62- statusName = "unknown"
63- case health .BatteryStatusCHARGING :
64- statusName = "charging"
65- case health .BatteryStatusDISCHARGING :
66- statusName = "discharging"
67- case health .BatteryStatusNotCharging :
68- statusName = "not charging"
69- case health .BatteryStatusFULL :
70- statusName = "full"
124+ return 0 , 0 , err
125+ }
126+ defer reply .Recycle ()
127+
128+ if err = binder .ReadStatus (reply ); err != nil {
129+ return 0 , 0 , err
130+ }
131+
132+ // Read the BatteryProperty out-parameter (nullable Parcelable).
133+ nullInd , err := reply .ReadInt32 ()
134+ if err != nil {
135+ return 0 , 0 , fmt .Errorf ("reading null indicator: %w" , err )
136+ }
137+ if nullInd != 0 {
138+ // BatteryProperty: { int64 ValueLong; String ValueString; }
139+ value , err = reply .ReadInt64 ()
140+ if err != nil {
141+ return 0 , 0 , fmt .Errorf ("reading ValueLong: %w" , err )
142+ }
143+ _ , err = reply .ReadString () // ValueString (usually empty)
144+ if err != nil {
145+ return 0 , 0 , fmt .Errorf ("reading ValueString: %w" , err )
71146 }
72- fmt .Printf ("Charge status: %s (%d)\n " , statusName , status )
73147 }
74148
75- current , err := h .GetCurrentNowMicroamps (ctx )
149+ // Read the return value (status code: 0 = success).
150+ status , err = reply .ReadInt32 ()
76151 if err != nil {
77- fmt .Fprintf (os .Stderr , "GetCurrentNowMicroamps: %v (health HAL may have died or returned an error)\n " , err )
78- } else {
79- fmt .Printf ("Current draw: %d µA\n " , current )
152+ return 0 , 0 , fmt .Errorf ("reading status: %w" , err )
80153 }
81154
82- counter , err := h .GetChargeCounterUah (ctx )
155+ return value , status , nil
156+ }
157+
158+ func statusToString (status int32 ) string {
159+ switch status {
160+ case 1 :
161+ return "unknown"
162+ case 2 :
163+ return "charging"
164+ case 3 :
165+ return "discharging"
166+ case 4 :
167+ return "not charging"
168+ case 5 :
169+ return "full"
170+ default :
171+ return fmt .Sprintf ("unknown(%d)" , status )
172+ }
173+ }
174+
175+ func printSysfs () {
176+ base := "/sys/class/power_supply/"
177+ entries , err := os .ReadDir (base )
178+ if err != nil {
179+ fmt .Fprintf (os .Stderr , "read sysfs: %v\n " , err )
180+ return
181+ }
182+
183+ for _ , entry := range entries {
184+ dir := base + entry .Name () + "/"
185+ typ := readSysfs (dir + "type" )
186+ if typ == "" {
187+ continue
188+ }
189+ fmt .Printf ("\n === %s (type: %s) ===\n " , entry .Name (), typ )
190+
191+ for _ , attr := range []string {
192+ "status" , "capacity" , "charge_counter" ,
193+ "current_now" , "current_avg" , "voltage_now" ,
194+ "temp" , "health" , "technology" ,
195+ } {
196+ val := readSysfs (dir + attr )
197+ if val != "" {
198+ fmt .Printf (" %-18s %s\n " , attr + ":" , val )
199+ }
200+ }
201+ }
202+ }
203+
204+ func readSysfs (path string ) string {
205+ data , err := os .ReadFile (path )
83206 if err != nil {
84- fmt .Fprintf (os .Stderr , "GetChargeCounterUah: %v (health HAL may have died or returned an error)\n " , err )
85- } else {
86- fmt .Printf ("Charge counter: %d µAh\n " , counter )
207+ return ""
87208 }
209+ return strings .TrimSpace (string (data ))
88210}
0 commit comments