view libervia/desktop_kivy/plugins/plugin_wid_remote.py @ 499:f387992d8e37

plugins: new "call" plugin for A/V calls: this is the base implementation for calls plugin, handling one2one calls. For now, the interface is very basic, call is done by specifying the bare jid of the destinee, then press the "call" button. Incoming calls are automatically accepted. rel 424
author Goffi <goffi@goffi.org>
date Wed, 04 Oct 2023 22:54:36 +0200
parents b3cedbee561d
children
line wrap: on
line source

#!/usr/bin/env python3


#Libervia Desktop-Kivy
# Copyright (C) 2016-2021 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


from libervia.backend.core import log as logging
from libervia.backend.core.i18n import _
from libervia.frontends.quick_frontend import quick_widgets
from ..core import cagou_widget
from ..core.constants import Const as C
from ..core.behaviors import TouchMenuBehavior, FilterBehavior
from ..core.common_widgets import (Identities, ItemWidget, DeviceWidget,
                                       CategorySeparator)
from libervia.backend.tools.common import template_xmlui
from libervia.backend.tools.common import data_format
from libervia.desktop_kivy.core import xmlui
from libervia.frontends.tools import jid
from kivy import properties
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from libervia.desktop_kivy import G
from functools import partial


log = logging.getLogger(__name__)

PLUGIN_INFO = {
    "name": _("remote control"),
    "main": "RemoteControl",
    "description": _("universal remote control"),
    "icon_symbol": "signal",
}

NOTE_TITLE = _("Media Player Remote Control")


class RemoteItemWidget(ItemWidget):

    def __init__(self, device_jid, node, name, main_wid, **kw):
        self.device_jid = device_jid
        self.node = node
        super(RemoteItemWidget, self).__init__(name=name, main_wid=main_wid, **kw)

    def do_item_action(self, touch):
        self.main_wid.layout.clear_widgets()
        player_wid = MediaPlayerControlWidget(main_wid=self.main_wid, remote_item=self)
        self.main_wid.layout.add_widget(player_wid)


class MediaPlayerControlWidget(BoxLayout):
    main_wid = properties.ObjectProperty()
    remote_item = properties.ObjectProperty()
    status = properties.OptionProperty("play", options=("play", "pause", "stop"))
    title = properties.StringProperty()
    identity = properties.StringProperty()
    command = properties.DictProperty()
    ui_tpl = properties.ObjectProperty()

    @property
    def profile(self):
        return self.main_wid.profile

    def update_ui(self, action_data_s):
        action_data = data_format.deserialise(action_data_s)
        xmlui_raw = action_data['xmlui']
        ui_tpl = template_xmlui.create(G.host, xmlui_raw)
        self.ui_tpl = ui_tpl
        for prop in ('Title', 'Identity'):
            try:
                setattr(self, prop.lower(), ui_tpl.widgets[prop].value)
            except KeyError:
                log.warning(_("Missing field: {name}").format(name=prop))
        playback_status = self.ui_tpl.widgets['PlaybackStatus'].value
        if playback_status == "Playing":
            self.status = "pause"
        elif playback_status == "Paused":
            self.status = "play"
        elif playback_status == "Stopped":
            self.status = "play"
        else:
            G.host.add_note(
                title=NOTE_TITLE,
                message=_("Unknown playback status: playback_status")
                          .format(playback_status=playback_status),
                level=C.XMLUI_DATA_LVL_WARNING)
        self.commands = {v:k for k,v in ui_tpl.widgets['command'].options}

    def ad_hoc_run_cb(self, xmlui_raw):
        ui_tpl = template_xmlui.create(G.host, xmlui_raw)
        data = {xmlui.XMLUIPanel.escape("media_player"): self.remote_item.node,
                "session_id": ui_tpl.session_id}
        G.host.bridge.action_launch(
            ui_tpl.submit_id, data_format.serialise(data),
            self.profile, callback=self.update_ui,
            errback=self.main_wid.errback
        )

    def on_remote_item(self, __, remote):
        NS_MEDIA_PLAYER = G.host.ns_map["mediaplayer"]
        G.host.bridge.ad_hoc_run(str(remote.device_jid), NS_MEDIA_PLAYER, self.profile,
                               callback=self.ad_hoc_run_cb,
                               errback=self.main_wid.errback)

    def do_cmd(self, command):
        try:
            cmd_value = self.commands[command]
        except KeyError:
            G.host.add_note(
                title=NOTE_TITLE,
                message=_("{command} command is not managed").format(command=command),
                level=C.XMLUI_DATA_LVL_WARNING)
        else:
            data = {xmlui.XMLUIPanel.escape("command"): cmd_value,
                    "session_id": self.ui_tpl.session_id}
            # hidden values are normally transparently managed by XMLUIPanel
            # but here we have to add them by hand
            hidden = {xmlui.XMLUIPanel.escape(k):v
                      for k,v in self.ui_tpl.hidden.items()}
            data.update(hidden)
            G.host.bridge.action_launch(
                self.ui_tpl.submit_id, data_format.serialise(data), self.profile,
                callback=self.update_ui, errback=self.main_wid.errback
            )


