Mercurial > libervia-desktop-kivy
changeset 322:e2b51663d8b8
core, android: new share widget + added Cagou to "share" menu:
- new intent filter to add Cagou to share menu for all media types
- minimum Kivy version is now 1.11.0
- new "Share" widget to display data to share via SàT and select the target
- new core.platform_ module (the suffix "_" avoid trouble with standard "platform"
module), for platform specific code.
- Android intent are now checked on startup and "on_new_intent" events
- if a android.intent.action.SEND action is received (i.e. some data is shared), the
"Share" widget is shown
- new Cagou.share method to share data using "Share" widget
- new Cagou.getAncestorWidget method to easily retrieve an instance of a specific class in
a widget's ancestors
- ContactList's Avatar and ContactItem widgets have been moved to core.common
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 06 Dec 2019 13:23:03 +0100 |
parents | a6eb154ba266 |
children | 5bd583d00594 |
files | .p4a .p4a_blacklist android_intents/share.xml cagou/core/cagou_main.py cagou/core/common.py cagou/core/constants.py cagou/core/menu.py cagou/core/platform_/__init__.py cagou/core/platform_/android.py cagou/kv/common.kv cagou/kv/share_widget.kv cagou/plugins/plugin_wid_chat.kv cagou/plugins/plugin_wid_chat.py cagou/plugins/plugin_wid_contact_list.kv cagou/plugins/plugin_wid_contact_list.py cagou/plugins/plugin_wid_remote.py |
diffstat | 15 files changed, 352 insertions(+), 82 deletions(-) [+] |
line wrap: on
line diff
--- a/.p4a Fri Dec 06 13:23:03 2019 +0100 +++ b/.p4a Fri Dec 06 13:23:03 2019 +0100 @@ -21,4 +21,5 @@ --permission RECORD_AUDIO --permission READ_EXTERNAL_STORAGE --permission WRITE_EXTERNAL_STORAGE +--intent-filters /home/goffi/dev/cagou/android_intents/share.xml --ignore-setup-py
--- a/.p4a_blacklist Fri Dec 06 13:23:03 2019 +0100 +++ b/.p4a_blacklist Fri Dec 06 13:23:03 2019 +0100 @@ -3,3 +3,4 @@ *_old *_old/* *.tar.bz2 +android_intents
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/android_intents/share.xml Fri Dec 06 13:23:03 2019 +0100 @@ -0,0 +1,5 @@ +<intent-filter> + <action android:name="android.intent.action.SEND" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="*/*" /> +</intent-filter>
--- a/cagou/core/cagou_main.py Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/core/cagou_main.py Fri Dec 06 13:23:03 2019 +0100 @@ -1,5 +1,4 @@ -#!/usr//bin/env python2 -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 # Cagou: desktop/mobile frontend for Salut à Toi XMPP client # Copyright (C) 2016-2019 Jérôme Poisson (goffi@goffi.org) @@ -26,7 +25,6 @@ kivy_hack.do_hack() from .constants import Const as C from sat.core import log as logging -log = logging.getLogger(__name__) from sat.core import exceptions from sat_frontends.quick_frontend.quick_app import QuickApp from sat_frontends.quick_frontend import quick_widgets @@ -37,7 +35,7 @@ from sat.tools import config from sat.tools.common import dynamic_import import kivy -kivy.require('1.10.0') +kivy.require('1.11.0') import kivy.support main_config = config.parseMainConf() bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus') @@ -65,6 +63,7 @@ from kivy import utils as kivy_utils from kivy.config import Config as KivyConfig from .cagou_widget import CagouWidget +from .share_widget import ShareWidget from . import widgets_handler from .common import IconButton from . import menu @@ -74,6 +73,11 @@ import cagou import cagou.plugins import cagou.kv + + +log = logging.getLogger(__name__) + + try: from plyer import notification except ImportError: @@ -85,28 +89,20 @@ if kivy_utils.platform == "android": import socket + from .platform_ import android + android.init_platform() +else: + # we don't want multi-touch emulation with mouse - # FIXME: move to separate android module - # sys.platform is "linux" on android by default - # so we change it to allow backend to detect android - sys.platform = "android" - C.PLUGIN_EXT = 'pyc' - SOCKET_DIR = "/data/data/org.salutatoi.cagou/" - SOCKET_FILE = ".socket" - STATE_RUNNING = b"running" - STATE_PAUSED = b"paused" - STATE_STOPPED = b"stopped" + # this option doesn't make sense on Android and cause troubles, so we only activate + # it for other platforms (cf. https://github.com/kivy/kivy/issues/6229) + KivyConfig.set('input', 'mouse', 'mouse,disable_multitouch') ## General Configuration ## # we want white background by default Window.clearcolor = (1, 1, 1, 1) -# we don't want multi-touch emulation with mouse -if sys.platform != 'android': - # this option doesn't make sense on Android and cause troubles - # cf. https://github.com/kivy/kivy/issues/6229 - KivyConfig.set('input', 'mouse', 'mouse,disable_multitouch') class NotifsIcon(IconButton): @@ -347,21 +343,21 @@ # try to call a bridge method in on_pause method, the call data # is not written before the actual pause s = self._frontend_status_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - s.connect(os.path.join(SOCKET_DIR, SOCKET_FILE)) - s.sendall(STATE_RUNNING) + s.connect(os.path.join(android.SOCKET_DIR, android.SOCKET_FILE)) + s.sendall(android.STATE_RUNNING) def on_pause(self): self.host.sync = False - self._frontend_status_socket.sendall(STATE_PAUSED) + self._frontend_status_socket.sendall(android.STATE_PAUSED) return True def on_resume(self): - self._frontend_status_socket.sendall(STATE_RUNNING) + self._frontend_status_socket.sendall(android.STATE_RUNNING) self.host.sync = True def on_stop(self): if sys.platform == "android": - self._frontend_status_socket.sendall(STATE_STOPPED) + self._frontend_status_socket.sendall(android.STATE_STOPPED) self._frontend_status_socket.close() def key_input(self, window, key, scancode, codepoint, modifier): @@ -371,9 +367,9 @@ elif key == 292: # F11: full screen if not Window.fullscreen: - window.fullscreen = 'auto' + Window.fullscreen = 'auto' else: - window.fullscreen = False + Window.fullscreen = False return True elif key == 109 and modifier == ['alt']: # M-m we hide/show menu @@ -403,13 +399,6 @@ if bridge_name == 'embedded': from sat.core import sat_main self.sat = sat_main.SAT() - if sys.platform == 'android': - from jnius import autoclass - service = autoclass('org.salutatoi.cagou.ServiceBackend') - mActivity = autoclass('org.kivy.android.PythonActivity').mActivity - argument = '' - service.start(mActivity, argument) - self.service = service bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge') if bridge_module is None: @@ -458,6 +447,9 @@ patches.apply() log.warning("SSL certificate validation is disabled, this is unsecure!") + if sys.platform == 'android': + android.host_init(self) + @property def visible_widgets(self): for w_list in self._visible_widgets.values(): @@ -954,6 +946,8 @@ def closeUI(self): self.app.root.show() + screen = self.app.root._manager.get_screen("extra") + screen.clear_widgets() def getDefaultAvatar(self, entity=None): return self.app.default_avatar @@ -984,6 +978,13 @@ else: log.warning(_("unknown dialog type: {dialog_type}").format(dialog_type=type)) + def share(self, media_type, data): + share_wid = ShareWidget(media_type=media_type, data=data) + try: + self.showExtraUI(share_wid) + except Exception as e: + log.error(e) + self.closeUI() def desktop_notif(self, message, title='', duration=5000): global notification @@ -998,3 +999,15 @@ log.warning(_("Can't use notifications, disabling: {msg}").format( msg = e)) notification = None + + def getAncestorWidget(self, wid, cls): + """Retrieve an ancestor of given class + + @param wid(Widget): current widget + @param cls(type): class of the ancestor to retrieve + @return (Widget, None): found instance or None + """ + parent = wid.parent + while parent and parent.__class__ != cls: + parent = parent.parent + return parent
--- a/cagou/core/common.py Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/core/common.py Fri Dec 06 13:23:03 2019 +0100 @@ -25,6 +25,7 @@ from kivy.uix.behaviors import ButtonBehavior from kivy.uix.behaviors import ToggleButtonBehavior from kivy.uix.boxlayout import BoxLayout +from kivy.metrics import dp from cagou.core.constants import Const as C from kivy import properties from cagou import G @@ -40,6 +41,17 @@ pass +class Avatar(Image): + pass + + +class ContactItem(BoxLayout): + base_width = dp(150) + profile = properties.StringProperty() + data = properties.DictProperty() + jid = properties.StringProperty('') + + class JidItem(BoxLayout): bg_color = properties.ListProperty([0.2, 0.2, 0.2, 1]) color = properties.ListProperty([1, 1, 1, 1])
--- a/cagou/core/constants.py Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/core/constants.py Fri Dec 06 13:23:03 2019 +0100 @@ -20,6 +20,8 @@ from sat_frontends.quick_frontend import constants import cagou +# Kivy must not be imported here due to log hijacking see core/kivy_hack.py + class Const(constants.Const): APP_NAME = "Cagou" @@ -30,6 +32,8 @@ ICON_SIZES = ('small', 'medium') # small = 32, medium = 44 DEFAULT_WIDGET_ICON = '{media}/misc/black.png' + BTN_HEIGHT = '35dp' + PLUG_TYPE_WID = 'wid' PLUG_TYPE_TRANSFER = 'transfer'
--- a/cagou/core/menu.py Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/core/menu.py Fri Dec 06 13:23:03 2019 +0100 @@ -312,9 +312,10 @@ self._closeUI(wid) self.callback( file_path, - cleaning_cb, transfer_type = (C.TRANSFER_UPLOAD - if self.ids['upload_btn'].state == "down" else C.TRANSFER_SEND)) + if self.ids['upload_btn'].state == "down" else C.TRANSFER_SEND), + cleaning_cb=cleaning_cb, + ) wid = plug_info['factory'](plug_info, onTransferCb, self.cancel_cb,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cagou/core/platform_/android.py Fri Dec 06 13:23:03 2019 +0100 @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016-2019 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/>. + +import sys +from jnius import autoclass, cast +from android import activity +from sat.core import log as logging +from cagou.core.constants import Const as C +from cagou import G +from kivy.clock import Clock + + +log = logging.getLogger(__name__) + +service = autoclass('org.salutatoi.cagou.ServiceBackend') +mActivity = autoclass('org.kivy.android.PythonActivity').mActivity +ImagesMedia = autoclass('android.provider.MediaStore$Images$Media') +AudioMedia = autoclass('android.provider.MediaStore$Audio$Media') +VideoMedia = autoclass('android.provider.MediaStore$Video$Media') +DATA = '_data' + + +STATE_RUNNING = b"running" +STATE_PAUSED = b"paused" +STATE_STOPPED = b"stopped" +SOCKET_DIR = "/data/data/org.salutatoi.cagou/" +SOCKET_FILE = ".socket" + +# cache for callbacks to run when profile is plugged +cache = [] + + +def on_new_intent(intent): + log.debug("on_new_intent") + Intent = autoclass('android.content.Intent') + action = intent.getAction(); + intent_type = intent.getType(); + if action == "android.intent.action.SEND": + # we have receiving data to share, we parse the intent data + # and show the share widget + data = {} + text = intent.getStringExtra(Intent.EXTRA_TEXT) + if text is not None: + data['text'] = text + item = intent.getParcelableExtra(Intent.EXTRA_STREAM) + if item is not None: + uri = cast('android.net.Uri', item) + data['uri'] = uri.toString() + path = getPathFromUri(uri) + if path is not None: + data['path'] = path + else: + uri = None + path = None + + Clock.schedule_once(lambda *args: G.host.share(intent_type, data), 0) + else: + text = None + uri = None + path = None + + msg = (f"NEW INTENT RECEIVED\n" + f"type: {intent_type}\n" + f"action: {action}\n" + f"text: {text}\n" + f"uri: {uri}\n" + f"path: {path}") + + log.debug(msg) + + +def onProfilePlugged(profile): + log.debug("ANDROID profilePlugged") + global cache + for method, *args in cache: + method(*args) + del cache + G.host.removeListener("profilePlugged", onProfilePlugged) + + +def init_platform(): + # sys.platform is "linux" on android by default + # so we change it to allow backend to detect android + sys.platform = "android" + C.PLUGIN_EXT = 'pyc' + + +def host_init(host): + argument = '' + service.start(mActivity, argument) + + activity.bind(on_new_intent=on_new_intent) + cache.append((on_new_intent, mActivity.getIntent())) + host.addListener('profilePlugged', onProfilePlugged) + + +def getPathFromUri(uri): + cursor = mActivity.getContentResolver().query(uri, None, None, None, None) + if cursor is None: + return uri.getPath() + else: + cursor.moveToFirst() + # FIXME: using DATA is not recommended (and DATA is deprecated) + # we should read directly the file with + # ContentResolver#openFileDescriptor(Uri, String) + col_idx = cursor.getColumnIndex(DATA); + if col_idx == -1: + return uri.getPath() + return cursor.getString(col_idx)
--- a/cagou/kv/common.kv Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/kv/common.kv Fri Dec 06 13:23:03 2019 +0100 @@ -15,6 +15,28 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. +<ContactItem>: + size_hint: None, None + width: self.base_width + height: self.minimum_height + orientation: 'vertical' + Avatar: + id: avatar + size_hint: 1, None + height: dp(60) + source: root.data.get('avatar') or app.default_avatar + allow_stretch: True + Label: + id: jid_label + size_hint: None, None + text_size: root.base_width, None + size: self.texture_size + text: root.jid + bold: True + valign: 'middle' + halign: 'center' + + <JidItem>: size_hint: 1, None height: dp(68)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cagou/kv/share_widget.kv Fri Dec 06 13:23:03 2019 +0100 @@ -0,0 +1,118 @@ +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016-2019 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/>. + +#:import _ sat.core.i18n._ +#:import C cagou.core.constants.Const + + +<ShareWidget>: + preview_box: preview_box + layout: layout + orientation: 'vertical' + Label: + size_hint: 1, None + text_size: self.size + halign: 'center' + text: _("share") + height: self.font_size + dp(5) + bold: True + font_size: '35sp' + BoxLayout: + id: preview_box + size_hint: 1, None + height: self.minimum_height + orientation: 'vertical' + text: str(root.data) + Label: + size_hint: 1, None + text_size: self.size + halign: 'center' + text: _("with") + height: self.font_size + dp(5) + bold: True + font_size: '25sp' + ScrollView: + StackLayout: + id: layout + size_hint: 1, None + height: self.minimum_height + spacing: 0 + Button: + size_hint: 1, None + height: C.BTN_HEIGHT + text: _("cancel") + on_press: app.host.closeUI() + + +<TextPreview>: + size_hint: 1, None + height: min(data.height, dp(100)) + ScrollView + Label: + id: data + size_hint: 1, None + text: root.text + text_size: self.width, None + size: self.texture_size + font_size: sp(20) + padding_x: dp(10) + padding_y: dp(5) + halign: 'center' + canvas.before: + Color: + rgba: 0.95, 0.95, 0.95, 1 + Rectangle: + pos: self.pos + size: self.size + +<ImagePreview>: + reduce_layout: reduce_layout + reduce_checkbox: reduce_checkbox + size_hint: 1, None + height: dp(120) + orientation: "vertical" + Image: + source: root.path + BoxLayout + id: reduce_layout + size_hint: 1, None + padding_y: None + opacity: 0 + height: 0 + Widget: + CheckBox: + id: reduce_checkbox + size_hint: None, 1 + width: dp(20) + active: True + Label: + size_hint: None, None + text: _("reduce image size") + text_size: None, None + size: self.texture_size + padding_x: dp(10) + font_size: sp(15) + Widget: + +<GenericPreview>: + size_hint: 1, None + height: dp(100) + Widget: + SymbolLabel: + symbol: "doc" + text: root.path + Widget: +
--- a/cagou/plugins/plugin_wid_chat.kv Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/plugins/plugin_wid_chat.kv Fri Dec 06 13:23:03 2019 +0100 @@ -103,6 +103,7 @@ <Chat>: messages_widget: messages_widget + message_input: message_input ScrollView: scroll_y: 0 do_scroll_x: False
--- a/cagou/plugins/plugin_wid_chat.py Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/plugins/plugin_wid_chat.py Fri Dec 06 13:23:03 2019 +0100 @@ -183,7 +183,7 @@ chat = properties.ObjectProperty() def on_release(self, *args): - menu.TransferMenu(callback=self.chat.onTransferOK).show(self) + menu.TransferMenu(callback=self.chat.transferFile).show(self) class ExtraMenu(DropDown): @@ -664,11 +664,14 @@ profile_key=profile ) - def onTransferOK(self, file_path, cleaning_cb, transfer_type): - if transfer_type == C.TRANSFER_UPLOAD: + if cleaning_cb is not None: + cleaning_cb() + + def transferFile(self, file_path, transfer_type=C.TRANSFER_UPLOAD, cleaning_cb=None): + if transfer_type == C.TRANSFER_UPLOAD: G.host.bridge.fileUpload( - file_path, + str(file_path), "", "", {"ignore_tls_errors": C.boolConst(not G.host.tls_validation)}, @@ -691,7 +694,7 @@ jid_ = self.target if not jid_.resource: jid_ = G.host.contact_lists[self.profile].getFullJid(jid_) - G.host.bridge.fileSend(str(jid_), file_path, "", "", {}, + G.host.bridge.fileSend(str(jid_), str(file_path), "", "", {}, profile=self.profile) # TODO: notification of sending/failing else:
--- a/cagou/plugins/plugin_wid_contact_list.kv Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/plugins/plugin_wid_contact_list.kv Fri Dec 06 13:23:03 2019 +0100 @@ -74,28 +74,6 @@ Widget: -<ContactItem>: - size_hint: None, None - width: self.base_width - height: self.minimum_height - orientation: 'vertical' - Avatar: - id: avatar - size_hint: 1, None - height: dp(60) - source: root.data.get('avatar', app.default_avatar) - allow_stretch: True - Label: - id: jid_label - size_hint: None, None - text_size: root.base_width, None - size: self.texture_size - text: root.jid - bold: True - valign: 'middle' - halign: 'center' - - <ContactList>: float_layout: float_layout layout: layout
--- a/cagou/plugins/plugin_wid_contact_list.py Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/plugins/plugin_wid_contact_list.py Fri Dec 06 13:23:03 2019 +0100 @@ -21,16 +21,14 @@ from sat.core import log as logging log = logging.getLogger(__name__) from cagou.core.constants import Const as C +from ..core.common import ContactItem from sat.core.i18n import _ from sat_frontends.quick_frontend.quick_contact_list import QuickContactList from sat_frontends.tools import jid -from kivy.uix.boxlayout import BoxLayout from cagou.core.utils import FilterBehavior from cagou.core.menu import SideMenu, TouchMenuBehaviour, TouchMenuItemBehaviour -from kivy.metrics import dp from kivy import properties from cagou.core import cagou_widget -from cagou.core import image from cagou import G from functools import partial import bisect @@ -99,19 +97,7 @@ message=_("error while trying to remove contact: {msg}"))) - -class Avatar(image.Image): - pass - - -class ContactItem(TouchMenuItemBehaviour, BoxLayout): - base_width = dp(150) - profile = properties.StringProperty() - data = properties.DictProperty() - jid = properties.StringProperty('') - - def __init__(self, **kwargs): - super(ContactItem, self).__init__(**kwargs) +class CLContactItem(TouchMenuItemBehaviour, ContactItem): def do_item_action(self, touch): assert self.profile @@ -162,14 +148,14 @@ ) def _addContactItem(self, bare_jid, profile): - """Create a new ContactItem instance, and add it + """Create a new CLContactItem instance, and add it item will be added in a sorted position @param bare_jid(jid.JID): entity bare JID @param profile(unicode): profile where the contact is """ data = G.host.contact_lists[profile].getItem(bare_jid) - wid = ContactItem(profile=profile, data=data, jid=bare_jid, main_wid=self) + wid = CLContactItem(profile=profile, data=data, jid=bare_jid, main_wid=self) child_jids = [c.jid for c in reversed(self.layout.children)] idx = bisect.bisect_right(child_jids, bare_jid) self.layout.add_widget(wid, -idx) @@ -181,7 +167,7 @@ log.debug("full contact list update") self.layout.clear_widgets() for bare_jid, data in self.items_sorted.items(): - wid = ContactItem(profile=profile, data=data, jid=bare_jid, main_wid=self) + wid = CLContactItem(profile=profile, data=data, jid=bare_jid, main_wid=self) self.layout.add_widget(wid) self._wid_map[(profile, bare_jid)] = wid elif type_ == C.UPDATE_MODIFY:
--- a/cagou/plugins/plugin_wid_remote.py Fri Dec 06 13:23:03 2019 +0100 +++ b/cagou/plugins/plugin_wid_remote.py Fri Dec 06 13:23:03 2019 +0100 @@ -19,7 +19,6 @@ from sat.core import log as logging -log = logging.getLogger(__name__) from sat.core.i18n import _ from sat_frontends.quick_frontend import quick_widgets from cagou.core import cagou_widget @@ -40,6 +39,8 @@ from functools import partial +log = logging.getLogger(__name__) + PLUGIN_INFO = { "name": _("remote control"), "main": "RemoteControl",