alien-everywhere/shimming/alienaudioservice/alienaudio-shim.py

1058 lines
35 KiB
Python
Raw Normal View History

2023-12-20 10:44:46 +00:00
#!/usr/bin/python
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from enum import IntEnum, IntFlag
import logging
from typing import Any, Callable, Coroutine, Optional
from sdbus import (
DbusInterfaceCommonAsync,
dbus_method_async,
dbus_signal_async,
get_current_message,
sd_bus_open_system,
)
import pulsectl
import pulsectl_asyncio
# Resource policy manager D-BUS parameters
POLICY_DBUS_SERVICE = "org.maemo.resource.manager"
POLICY_DBUS_PATH = "/org/maemo/resource/manager"
POLICY_DBUS_INTERFACE = "org.maemo.resource.manager"
CLIENT_DBUS_INTERFACE = "org.maemo.resource.client"
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)
class Client(DbusInterfaceCommonAsync, interface_name=CLIENT_DBUS_INTERFACE):
@dbus_method_async("iuuu", method_name="advice")
async def advice(self, type: int, id: int, reqno: int, resources: int) -> None:
raise NotImplementedError
@dbus_method_async("iuuu", method_name="grant")
async def grant(self, type: int, id: int, reqno: int, resources: int) -> None:
raise NotImplementedError
@dataclass
class Resource:
mandatory: int
optional: int
dbus: Client
is_alien_call: bool = False
class DummyPolicyDaemon(DbusInterfaceCommonAsync, interface_name=POLICY_DBUS_INTERFACE):
def __init__(self, routeManager: RouteManagerShim) -> None:
super().__init__()
self.route_manager = routeManager
self.background_tasks: set[asyncio.Task[None]] = set()
self.granted = True
self.clients: dict[str, dict[str, Resource]] = {}
def path(self, id: int) -> str:
return f"/org/maemo/resource/client{id}"
async def advice(self, sender: str, id: int, reqno: int, resources: int) -> None:
type = 6
print("")
print("ADVICE")
print("type: ", str(type))
print("id: ", str(id))
print("reqno: ", str(reqno))
print("resources:", str(resources))
path = self.path(id)
if sender in self.clients:
if path in self.clients[sender]:
resource = self.clients[sender][path]
try:
await resource.dbus.advice(type, id, reqno, resources)
log.info("Reply")
except:
log.exception("Something went wrong")
async def grant(self, sender: str, id: int, reqno: int, resources: int) -> None:
type = 5
print("")
print("GRANT")
print("type: ", str(type))
print("id: ", str(id))
print("reqno: ", str(reqno))
print("resources:", str(resources))
path = self.path(id)
if sender in self.clients:
if path in self.clients[sender]:
resource = self.clients[sender][path]
try:
await resource.dbus.grant(type, id, reqno, resources)
except:
log.exception("Something went wrong")
def idle_response(
self,
func: Callable[[str, int, int, int], Coroutine[None, None, None]],
sender: str,
id: int,
reqno: int,
resources: int,
) -> None:
async def wrap() -> None:
await asyncio.sleep(1)
await func(sender, id, reqno, resources)
task = asyncio.create_task(wrap())
task.add_done_callback(self.background_tasks.discard)
self.background_tasks.add(task)
@dbus_method_async("iuuuuuussu", "iuuis", method_name="register")
async def register(
self,
type: int,
id: int,
reqno: int,
mandatory: int,
optional: int,
share: int,
mask: int,
app_id: str,
klass: str,
mode: int,
) -> tuple[int, int, int, int, str]:
try:
sender = get_current_message().sender
error_code = 0
error_string = "OK"
print("")
print("REGISTER", sender)
print("type: ", str(type))
print("id: ", str(id))
print("reqno: ", str(reqno))
print("mandatory:", str(mandatory))
print("optional: ", str(optional))
print("share: ", str(share))
print("mask: ", str(mask))
print("app_id: ", app_id)
print("class: ", klass)
print("mode: ", str(mode))
client = {}
if sender in self.clients:
client = self.clients[sender]
path = self.path(id)
assert sender is not None
proxy = Client.new_proxy(sender, path, bus=sd_bus_open_system())
resource = Resource(mandatory, optional, proxy)
client[path] = resource
self.clients[sender] = client
self.idle_response(self.advice, sender, id, reqno, resource.mandatory)
return (9, id, reqno, error_code, error_string)
except:
log.exception('')
@dbus_method_async("iuuuuuussu", "iuuis", method_name="update")
async def update(
self,
type: int,
id: int,
reqno: int,
mandatory: int,
optional: int,
share: int,
mask: int,
app_id: str,
klass: str,
mode: int,
) -> tuple[int, int, int, int, str]:
try:
sender = get_current_message().sender
error_code = 0
error_string = "OK"
print("")
print("UPDATE", sender)
print("type: ", str(type))
print("id: ", str(id))
print("reqno: ", str(reqno))
print("mandatory:", str(mandatory))
print("optional: ", str(optional))
print("share: ", str(share))
print("mask: ", str(mask))
print("app_id: ", app_id)
print("class: ", klass)
print("mode: ", str(mode))
return (9, id, reqno, error_code, error_string)
except:
log.exception('')
@dbus_method_async("iuu", "iuuis", method_name="acquire")
async def acquire(
self, type: int, id: int, reqno: int
) -> tuple[int, int, int, int, str]:
try:
sender = get_current_message().sender
error_code = 0
error_string = "OK"
print("")
print("ACQUIRE", sender)
print("type: ", str(type))
print("id: ", str(id))
print("reqno: ", str(reqno))
assert sender is not None
# find the client
resource = self.clients[sender][self.path(id)]
if resource.is_alien_call:
log.info("switching to call mode")
# subprocess.run(['callaudiocli', '-m', '1'])
await self.route_manager.route_output_for_call(True)
self.idle_response(self.grant, sender, id, reqno, resource.mandatory)
return (9, id, reqno, error_code, error_string)
except:
log.exception('')
@dbus_method_async("iuu", "iuuis", method_name="release")
async def release(
self, type: int, id: int, reqno: int
) -> tuple[int, int, int, int, str]:
try:
sender = get_current_message().sender
error_code = 0
error_string = "OK"
print("")
print("RELEASE", sender)
print("type: ", str(type))
print("id: ", str(id))
print("reqno: ", str(reqno))
assert sender is not None
self.idle_response(self.grant, sender, id, reqno, 0)
return (9, id, reqno, error_code, error_string)
except:
log.exception('')
@dbus_method_async("iuu", "iuuis", method_name="unregister")
async def unregister(
self, type: int, id: int, reqno: int
) -> tuple[int, int, int, int, str]:
try:
sender = get_current_message().sender
errorCode = 0
errorString = "OK"
print("")
print("UNREGISTER", sender)
print("type: ", str(type))
print("id: ", str(id))
print("reqno: ", str(reqno))
assert sender is not None
client = self.clients[sender]
path = self.path(id)
resource = client[path]
if resource.is_alien_call:
log.info("switching back from call mode")
# subprocess.run(['callaudiocli', '-m', '0'])
await self.route_manager.route_output_for_call(False)
del client[path]
if len(client) == 0:
del self.clients[sender]
return (9, id, reqno, errorCode, errorString)
except:
log.exception('')
@dbus_method_async("iuusssis", "iuuis", method_name="audio")
async def audio(
self,
type: int,
id: int,
reqno: int,
group: str,
pid: str,
streamName: str,
method: int,
pattern: str,
) -> tuple[int, int, int, int, str]:
try:
sender = get_current_message().sender
error_code = 0
error_string = "OK"
print("")
print("AUDIO", sender)
print("type: ", str(type))
print("id: ", str(id))
print("reqno: ", str(reqno))
print("group: ", group)
print("pid: ", pid)
print("stream name:", streamName)
# typedef enum {
# resmsg_method_equals = 0,
# resmsg_method_startswith,
# resmsg_method_matches
# } resmsg_match_method_t;
print("method: ", str(method))
print("pattern: ", pattern)
assert sender is not None
resource = self.clients[sender][self.path(id)]
if streamName == "media.name" and method == 0 and pattern == "voice":
log.info("detected voice call, will switch devices on acquire()")
resource.is_alien_call = True
return (9, id, reqno, error_code, error_string)
except:
log.exception('')
@dbus_method_async("iuuu", "iuuis", method_name="video")
async def video(
self, type: int, id: int, reqno: int, pid: int
) -> tuple[int, int, int, int, str]:
try:
sender = get_current_message().sender
error_code = 0
error_string = "OK"
print("")
print("VIDEO", sender)
print("type: ", str(type))
print("id: ", str(id))
print("reqno: ", str(reqno))
print("pid: ", str(pid))
return (9, id, reqno, error_code, error_string)
except:
log.exception('')
class AudioOutputRoute(IntEnum):
EARPIECE = 1
PHONE_SPEAKER = 2
EXTERNAL_SPEAKER = 3
HEADPHONE = 4
class OhmRouteType(IntFlag):
OUTPUT = 1 << 0 # sink
INPUT = 1 << 1 # source
BUILTIN = 1 << 2
WIRED = 1 << 3
WIRELESS = 1 << 4
VOICE = 1 << 5
BLUETOOTH_SCO = 1 << 6
BLUETOOTH_A2DP = 1 << 7
HEADSET = 1 << 8
HEADPHONE = 1 << 9
USB = 1 << 10
UNKNOWN = 1 << 11
AVAILABLE = 1 << 25
PREFERRED = 1 << 26
ACTIVE = 1 << 27
class RouteManagerShim(
DbusInterfaceCommonAsync, interface_name="org.nemomobile.Route.Manager"
):
def __init__(self, pulse: pulsectl_asyncio.PulseAsync) -> None:
super().__init__()
self.pulse = pulse
self.output_route_before_call: Optional[
tuple[pulsectl.PulseSinkInfo, pulsectl.PulsePortInfo]
] = None
self.output_route_before_speaker: Optional[
tuple[pulsectl.PulseSinkInfo, pulsectl.PulsePortInfo]
] = None
self.input_route_before_speaker: Optional[
tuple[pulsectl.PulseSourceInfo, pulsectl.PulsePortInfo]
] = None
self.background_tasks: set[asyncio.Task[None]] = set()
async def listen() -> None:
async for event in self.pulse.subscribe_events('all'):
if event.t == "change":
log.info("pulse event happend")
await self.mce.sync_proximity_monitoring()
task = asyncio.create_task(listen())
task.add_done_callback(self.background_tasks.discard)
self.background_tasks.add(task)
# register signal handlers to cancel listener when program is asked to terminate
# Alternatively, the PulseAudio event subscription can be ended by breaking/returning from the `async for` loop
# for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
# loop.add_signal_handler(sig, task.cancel)
async def _sink_list(self) -> list[pulsectl.PulseSinkInfo]:
sinks = await self.pulse.sink_list()
assert isinstance(sinks, list)
return sinks
async def _source_list(self) -> list[pulsectl.PulseSourceInfo]:
sources = await self.pulse.source_list()
assert isinstance(sources, list)
return sources
@dbus_method_async("s", method_name="Enable")
async def enable(self, route: str) -> None:
try:
log.info("route enable: %s", route)
if route == "speaker":
await self.use_speaker_in_call(True)
except:
log.exception('')
@dbus_method_async("s", method_name="Disable")
async def disable(self, route: str) -> None:
try:
log.info("route disable: %s", route)
if route == "speaker":
await self.use_speaker_in_call(False)
except:
log.exception('')
@dbus_method_async(result_signature="susu", method_name="ActiveRoutes")
async def active_routes(self) -> tuple[str, int, str, int]:
try:
OHM_EXT_ROUTE_TYPE_OUTPUT = 1 << 0 # sink
OHM_EXT_ROUTE_TYPE_INPUT = 1 << 1 # source
OHM_EXT_ROUTE_TYPE_BUILTIN = 1 << 2
OHM_EXT_ROUTE_TYPE_WIRED = 1 << 3
OHM_EXT_ROUTE_TYPE_WIRELESS = 1 << 4
OHM_EXT_ROUTE_TYPE_VOICE = 1 << 5
OHM_EXT_ROUTE_TYPE_BLUETOOTH_SCO = 1 << 6
OHM_EXT_ROUTE_TYPE_BLUETOOTH_A2DP = 1 << 7
OHM_EXT_ROUTE_TYPE_HEADSET = 1 << 8
OHM_EXT_ROUTE_TYPE_HEADPHONE = 1 << 9
OHM_EXT_ROUTE_TYPE_USB = 1 << 10
OHM_EXT_ROUTE_TYPE_UNKNOWN = 1 << 11
OHM_EXT_ROUTE_TYPE_AVAILABLE = 1 << 25
OHM_EXT_ROUTE_TYPE_PREFERRED = 1 << 26
OHM_EXT_ROUTE_TYPE_ACTIVE = 1 << 27
routes = (
"speaker",
OHM_EXT_ROUTE_TYPE_OUTPUT
| OHM_EXT_ROUTE_TYPE_BUILTIN
| OHM_EXT_ROUTE_TYPE_AVAILABLE
| OHM_EXT_ROUTE_TYPE_ACTIVE,
"microphone",
OHM_EXT_ROUTE_TYPE_INPUT | OHM_EXT_ROUTE_TYPE_BUILTIN,
)
log.info("ActiveRoutes: %s", routes)
return routes
except:
log.exception('')
@dbus_signal_async("su", signal_name="AudioRouteChanged")
async def audio_route_changed(self) -> None:
raise NotImplementedError
def audio_route_changed_emit(self, name: str, type: OhmRouteType) -> None:
# fairly certain the checker is wrong here
self.audio_route_changed.emit((name, int(type))) # type: ignore
@dbus_signal_async("suu", signal_name="AudioFeatureChanged")
async def audio_feature_changed(self) -> None:
raise NotImplementedError
def audio_feature_changed_emit(self, name: str, a: bool, b: bool) -> None:
# fairly certain the checker is wrong here
self.audio_feature_changed.emit((name, int(a), int(b))) # type: ignore
async def get_default_sink(self) -> pulsectl.PulseSinkInfo:
sink_name = (await self.pulse.server_info()).default_sink_name
for sink in await self._sink_list():
if sink.name == sink_name:
return sink
raise Exception("No default sink found")
async def get_default_source(self) -> pulsectl.PulseSourceInfo:
source_name = (await self.pulse.server_info()).default_source_name
for source in await self._source_list():
if source.name == source_name:
return source
raise Exception("No default source found")
async def find_private_output_for_call(
self,
) -> tuple[pulsectl.PulseSinkInfo, pulsectl.PulsePortInfo]:
# first try active sink
sink = await self.get_default_sink()
for port in sink.port_list:
if port.available != "no" and port.type == "headphones":
return (sink, port)
if port.available != "no" and port.type == "earpiece":
return (sink, port)
# now try all sinks (these don't have priorities)
for sink in await self._sink_list():
# ports should be ordered by priority already
for port in sink.port_list:
if port.available != "no" and port.type == "headphones":
return (sink, port)
if port.available != "no" and port.type == "earpiece":
return (sink, port)
log.info("no earpiece found")
raise Exception("No earpiece found")
async def find_mic_for_output(
self,
sink: pulsectl.PulseSinkInfo,
port: pulsectl.PulsePortInfo,
) -> tuple[pulsectl.PulseSourceInfo, pulsectl.PulsePortInfo]:
# look for mic on same device as the output
sink_device = sink.name.split('.')[1]
log.info("looking at output dev: %s", sink_device)
searching_for_type = "mic"
for source in await self._source_list():
device = source.name.split('.')[1]
if device == sink_device:
for source_port in source.port_list:
if source_port.available != "no" and source_port.type == searching_for_type:
return (source, source_port)
return (None, None)
async def find_best_output(
self,
) -> tuple[pulsectl.PulseSinkInfo, pulsectl.PulsePortInfo]:
# first try the highest priority port on the current sink
sink = await self.get_default_sink()
for port in sink.port_list:
if port.available != "no":
return (sink, port)
# now try other sinks (these don't have priorities)
for sink in await self._sink_list():
for port in sink.port_list:
if port.available != "no":
return (sink, port)
raise Exception("No best output found")
async def find_speaker_for_call(
self,
) -> tuple[pulsectl.PulseSinkInfo, pulsectl.PulsePortInfo]:
# if route before call happened to be speaker, use that
if self.output_route_before_call is not None:
(sink, port) = self.output_route_before_call
if await self.route_still_exists(sink, port):
if port.available != "no" and port.type == "speaker":
return (sink, port)
# otherwise look for a speaker on current sink
sink = await self.get_default_sink()
for port in sink.port_list:
if port.available != "no" and port.type == "speaker":
return (sink, port)
# otherwise try all sinks (these don't have priorities)
for sink in await self._sink_list():
# ports should be ordered by priority already
for port in sink.port_list:
if port.available != "no" and port.type == "speaker":
return (sink, port)
log.info("no speaker found")
raise Exception("No speaker found")
async def route_still_exists(
self,
existing_sink: pulsectl.PulseSinkInfo,
existing_port: pulsectl.PulsePortInfo,
) -> bool:
log.info("checking if route still exists")
for sink in await self._sink_list():
if sink.name != existing_sink.name:
continue
for port in sink.port_list:
if port.name == existing_port.name and port.available != "no":
print("yupppp")
return True
log.info("that's a no")
return False
async def source_route_still_exists(
self,
existing_source: pulsectl.PulseSourceInfo,
existing_port: pulsectl.PulsePortInfo,
) -> bool:
log.info("checking if route still exists")
for source in await self._source_list():
if source.name != existing_source.name:
continue
for port in source.port_list:
if port.name == existing_port.name and port.available != "no":
print("yupppp")
return True
log.info("that's a no")
return False
async def switch_output(
self,
sink: pulsectl.PulseSinkInfo,
port: pulsectl.PulsePortInfo,
) -> None:
default_sink = await self.get_default_sink()
if sink.name != default_sink.name:
print("need to switch sink")
await self.pulse.default_set(sink)
for sink_input in await self.pulse.sink_input_list():
print("moving stream over to new sink")
await self.pulse.sink_input_move(sink_input.index, sink.index)
if len(sink.port_list) > 1:
await self.pulse.port_set(sink, port)
async def switch_input(
self,
source: pulsectl.PulseSourceInfo,
port: pulsectl.PulsePortInfo,
) -> None:
default_source = await self.get_default_source()
if source.name != default_source.name:
print("need to switch source")
await self.pulse.default_set(source)
for source_output in await self.pulse.source_output_list():
print("moving stream over to new source")
await self.pulse.source_output_move(source_output.index, source.index)
if len(source.port_list) > 1:
await self.pulse.port_set(source, port)
async def route_output_for_call(self, in_call: bool) -> None:
log.info("routeOutputForCall %r", in_call)
default_sink = await self.get_default_sink()
log.info("cur route %s", default_sink.port_active.name)
if in_call:
# if any speaker (could also be bt) is in use, switch to a more
# private device (earpiece or headphones), otherwise just leave
# things as-is
if default_sink.port_active.type == "speaker":
self.output_route_before_call = (default_sink, default_sink.port_active)
(sink, port) = await self.find_private_output_for_call()
await self.switch_output(sink, port)
self.audio_route_changed_emit(
"speaker",
OhmRouteType.OUTPUT | OhmRouteType.BUILTIN | OhmRouteType.AVAILABLE,
)
self.audio_route_changed_emit(
"earpiece",
OhmRouteType.OUTPUT
| OhmRouteType.BUILTIN
| OhmRouteType.VOICE
| OhmRouteType.AVAILABLE
| OhmRouteType.ACTIVE,
)
self.audio_feature_changed_emit("speaker", True, False)
else:
if default_sink.port_active.type == "earpiece":
if self.output_route_before_call is not None:
log.info("Restoring output route from before call")
(sink, prev_port) = self.output_route_before_call
if not await self.route_still_exists(sink, prev_port):
log.info("Route no longer exists, finding new one")
(sink, prev_port) = await self.find_best_output()
else:
(sink, prev_port) = await self.find_best_output()
await self.switch_output(sink, prev_port)
if self.input_route_before_speaker is not None:
log.info("Restoring input route from before call")
(source, prev_port) = self.input_route_before_speaker
if await self.source_route_still_exists(source, prev_port):
await self.switch_input(source, prev_port)
else:
log.info("Route no longer exists, not restoring")
self.output_route_before_call = None
self.output_route_before_speaker = None
self.input_route_before_speaker = None
log.info("new route: %s", (await self.get_default_sink()).port_active.name)
await self.mce.sync_proximity_monitoring()
async def use_speaker_in_call(self, use_speaker: bool) -> None:
log.info("useSpeakerInCall %r", use_speaker)
default_sink = await self.get_default_sink()
log.info("cur route %s", default_sink.port_active.name)
if use_speaker:
if default_sink.port_active.type != "speaker":
self.output_route_before_speaker = (default_sink, default_sink.port_active)
(sink, port) = await self.find_speaker_for_call()
await self.switch_output(sink, port)
default_source = await self.get_default_source()
if default_source.port_active.type != "mic":
(source, port) = await self.find_mic_for_output(sink, port)
if source != None:
log.info("Found an internal mic, switching")
self.input_route_before_speaker = (default_source, default_source.port_active)
await self.switch_input(source, port)
self.audio_route_changed_emit(
"earpiece",
OhmRouteType.OUTPUT
| OhmRouteType.BUILTIN
| OhmRouteType.VOICE
| OhmRouteType.AVAILABLE,
)
self.audio_route_changed_emit(
"speaker",
OhmRouteType.OUTPUT
| OhmRouteType.BUILTIN
| OhmRouteType.AVAILABLE
| OhmRouteType.ACTIVE,
)
self.audio_feature_changed_emit("speaker", True, True)
else:
# we're not listening to sink/port changes from pulse,
# so if output changed to a speaker underneath our feet, just change the route silently
self.audio_route_changed_emit(
"earpiece",
OhmRouteType.OUTPUT
| OhmRouteType.BUILTIN
| OhmRouteType.VOICE
| OhmRouteType.AVAILABLE,
)
self.audio_route_changed_emit(
"speaker",
OhmRouteType.OUTPUT
| OhmRouteType.BUILTIN
| OhmRouteType.AVAILABLE
| OhmRouteType.ACTIVE,
)
self.audio_feature_changed_emit("speaker", True, True)
else:
if default_sink.port_active.type == "speaker":
if self.output_route_before_speaker is not None:
log.info("Restoring output route from before speaker")
(sink, prev_port) = self.output_route_before_speaker
if not await self.route_still_exists(sink, prev_port):
log.info("Route no longer exists, finding new one")
(sink, prev_port) = await self.find_private_output_for_call()
else:
(sink, prev_port) = await self.find_private_output_for_call()
await self.switch_output(sink, prev_port)
if self.input_route_before_speaker is not None:
log.info("Restoring input route from before speaker")
(source, prev_port) = self.input_route_before_speaker
if await self.source_route_still_exists(source, prev_port):
await self.switch_input(source, prev_port)
else:
log.info("Route no longer exists, not restoring")
self.audio_route_changed_emit(
"speaker",
OhmRouteType.OUTPUT | OhmRouteType.BUILTIN | OhmRouteType.AVAILABLE,
)
self.audio_route_changed_emit(
"earpiece",
OhmRouteType.OUTPUT
| OhmRouteType.BUILTIN
| OhmRouteType.VOICE
| OhmRouteType.AVAILABLE
| OhmRouteType.ACTIVE,
)
self.audio_feature_changed_emit("speaker", True, False)
else:
# we're not listening to sink/port changes from pulse,
# so if output changed to a speaker underneath our feet, just change the route silently
self.audio_route_changed_emit(
"speaker",
OhmRouteType.OUTPUT | OhmRouteType.BUILTIN | OhmRouteType.AVAILABLE,
)
self.audio_route_changed_emit(
"earpiece",
OhmRouteType.OUTPUT
| OhmRouteType.BUILTIN
| OhmRouteType.VOICE
| OhmRouteType.AVAILABLE
| OhmRouteType.ACTIVE,
)
self.audio_feature_changed_emit("speaker", True, False)
self.output_route_before_speaker = None
self.input_route_before_speaker = None
log.info("new route: %s", (await self.get_default_sink()).port_active.name)
await self.mce.sync_proximity_monitoring()
async def is_routing_to_earpiece(self) -> bool:
default_sink = await self.get_default_sink()
return default_sink.port_active.type == "earpiece"
def set_mce(self, mce: MceShim) -> bool:
self.mce = mce
class SensorDaemon(DbusInterfaceCommonAsync, interface_name="org.gnome.Shell.SensorDaemon"):
@dbus_method_async(method_name="StartProximityMonitoring")
async def start_proximity_monitoring() -> None:
raise NotImplementedError
@dbus_method_async(method_name="StopProximityMonitoring")
async def stop_proximity_monitoring() -> None:
raise NotImplementedError
class MceShim(DbusInterfaceCommonAsync, interface_name="com.nokia.mce.request"):
def __init__(self, route_manager: RouteManagerShim) -> None:
super().__init__()
self.route_manager = route_manager
self.sensor_daemon = SensorDaemon.new_proxy(
"org.gnome.Shell.SensorDaemon", "/org/gnome/Shell/SensorDaemon"
)
self.in_call = False
self.monitoring_proximity = False
async def sync_proximity_monitoring(self) -> None:
should_monitor = self.in_call and await self.route_manager.is_routing_to_earpiece()
log.info("sync moni %d", should_monitor)
if should_monitor != self.monitoring_proximity:
self.monitoring_proximity = should_monitor
if should_monitor:
await self.sensor_daemon.start_proximity_monitoring()
else:
await self.sensor_daemon.stop_proximity_monitoring()
@dbus_method_async("ss", "b", method_name="req_call_state_change")
async def req_call_state_change(self, state: str, desc: str) -> bool:
try:
log.info("req_call_state_change: %s", state)
if state == "active":
self.in_call = True
elif state == "none":
self.in_call = False
await self.sync_proximity_monitoring()
return True
except:
log.exception('')
class Feedbackd(DbusInterfaceCommonAsync, interface_name="org.sigxcpu.Feedback"):
@dbus_method_async("ssa{sv}i", "u", method_name="TriggerFeedback")
async def trigger_feedback(
self, app_id: str, event: str, hints: dict[str, Any], timeout: int
) -> int:
raise NotImplementedError
@dbus_method_async("u", method_name="EndFeedback")
async def end_feedback(self, id: int) -> None:
raise NotImplementedError
class NonGraphicFeedbackShim(
DbusInterfaceCommonAsync, interface_name="com.nokia.NonGraphicFeedback1"
):
def __init__(self) -> None:
super().__init__()
self.feedbackd = Feedbackd.new_proxy(
"org.sigxcpu.Feedback", "/org/sigxcpu/Feedback"
)
self.current_id = -1
def translate_feedback(
self, name: str, params: dict[str, Any]
) -> tuple[str, dict[str, Any]]:
hints = {}
feedbackName = None
if name == "feedback_press":
# generic touch feedback
feedbackName = "button-pressed"
elif name == "pulldown_highlight":
# another generic touch feedback
feedbackName = "button-pressed"
elif name == "vibra":
# only vibration feedback, no sound
feedbackName = "bell-terminal"
hints["profile"] = ("s", "quiet")
if 'media.vibra' in params:
should_vibrate = params['media.vibra'][1]
if not should_vibrate:
# unfortunately we can only disable vibration by also disabling sound
hints["profile"] = ("s", "silent")
if 'haptic.sequence' in params:
haptic_sequence = params['haptic.sequence'][1]
# in format "on=timeMs"
if 'haptic.type' in params:
haptic_type = params['haptic.type'][1]
# can be "touch"
return (feedbackName, hints)
@dbus_method_async("sa{sv}", "u", method_name="Play")
async def play(self, name: str, params: dict[str, Any]) -> int:
try:
log.info("NonGraphicFeedbackShim Play: %s", name)
print(params)
(feedback_name, hints) = self.translate_feedback(name, params)
if feedback_name == None:
return 0
print("NonGraphicFeedbackShim actually playing:", feedback_name, hints)
self.current_id = await self.feedbackd.trigger_feedback(
"aliendalvik", feedback_name, hints, -1
)
return self.current_id
except:
log.exception('')
@dbus_method_async("u", "u", method_name="Stop")
async def stop(self, feedback_id: int) -> int:
try:
log.info("NonGraphicFeedbackShim Stop: %i", feedback_id)
await self.feedbackd.end_feedback(feedback_id)
self.current_id = -1
return 0
except:
log.exception('')
async def main() -> None:
system = sd_bus_open_system()
await asyncio.gather(
*(
system.request_name_async(name, 0)
for name in [
POLICY_DBUS_SERVICE,
"com.nokia.NonGraphicFeedback1.Backend",
"org.nemomobile.Route.Manager",
"com.nokia.mce",
]
)
)
async with pulsectl_asyncio.PulseAsync("my-client-name") as pulse:
log.info("sinks: %r", await pulse.sink_list())
route_manager = RouteManagerShim(pulse)
route_manager.export_to_dbus("/org/nemomobile/Route/Manager", system)
call_daemon = DummyPolicyDaemon(route_manager)
call_daemon.export_to_dbus(POLICY_DBUS_PATH, system)
mce = MceShim(route_manager)
route_manager.set_mce(mce)
mce.export_to_dbus("/com/nokia/mce/request", system)
ngf = NonGraphicFeedbackShim()
ngf.export_to_dbus("/com/nokia/NonGraphicFeedback1", system)
while True:
await asyncio.sleep(10)
if __name__ == "__main__":
asyncio.run(main())