class RemoteDeviceWidget(DeviceWidget):

    def xmlui_cb(self, data, cb_id, profile):
        if 'xmlui' in data:
            xml_ui = xmlui.create(
                G.host, data['xmlui'], callback=self.xmlui_cb, profile=profile)
            if isinstance(xml_ui, xmlui.XMLUIDialog):
                self.main_wid.show_root_widget()
                xml_ui.show()
            else:
                xml_ui.set_close_cb(self.on_close)
                self.main_wid.layout.add_widget(xml_ui)
        else:
            if data:
                log.warning(_("Unhandled data: {data}").format(data=data))
            self.main_wid.show_root_widget()

    def on_close(self, __, reason):
        if reason == C.XMLUI_DATA_CANCELLED:
            self.main_wid.show_root_widget()
        else:
            self.main_wid.layout.clear_widgets()

    def ad_hoc_run_cb(self, data):
        xml_ui = xmlui.create(G.host, data, callback=self.xmlui_cb, profile=self.profile)
        xml_ui.set_close_cb(self.on_close)
        self.main_wid.layout.add_widget(xml_ui)

    def do_item_action(self, touch):
        self.main_wid.layout.clear_widgets()
        G.host.bridge.ad_hoc_run(str(self.entity_jid), '', self.profile,
            callback=self.ad_hoc_run_cb, errback=self.main_wid.errback)


class DevicesLayout(FloatLayout):
    """Layout used to show devices"""
    layout = properties.ObjectProperty()


class RemoteControl(quick_widgets.QuickWidget, cagou_widget.LiberviaDesktopKivyWidget, FilterBehavior,
                  TouchMenuBehavior):
    SINGLE=False
    layout = properties.ObjectProperty()

    def __init__(self, host, target, profiles):
        quick_widgets.QuickWidget.__init__(self, host, target, profiles)
        cagou_widget.LiberviaDesktopKivyWidget.__init__(self)
        FilterBehavior.__init__(self)
        TouchMenuBehavior.__init__(self)
        self.stack_layout = None
        self.show_root_widget()

    def errback(self, failure_):
        """Generic errback which add a warning note and go back to root widget"""
        G.host.add_note(
            title=NOTE_TITLE,
            message=_("Can't use remote control: {reason}").format(reason=failure_),
            level=C.XMLUI_DATA_LVL_WARNING)
        self.show_root_widget()

    def key_input(self, window, key, scancode, codepoint, modifier):
        if key == 27:
            self.show_root_widget()
            return True

    def show_root_widget(self):
        self.layout.clear_widgets()
        devices_layout = DevicesLayout()
        self.stack_layout = devices_layout.layout
        self.layout.add_widget(devices_layout)
        found = []
        self.get_remotes(found)
        self.discover_devices(found)

    def ad_hoc_remotes_get_cb(self, remotes_data, found):
        found.insert(0, remotes_data)
        if len(found) == 2:
            self.show_devices(found)

    def ad_hoc_remotes_get_eb(self, failure_, found):
        G.host.errback(failure_, title=_("discovery error"),
                       message=_("can't check remote controllers: {msg}"))
        found.insert(0, [])
        if len(found) == 2:
            self.show_devices(found)

    def get_remotes(self, found):
        self.host.bridge.ad_hoc_remotes_get(
            self.profile,
            callback=partial(self.ad_hoc_remotes_get_cb, found=found),
            errback=partial(self.ad_hoc_remotes_get_eb,found=found))

    def _disco_find_by_features_cb(self, data, found):
        found.append(data)
        if len(found) == 2:
            self.show_devices(found)

    def _disco_find_by_features_eb(self, failure_, found):
        G.host.errback(failure_, title=_("discovery error"),
                       message=_("can't check devices: {msg}"))
        found.append(({}, {}, {}))
        if len(found) == 2:
            self.show_devices(found)

    def discover_devices(self, found):
        """Looks for devices handling file "File Information Sharing" and display them"""
        try:
            namespace = self.host.ns_map['commands']
        except KeyError:
            msg = _("can't find ad-hoc commands namespace, is the plugin running?")
            log.warning(msg)
            G.host.add_note(_("missing plugin"), msg, C.XMLUI_DATA_LVL_ERROR)
            return
        self.host.bridge.disco_find_by_features(
            [namespace], [], False, True, True, True, False, self.profile,
            callback=partial(self._disco_find_by_features_cb, found=found),
            errback=partial(self._disco_find_by_features_eb, found=found))

    def show_devices(self, found):
        remotes_data, (entities_services, entities_own, entities_roster) = found
        if remotes_data:
            title = _("media players remote controls")
            self.stack_layout.add_widget(CategorySeparator(text=title))

        for remote_data in remotes_data:
            device_jid, node, name = remote_data
            wid = RemoteItemWidget(device_jid, node, name, self)
            self.stack_layout.add_widget(wid)

        for entities_map, title in ((entities_services,
                                     _('services')),
                                    (entities_own,
                                     _('your devices')),
                                    (entities_roster,
                                     _('your contacts devices'))):
            if entities_map:
                self.stack_layout.add_widget(CategorySeparator(text=title))
                for entity_str, entity_ids in entities_map.items():
                    entity_jid = jid.JID(entity_str)
                    item = RemoteDeviceWidget(
                        self, entity_jid, Identities(entity_ids))
                    self.stack_layout.add_widget(item)
        if (not remotes_data and not entities_services and not entities_own
            and not entities_roster):
            self.stack_layout.add_widget(Label(
                size_hint=(1, 1),
                halign='center',
                text_size=self.size,
                text=_("No sharing device found")))