VulpesOS/tundra/devices/beatles/call_audio.py
2026-10-22 20:42:26 +02:00

71 lines
3.8 KiB
Python

# SPDX-License-Identifier: MPL-2.0
"""Android 6 audio services used by the Nokia firmware, outside Gecko.
Transaction IDs and argument types: AOSP android-6.0.1_r1,
frameworks/av/media/libmedia/{IAudioPolicyService,IAudioFlinger}.cpp.
This adapter is specific to that ABI. No arbitrary Binder transaction is exposed.
"""
import re
import subprocess
import time
class CallAudio:
def __init__(self):
self.mode=0;self.muted=False;self.speaker=False;self.available=False
self.original=None;self.retry_at=0
def transact(self,service,method,*values):
command=['/system/bin/service','call','media.audio_'+service,str(method)]
for value in values:command.extend(('i32',str(value)))
result=subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,
text=True,timeout=2,env={'PATH':'/system/bin','LD_LIBRARY_PATH':'/system/lib'})
match=re.fullmatch(r"Result: Parcel\(([0-9a-fA-F]{8})\s+'[^']*'\)\s*",result.stdout)
if result.returncode or not match:raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
return int(match[1],16)
def initialize(self):
if self.available:return
if time.monotonic()<self.retry_at:raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
try:
muted=self.transact('flinger',18);route=self.transact('policy',6,0)
if muted not in (0,1) or route not in range(16):raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
if self.original is None:self.original=(muted,route)
self.muted=bool(muted);self.speaker=route==1;self.available=True
except (OSError,RuntimeError,subprocess.SubprocessError):
self.retry_at=time.monotonic()+5;raise RuntimeError('CALL_AUDIO_UNAVAILABLE') from None
def set_mode(self,mode):
self.initialize()
if self.mode==mode:return
if self.transact('policy',3,mode):raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
self.mode=mode
def update(self,calls):
mode=2 if any(c['state'] in ('active','held','dialing','alerting') for c in calls) else 1 if calls else 0
try:self.set_mode(mode)
except (OSError,RuntimeError,subprocess.SubprocessError):
self.available=False;self.retry_at=time.monotonic()+5
def request(self,value):
operation=value.get('operation')
if operation not in ('status','set'):raise ValueError('INVALID_REQUEST')
for key in ('muted','speaker'):
if key in value and type(value[key]) is not bool:raise ValueError('INVALID_REQUEST')
self.initialize()
if operation=='set':
if 'speaker' in value and value['speaker']!=self.speaker:
if self.transact('policy',5,0,int(value['speaker'])):raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
actual=self.transact('policy',6,0)
if actual!=int(value['speaker']):raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
self.speaker=value['speaker']
if 'muted' in value and value['muted']!=self.muted:
if self.transact('flinger',17,int(value['muted'])):raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
if self.transact('flinger',18)!=int(value['muted']):raise RuntimeError('CALL_AUDIO_UNAVAILABLE')
self.muted=value['muted']
return {'muted':self.muted,'speaker':self.speaker,'active':self.mode==2,'available':self.available}
def restore(self):
if self.original is None:return
muted,route=self.original
changes=[]
if self.mode:changes.append(('policy',3,(0,)))
if self.speaker!=(route==1):changes.append(('policy',5,(0,route)))
if self.muted!=bool(muted):changes.append(('flinger',17,(muted,)))
for service,method,values in changes:
try:self.transact(service,method,*values)
except (OSError,RuntimeError,subprocess.SubprocessError):pass