# HG changeset patch # User Goffi # Date 1483051627 -3600 # Node ID 5d2289127bb7b0275f2d853bcf091e2f3047c22f # Parent 641678ddc26c4231b9100116381083accc40f836 menu (upload): better menu using dedicated widget: upload menu now use a decicated widget instead of context menu. The menu take half the size of the main window, and show each upload option as an icon. Use can select upload or P2P sending, and a short text message explains how the file will be transmitted. diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/core/cagou_main.py --- a/src/cagou/core/cagou_main.py Thu Dec 29 23:47:04 2016 +0100 +++ b/src/cagou/core/cagou_main.py Thu Dec 29 23:47:07 2016 +0100 @@ -295,7 +295,7 @@ self.app.default_avatar = os.path.join(self.media_dir, "misc/default_avatar.png") self.app.icon = os.path.join(self.media_dir, "icons/muchoslava/png/cagou_profil_bleu_96.png") self._plg_wids = [] # main widgets plugins - self._plg_wids_upload = [] # upload widgets plugins + self._plg_wids_transfer = [] # transfer widgets plugins self._import_plugins() self._visible_widgets = {} # visible widgets by classes @@ -357,13 +357,13 @@ main_cls = plugin_info['main'] return self.widgets.getOrCreateWidget(main_cls, target, on_new_widget=None, profiles=iter(self.profiles)) - def _defaultFactoryUpload(self, plugin_info, callback, cancel_cb, profiles): - """default factory used to create upload widgets instances + def _defaultFactoryTransfer(self, plugin_info, callback, cancel_cb, profiles): + """default factory used to create transfer widgets instances @param plugin_info(dict): plugin datas - @param callback(callable): method to call with path to file to upload - @param cancel_cb(callable): call when upload is cancelled - upload widget must be used as first argument + @param callback(callable): method to call with path to file to transfer + @param cancel_cb(callable): call when transfer is cancelled + transfer widget must be used as first argument @param profiles(iterable): list of profiles None if not specified """ @@ -387,7 +387,7 @@ plug_lst = [os.path.splitext(p)[0] for p in map(os.path.basename, glob.glob(os.path.join(plugins_path, plugin_glob)))] imported_names_main = set() # used to avoid loading 2 times plugin with same import name - imported_names_upload = set() + imported_names_transfer = set() for plug in plug_lst: plugin_path = 'cagou.plugins.' + plug @@ -402,9 +402,9 @@ if plugin_type == C.PLUG_TYPE_WID: imported_names = imported_names_main default_factory = self._defaultFactoryMain - elif plugin_type == C.PLUG_TYPE_UPLOAD: - imported_names = imported_names_upload - default_factory = self._defaultFactoryUpload + elif plugin_type == C.PLUG_TYPE_TRANSFER: + imported_names = imported_names_transfer + default_factory = self._defaultFactoryTransfer else: log.error(u"unknown plugin type {type_} for plugin {file_}, skipping".format( type_ = plugin_type, @@ -484,7 +484,7 @@ # we want widgets sorted by names self._plg_wids.sort(key=lambda p: p['name'].lower()) - self._plg_wids_upload.sort(key=lambda p: p['name'].lower()) + self._plg_wids_transfer.sort(key=lambda p: p['name'].lower()) if self.default_wid is None: # we have no selector widget, we use the first widget as default @@ -493,8 +493,8 @@ def _getPluginsSet(self, type_): if type_ == C.PLUG_TYPE_WID: return self._plg_wids - elif type_ == C.PLUG_TYPE_UPLOAD: - return self._plg_wids_upload + elif type_ == C.PLUG_TYPE_TRANSFER: + return self._plg_wids_transfer else: raise KeyError(u"{} plugin type is unknown".format(type_)) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/core/constants.py --- a/src/cagou/core/constants.py Thu Dec 29 23:47:04 2016 +0100 +++ b/src/cagou/core/constants.py Thu Dec 29 23:47:07 2016 +0100 @@ -29,4 +29,7 @@ DEFAULT_WIDGET_ICON = u'{media}/misc/black.png' PLUG_TYPE_WID = u'wid' - PLUG_TYPE_UPLOAD = u'upload' + PLUG_TYPE_TRANSFER = u'transfer' + + TRANSFER_UPLOAD = u"upload" + TRANSFER_SEND = u"send" diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/core/menu.py --- a/src/cagou/core/menu.py Thu Dec 29 23:47:04 2016 +0100 +++ b/src/cagou/core/menu.py Thu Dec 29 23:47:07 2016 +0100 @@ -159,55 +159,78 @@ about.open() -class UploadMenu(contextmenu.ContextMenu): - """upload menu which handle display and callbacks""" - # callback will be called with path to file to upload +class TransferItem(BoxLayout): + plug_info = properties.DictProperty() + + def on_touch_up(self, touch): + if not self.collide_point(*touch.pos): + return super(TransferItem, self).on_touch_up(touch) + else: + transfer_menu = self.parent + while not isinstance(transfer_menu, TransferMenu): + transfer_menu = transfer_menu.parent + transfer_menu.do_callback(self.plug_info) + return True + + +class TransferMenu(BoxLayout): + """transfer menu which handle display and callbacks""" + # callback will be called with path to file to transfer callback = properties.ObjectProperty() # cancel callback need to remove the widget for UI # will be called with the widget to remove as argument cancel_cb = properties.ObjectProperty() - # profiles if set will be sent to upload widget, may be used to get specific files + # profiles if set will be sent to transfer widget, may be used to get specific files profiles = properties.ObjectProperty() + transfer_txt = _(u"Beware! The file will be sent to your server and stay unencrypted there\nServer admin(s) can see the file, and they choose how, when and if it will be deleted") + send_txt = _(u"The file will be sent unencrypted directly to your contact (without transiting by the server), except in some cases") + items_layout = properties.ObjectProperty() def __init__(self, **kwargs): - super(UploadMenu, self).__init__(**kwargs) + super(TransferMenu, self).__init__(**kwargs) if self.cancel_cb is None: - self.cancel_cb = self.onUploadCancelled + self.cancel_cb = self.onTransferCancelled if self.profiles is None: self.profiles = iter(G.host.profiles) - for plug_info in G.host.getPluggedWidgets(type_=C.PLUG_TYPE_UPLOAD): - item = contextmenu.ContextMenuTextItem( - text = plug_info['name'], - on_release = lambda dummy, plug_info=plug_info: self.do_callback(plug_info) + for plug_info in G.host.getPluggedWidgets(type_=C.PLUG_TYPE_TRANSFER): + item = TransferItem( + plug_info = plug_info ) - self.add_widget(item) + self.items_layout.add_widget(item) def show(self, caller_wid=None): - if caller_wid is not None: - pos = caller_wid.x, caller_wid.top + self.get_height() + self.visible = True + G.host.app.root.add_widget(self) + + def on_touch_down(self, touch): + # we remove the menu if we click outside + # else we want to handle the event, but not + # transmit it to parents + if not self.collide_point(*touch.pos): + self.parent.remove_widget(self) else: - pos = G.host.app.root_window.mouse_pos - super(UploadMenu, self).show(*pos) + return super(TransferMenu, self).on_touch_down(touch) + return True def _closeUI(self, wid): G.host.closeUI() - def onUploadCancelled(self, wid, cleaning_cb=None): + def onTransferCancelled(self, wid, cleaning_cb=None): self._closeUI(wid) if cleaning_cb is not None: cleaning_cb() def do_callback(self, plug_info): - self.hide() + self.parent.remove_widget(self) if self.callback is None: - log.warning(u"UploadMenu callback is not set") + log.warning(u"TransferMenu callback is not set") else: wid = None external = plug_info.get('external', False) - def onUploadCb(file_path, cleaning_cb=None): + def onTransferCb(file_path, cleaning_cb=None): if not external: self._closeUI(wid) self.callback(file_path, cleaning_cb) - wid = plug_info['factory'](plug_info, onUploadCb, self.cancel_cb, self.profiles) + wid = plug_info['factory'](plug_info, onTransferCb, self.cancel_cb, self.profiles) if not external: G.host.showExtraUI(wid) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/kv/menu.kv --- a/src/cagou/kv/menu.kv Thu Dec 29 23:47:04 2016 +0100 +++ b/src/cagou/kv/menu.kv Thu Dec 29 23:47:07 2016 +0100 @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +#:import _ sat.core.i18n._ : text_size: self.size @@ -34,5 +35,55 @@ : cancel_handler_widget: self.parent -: - cancel_handler_widget: self.parent if self.parent else self.orig_parent +: + items_layout: items_layout + orientation: "vertical" + pos_hint: {"top": 0.5} + size_hint: 1, 0.5 + canvas.before: + Color: + rgba: 0, 0, 0, 1 + Rectangle: + pos: self.pos + size: self.size + BoxLayout: + size_hint: 1, None + height: dp(50) + ToggleButton: + id: upload_btn + text: _(u"upload") + group: "transfer" + state: "down" + ToggleButton: + id: send_btn + text: _(u"send") + group: "transfer" + Label: + size_hint: 1, 0.3 + text: root.transfer_txt if upload_btn.state == 'down' else root.send_txt + text_size: self.size + halign: 'center' + valign: 'top' + ScrollView: + do_scroll_x: False + StackLayout: + size_hint: 1, None + padding: 20, 0 + spacing: 15, 5 + id: items_layout + +: + orientation: "vertical" + size_hint: None, None + size: dp(50), dp(90) + IconButton: + source: root.plug_info['icon_medium'] + allow_stretch: True + size_hint: 1, None + height: dp(50) + Label: + text: root.plug_info['name'] + text_size: self.size + halign: "center" + valign: "top" + diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_transfer_android_gallery.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cagou/plugins/plugin_transfer_android_gallery.py Thu Dec 29 23:47:07 2016 +0100 @@ -0,0 +1,95 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016 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 . + + +from sat.core import log as logging +log = logging.getLogger(__name__) +from sat.core.i18n import _ +import sys +import tempfile +import os +import os.path +if sys.platform=="android": + from jnius import autoclass + from android import activity, mActivity + + Intent = autoclass('android.content.Intent') + OpenableColumns = autoclass('android.provider.OpenableColumns') + PHOTO_GALLERY = 1 + RESULT_OK = -1 + + + +PLUGIN_INFO = { + "name": _(u"photo"), + "main": "AndroidGallery", + "platforms": ('android',), + "external": True, + "description": _(u"upload a photo from photo gallery"), + "icon_medium": u"{media}/icons/muchoslava/png/gallery_50.png", +} + + +class AndroidGallery(object): + + def __init__(self, callback, cancel_cb): + self.callback = callback + self.cancel_cb = cancel_cb + activity.bind(on_activity_result=self.on_activity_result) + intent = Intent() + intent.setType('image/*') + intent.setAction(Intent.ACTION_GET_CONTENT) + mActivity.startActivityForResult(intent, PHOTO_GALLERY); + + def on_activity_result(self, requestCode, resultCode, data): + # TODO: move file dump to a thread or use async callbacks during file writting + if requestCode == PHOTO_GALLERY and resultCode == RESULT_OK: + if data is None: + log.warning(u"No data found in activity result") + self.cancel_cb(self, None) + return + uri = data.getData() + + # we get filename in the way explained at https://developer.android.com/training/secure-file-sharing/retrieve-info.html + cursor = mActivity.getContentResolver().query(uri, None, None, None, None ) + name_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + filename = cursor.getString(name_idx) + + # we save data in a temporary file that we send to callback + # the file will be removed once upload is done (or if an error happens) + input_stream = mActivity.getContentResolver().openInputStream(uri) + tmp_dir = tempfile.mkdtemp() + tmp_file = os.path.join(tmp_dir, filename) + def cleaning(): + os.unlink(tmp_file) + os.rmdir(tmp_dir) + log.debug(u'temporary file cleaned') + buff = bytearray(4096) + with open(tmp_file, 'wb') as f: + while True: + ret = input_stream.read(buff, 0, 4096) + if ret != -1: + f.write(buff) + else: + break + input_stream.close() + self.callback(tmp_file, cleaning) + else: + self.cancel_cb(self, None) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_transfer_android_photo.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cagou/plugins/plugin_transfer_android_photo.py Thu Dec 29 23:47:07 2016 +0100 @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016 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 . + + +from sat.core import log as logging +log = logging.getLogger(__name__) +from sat.core.i18n import _ +import sys +import os +import os.path +import time +if sys.platform == "android": + from plyer import camera + from jnius import autoclass + Environment = autoclass('android.os.Environment') +else: + import tempfile + + +PLUGIN_INFO = { + "name": _(u"take photo"), + "main": "AndroidPhoto", + "platforms": ('android',), + "external": True, + "description": _(u"upload a photo from photo application"), + "icon_medium": u"{media}/icons/muchoslava/png/camera_off_50.png", +} + + +class AndroidPhoto(object): + + def __init__(self, callback, cancel_cb): + self.callback = callback + self.cancel_cb = cancel_cb + filename = time.strftime("%Y-%m-%d_%H:%M:%S.jpg", time.gmtime()) + tmp_dir = self.getTmpDir() + tmp_file = os.path.join(tmp_dir, filename) + log.debug(u"Picture will be saved to {}".format(tmp_file)) + camera.take_picture(tmp_file, self.callback) + # we don't delete the file, as it is nice to keep it locally + + def getTmpDir(self): + if sys.platform == "android": + dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + return dcim_path + else: + return tempfile.mkdtemp() diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_transfer_android_video.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cagou/plugins/plugin_transfer_android_video.py Thu Dec 29 23:47:07 2016 +0100 @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016 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 . + + +from sat.core import log as logging +log = logging.getLogger(__name__) +from sat.core.i18n import _ +import sys +import os +import os.path +import time +if sys.platform == "android": + from plyer import camera + from jnius import autoclass + Environment = autoclass('android.os.Environment') +else: + import tempfile + + +PLUGIN_INFO = { + "name": _(u"take video"), + "main": "AndroidVideo", + "platforms": ('android',), + "external": True, + "description": _(u"upload a video from video application"), + "icon_medium": u"{media}/icons/muchoslava/png/film_camera_off_50.png", +} + + +class AndroidVideo(object): + + def __init__(self, callback, cancel_cb): + self.callback = callback + self.cancel_cb = cancel_cb + filename = time.strftime("%Y-%m-%d_%H:%M:%S.mpg", time.gmtime()) + tmp_dir = self.getTmpDir() + tmp_file = os.path.join(tmp_dir, filename) + log.debug(u"Video will be saved to {}".format(tmp_file)) + camera.take_video(tmp_file, self.callback) + # we don't delete the file, as it is nice to keep it locally + + def getTmpDir(self): + if sys.platform == "android": + dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + return dcim_path + else: + return tempfile.mkdtemp() diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_transfer_file.kv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cagou/plugins/plugin_transfer_file.kv Thu Dec 29 23:47:07 2016 +0100 @@ -0,0 +1,35 @@ +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016 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 . + +#:import expanduser os.path.expanduser +#:import platform kivy.utils.platform + + +: + orientation: "vertical" + FileChooserListView: + id: filechooser + rootpath: "/" if platform == 'android' else expanduser('~') + Button: + text: "choose" + size_hint: 1, None + height: dp(50) + on_release: root.onTransmitOK(filechooser) + Button: + text: "cancel" + size_hint: 1, None + height: dp(50) + on_release: root.cancel_cb(root) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_transfer_file.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cagou/plugins/plugin_transfer_file.py Thu Dec 29 23:47:07 2016 +0100 @@ -0,0 +1,43 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016 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 . + + +from sat.core import log as logging +log = logging.getLogger(__name__) +from sat.core.i18n import _ +from kivy.uix.boxlayout import BoxLayout +from kivy import properties + + +PLUGIN_INFO = { + "name": _(u"file"), + "main": "FileTransmitter", + "description": _(u"transmit a local file"), + "icon_medium": u"{media}/icons/muchoslava/png/fichier_50.png", +} + + +class FileTransmitter(BoxLayout): + callback = properties.ObjectProperty() + cancel_cb = properties.ObjectProperty() + + def onTransmitOK(self, filechooser): + if filechooser.selection: + file_path = filechooser.selection[0] + self.callback(file_path) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_transfer_voice.kv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cagou/plugins/plugin_transfer_voice.kv Thu Dec 29 23:47:07 2016 +0100 @@ -0,0 +1,71 @@ +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016 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 . + +#:import _ sat.core.i18n._ +#:import IconButton cagou.core.common.IconButton + + +: + orientation: "vertical" + counter: counter + Label: + size_hint: 1, 0.4 + text_size: self.size + halign: 'center' + valign: 'top' + text: _(u"Push the microphone button to start the record, then push it again to stop it.\nWhen you are satisfied, click on the transmit button") + Label: + id: counter + size_hint: 1, None + height: dp(60) + bold: True + font_size: sp(40) + text_size: self.size + text: u"{}:{:02}".format(root.time/60, root.time%60) + halign: 'center' + valign: 'middle' + BoxLayout: + size_hint: 1, None + height: dp(60) + Widget + IconButton: + source: app.expand("{media}/icons/muchoslava/png/") + ("micro_on_50.png" if root.recording else "micro_off_50.png") + allow_stretch: True + size_hint: None, None + size: dp(60), dp(60) + on_release: root.switchRecording() + IconButton: + opacity: 0 if root.recording or not root.time and not root.playing else 1 + source: app.expand("{media}/icons/muchoslava/png/") + ("stop_50.png" if root.playing else "play_50.png") + allow_stretch: True + size_hint: None, None + size: dp(60), dp(60) + on_release: root.playRecord() + Widget + Widget: + size_hint: 1, None + height: dp(50) + Button: + text: _("transmit") + size_hint: 1, None + height: dp(50) + on_release: root.callback(root.audio.file_path) + Button: + text: _("cancel") + size_hint: 1, None + height: dp(50) + on_release: root.cancel_cb(root) + Widget diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_transfer_voice.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cagou/plugins/plugin_transfer_voice.py Thu Dec 29 23:47:07 2016 +0100 @@ -0,0 +1,111 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Cagou: desktop/mobile frontend for Salut à Toi XMPP client +# Copyright (C) 2016 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 . + + +from sat.core import log as logging +log = logging.getLogger(__name__) +from sat.core.i18n import _ +from kivy.uix.boxlayout import BoxLayout +import sys +import time +from kivy.clock import Clock +from kivy import properties +if sys.platform == "android": + from plyer import audio + + +PLUGIN_INFO = { + "name": _(u"voice"), + "main": "VoiceRecorder", + "platforms": ["android"], + "description": _(u"transmit a voice record"), + "icon_medium": u"{media}/icons/muchoslava/png/micro_off_50.png", +} + + +class VoiceRecorder(BoxLayout): + callback = properties.ObjectProperty() + cancel_cb = properties.ObjectProperty() + recording = properties.BooleanProperty(False) + playing = properties.BooleanProperty(False) + time = properties.NumericProperty(0) + + def __init__(self, **kwargs): + super(VoiceRecorder, self).__init__(**kwargs) + self._started_at = None + self._counter_timer = None + self._play_timer = None + self.record_time = None + self.audio = audio + self.audio.file_path = "/sdcard/cagou_record.3gp" + + def _updateTimer(self, dt): + self.time = int(time.time() - self._started_at) + + def switchRecording(self): + if self.playing: + self._stopPlaying() + if self.recording: + try: + audio.stop() + except Exception as e: + # an exception can happen if record is pressed + # repeatedly in a short time (not a normal use) + log.warning(u"Exception on stop: {}".format(e)) + self._counter_timer.cancel() + self.time = self.time + 1 + else: + audio.start() + self._started_at = time.time() + self.time = 0 + self._counter_timer = Clock.schedule_interval(self._updateTimer, 1) + + self.recording = not self.recording + + def _stopPlaying(self, dummy=None): + if self.record_time is None: + log.error("_stopPlaying should no be called when record_time is None") + return + audio.stop() + self.playing = False + self.time = self.record_time + if self._counter_timer is not None: + self._counter_timer.cancel() + + def playRecord(self): + if self.recording: + return + if self.playing: + self._stopPlaying() + else: + try: + audio.play() + except Exception as e: + # an exception can happen in the same situation + # as for audio.stop() above (i.e. bad record) + log.warning(u"Exception on play: {}".format(e)) + self.time = 0 + return + + self.playing = True + self.record_time = self.time + Clock.schedule_once(self._stopPlaying, self.time + 1) + self._started_at = time.time() + self.time = 0 + self._counter_timer = Clock.schedule_interval(self._updateTimer, 0.5) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_upload_android_gallery.py --- a/src/cagou/plugins/plugin_upload_android_gallery.py Thu Dec 29 23:47:04 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Cagou: desktop/mobile frontend for Salut à Toi XMPP client -# Copyright (C) 2016 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 . - - -from sat.core import log as logging -log = logging.getLogger(__name__) -from sat.core.i18n import _ -import sys -import tempfile -import os -import os.path -if sys.platform=="android": - from jnius import autoclass - from android import activity, mActivity - - Intent = autoclass('android.content.Intent') - OpenableColumns = autoclass('android.provider.OpenableColumns') - PHOTO_GALLERY = 1 - RESULT_OK = -1 - - - -PLUGIN_INFO = { - "name": _(u"photo"), - "main": "AndroidGallery", - "platforms": ('android',), - "external": True, - "description": _(u"upload a photo from photo gallery"), -} - - -class AndroidGallery(object): - - def __init__(self, callback, cancel_cb): - self.callback = callback - self.cancel_cb = cancel_cb - activity.bind(on_activity_result=self.on_activity_result) - intent = Intent() - intent.setType('image/*') - intent.setAction(Intent.ACTION_GET_CONTENT) - mActivity.startActivityForResult(intent, PHOTO_GALLERY); - - def on_activity_result(self, requestCode, resultCode, data): - # TODO: move file dump to a thread or use async callbacks during file writting - if requestCode == PHOTO_GALLERY and resultCode == RESULT_OK: - if data is None: - log.warning(u"No data found in activity result") - self.cancel_cb(self, None) - return - uri = data.getData() - - # we get filename in the way explained at https://developer.android.com/training/secure-file-sharing/retrieve-info.html - cursor = mActivity.getContentResolver().query(uri, None, None, None, None ) - name_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - cursor.moveToFirst() - filename = cursor.getString(name_idx) - - # we save data in a temporary file that we send to callback - # the file will be removed once upload is done (or if an error happens) - input_stream = mActivity.getContentResolver().openInputStream(uri) - tmp_dir = tempfile.mkdtemp() - tmp_file = os.path.join(tmp_dir, filename) - def cleaning(): - os.unlink(tmp_file) - os.rmdir(tmp_dir) - log.debug(u'temporary file cleaned') - buff = bytearray(4096) - with open(tmp_file, 'wb') as f: - while True: - ret = input_stream.read(buff, 0, 4096) - if ret != -1: - f.write(buff) - else: - break - input_stream.close() - self.callback(tmp_file, cleaning) - else: - self.cancel_cb(self, None) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_upload_android_photo.py --- a/src/cagou/plugins/plugin_upload_android_photo.py Thu Dec 29 23:47:04 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Cagou: desktop/mobile frontend for Salut à Toi XMPP client -# Copyright (C) 2016 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 . - - -from sat.core import log as logging -log = logging.getLogger(__name__) -from sat.core.i18n import _ -import sys -import os -import os.path -import time -if sys.platform == "android": - from plyer import camera - from jnius import autoclass - Environment = autoclass('android.os.Environment') -else: - import tempfile - - -PLUGIN_INFO = { - "name": _(u"take photo"), - "main": "AndroidPhoto", - "platforms": ('android',), - "external": True, - "description": _(u"upload a photo from photo application"), -} - - -class AndroidPhoto(object): - - def __init__(self, callback, cancel_cb): - self.callback = callback - self.cancel_cb = cancel_cb - filename = time.strftime("%Y-%m-%d_%H:%M:%S.jpg", time.gmtime()) - tmp_dir = self.getTmpDir() - tmp_file = os.path.join(tmp_dir, filename) - log.debug(u"Picture will be saved to {}".format(tmp_file)) - camera.take_picture(tmp_file, self.callback) - # we don't delete the file, as it is nice to keep it locally - - def getTmpDir(self): - if sys.platform == "android": - dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() - return dcim_path - else: - return tempfile.mkdtemp() diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_upload_android_video.py --- a/src/cagou/plugins/plugin_upload_android_video.py Thu Dec 29 23:47:04 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Cagou: desktop/mobile frontend for Salut à Toi XMPP client -# Copyright (C) 2016 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 . - - -from sat.core import log as logging -log = logging.getLogger(__name__) -from sat.core.i18n import _ -import sys -import os -import os.path -import time -if sys.platform == "android": - from plyer import camera - from jnius import autoclass - Environment = autoclass('android.os.Environment') -else: - import tempfile - - -PLUGIN_INFO = { - "name": _(u"take video"), - "main": "AndroidVideo", - "platforms": ('android',), - "external": True, - "description": _(u"upload a video from video application"), -} - - -class AndroidVideo(object): - - def __init__(self, callback, cancel_cb): - self.callback = callback - self.cancel_cb = cancel_cb - filename = time.strftime("%Y-%m-%d_%H:%M:%S.mpg", time.gmtime()) - tmp_dir = self.getTmpDir() - tmp_file = os.path.join(tmp_dir, filename) - log.debug(u"Video will be saved to {}".format(tmp_file)) - camera.take_video(tmp_file, self.callback) - # we don't delete the file, as it is nice to keep it locally - - def getTmpDir(self): - if sys.platform == "android": - dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() - return dcim_path - else: - return tempfile.mkdtemp() diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_upload_file.kv --- a/src/cagou/plugins/plugin_upload_file.kv Thu Dec 29 23:47:04 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +0,0 @@ -# Cagou: desktop/mobile frontend for Salut à Toi XMPP client -# Copyright (C) 2016 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 . - -#:import expanduser os.path.expanduser -#:import platform kivy.utils.platform - - -: - orientation: "vertical" - FileChooserListView: - id: filechooser - rootpath: "/" if platform == 'android' else expanduser('~') - Button: - text: "upload" - size_hint: 1, None - height: dp(50) - on_release: root.onUploadOK(filechooser) - Button: - text: "cancel" - size_hint: 1, None - height: dp(50) - on_release: root.cancel_cb(root) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_upload_file.py --- a/src/cagou/plugins/plugin_upload_file.py Thu Dec 29 23:47:04 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Cagou: desktop/mobile frontend for Salut à Toi XMPP client -# Copyright (C) 2016 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 . - - -from sat.core import log as logging -log = logging.getLogger(__name__) -from sat.core.i18n import _ -from kivy.uix.boxlayout import BoxLayout -from kivy import properties - - -PLUGIN_INFO = { - "name": _(u"file"), - "main": "FileUploader", - "description": _(u"upload a local file"), -} - - -class FileUploader(BoxLayout): - callback = properties.ObjectProperty() - cancel_cb = properties.ObjectProperty() - - def onUploadOK(self, filechooser): - if filechooser.selection: - file_path = filechooser.selection[0] - self.callback(file_path) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_upload_voice.kv --- a/src/cagou/plugins/plugin_upload_voice.kv Thu Dec 29 23:47:04 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +0,0 @@ -# Cagou: desktop/mobile frontend for Salut à Toi XMPP client -# Copyright (C) 2016 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 . - -#:import _ sat.core.i18n._ -#:import IconButton cagou.core.common.IconButton - - -: - orientation: "vertical" - counter: counter - Label: - size_hint: 1, 0.4 - text_size: self.size - halign: 'center' - valign: 'top' - text: _(u"Push the microphone button to start the record, then push it again to stop it.\nWhen you are satisfied, click on the upload button") - Label: - id: counter - size_hint: 1, None - height: dp(60) - bold: True - font_size: sp(40) - text_size: self.size - text: u"{}:{:02}".format(root.time/60, root.time%60) - halign: 'center' - valign: 'middle' - BoxLayout: - size_hint: 1, None - height: dp(60) - Widget - IconButton: - source: app.expand("{media}/icons/muchoslava/png/") + ("micro_on_50.png" if root.recording else "micro_off_50.png") - allow_stretch: True - size_hint: None, None - size: dp(60), dp(60) - on_release: root.switchRecording() - IconButton: - opacity: 0 if root.recording or not root.time and not root.playing else 1 - source: app.expand("{media}/icons/muchoslava/png/") + ("stop_50.png" if root.playing else "play_50.png") - allow_stretch: True - size_hint: None, None - size: dp(60), dp(60) - on_release: root.playRecord() - Widget - Widget: - size_hint: 1, None - height: dp(50) - Button: - text: "upload" - size_hint: 1, None - height: dp(50) - on_release: root.callback(root.audio.file_path) - Button: - text: "cancel" - size_hint: 1, None - height: dp(50) - on_release: root.cancel_cb(root) - Widget diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_upload_voice.py --- a/src/cagou/plugins/plugin_upload_voice.py Thu Dec 29 23:47:04 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Cagou: desktop/mobile frontend for Salut à Toi XMPP client -# Copyright (C) 2016 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 . - - -from sat.core import log as logging -log = logging.getLogger(__name__) -from sat.core.i18n import _ -from kivy.uix.boxlayout import BoxLayout -import sys -import time -from kivy.clock import Clock -from kivy import properties -if sys.platform == "android": - from plyer import audio - - -PLUGIN_INFO = { - "name": _(u"voice"), - "main": "VoiceRecorder", - "platforms": ["android"], - "description": _(u"upload a voice record"), -} - - -class VoiceRecorder(BoxLayout): - callback = properties.ObjectProperty() - cancel_cb = properties.ObjectProperty() - recording = properties.BooleanProperty(False) - playing = properties.BooleanProperty(False) - time = properties.NumericProperty(0) - - def __init__(self, **kwargs): - super(VoiceRecorder, self).__init__(**kwargs) - self._started_at = None - self._counter_timer = None - self._play_timer = None - self.record_time = None - self.audio = audio - self.audio.file_path = "/sdcard/cagou_record.3gp" - - def _updateTimer(self, dt): - self.time = int(time.time() - self._started_at) - - def switchRecording(self): - if self.playing: - self._stopPlaying() - if self.recording: - try: - audio.stop() - except Exception as e: - # an exception can happen if record is pressed - # repeatedly in a short time (not a normal use) - log.warning(u"Exception on stop: {}".format(e)) - self._counter_timer.cancel() - self.time = self.time + 1 - else: - audio.start() - self._started_at = time.time() - self.time = 0 - self._counter_timer = Clock.schedule_interval(self._updateTimer, 1) - - self.recording = not self.recording - - def _stopPlaying(self, dummy=None): - if self.record_time is None: - log.error("_stopPlaying should no be called when record_time is None") - return - audio.stop() - self.playing = False - self.time = self.record_time - if self._counter_timer is not None: - self._counter_timer.cancel() - - def playRecord(self): - if self.recording: - return - if self.playing: - self._stopPlaying() - else: - try: - audio.play() - except Exception as e: - # an exception can happen in the same situation - # as for audio.stop() above (i.e. bad record) - log.warning(u"Exception on play: {}".format(e)) - self.time = 0 - return - - self.playing = True - self.record_time = self.time - Clock.schedule_once(self._stopPlaying, self.time + 1) - self._started_at = time.time() - self.time = 0 - self._counter_timer = Clock.schedule_interval(self._updateTimer, 0.5) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_wid_chat.kv --- a/src/cagou/plugins/plugin_wid_chat.kv Thu Dec 29 23:47:04 2016 +0100 +++ b/src/cagou/plugins/plugin_wid_chat.kv Thu Dec 29 23:47:07 2016 +0100 @@ -14,6 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +#:import TransferMenu cagou.core.menu.TransferMenu + : size_hint: None, None @@ -100,12 +102,9 @@ hint_text: "Enter your message here" on_text_validate: root.onSend(args[0]) IconButton - # upload button + # transfer button source: app.expand("{media}/icons/tango/actions/32/list-add.png") allow_stretch: True size_hint: None, 1 width: max(self.texture_size[0], dp(40)) - on_release: upload_menu.show(self) - UploadMenu: - id: upload_menu - callback: root.onUploadOK + on_release: TransferMenu(callback=root.onTransferOK).show(self) diff -r 641678ddc26c -r 5d2289127bb7 src/cagou/plugins/plugin_wid_chat.py --- a/src/cagou/plugins/plugin_wid_chat.py Thu Dec 29 23:47:04 2016 +0100 +++ b/src/cagou/plugins/plugin_wid_chat.py Thu Dec 29 23:47:07 2016 +0100 @@ -613,10 +613,10 @@ if cleaning_cb is not None: cleaning_cb() # TODO: display message to user - log.warning(u"Can't upload file: {}".format(err_msg)) + log.warning(u"Can't transfer file: {}".format(err_msg)) - def fileUploadDone(self, metadata, profile): - log.debug("file uploaded: {}".format(metadata)) + def fileTransferDone(self, metadata, profile): + log.debug("file transfered: {}".format(metadata)) G.host.messageSend( self.target, {'': metadata['url']}, @@ -624,23 +624,23 @@ profile_key=profile ) - def fileUploadCb(self, progress_data, cleaning_cb): + def fileTransferCb(self, progress_data, cleaning_cb): try: progress_id = progress_data['progress'] except KeyError: xmlui = progress_data['xmlui'] G.host.showUI(xmlui) else: - self._waiting_pids[progress_id] = (self.fileUploadDone, cleaning_cb) + self._waiting_pids[progress_id] = (self.fileTransferDone, cleaning_cb) - def onUploadOK(self, file_path, cleaning_cb): - G.host.bridge.fileUpload( + def onTransferOK(self, file_path, cleaning_cb): + G.host.bridge.fileTransfer( file_path, "", "", {"ignore_tls_errors": C.BOOL_TRUE}, # FIXME: should not be the default self.profile, - callback = lambda progress_data: self.fileUploadCb(progress_data, cleaning_cb) + callback = lambda progress_data: self.fileTransferCb(progress_data, cleaning_cb) ) def _mucJoinCb(self, joined_data):