view cagou/core/platform_/android.py @ 353:19422bbd9c8e

core (widgets handler): refactoring: - CagouWidget now has class properties (to be overridden when needed) which indicate how if the widget handle must add a wrapping ScreenManager (global_screen_manager) or show all instances of the class in a Carousel (collection_carousel). If none of those options is used, a ScrollView will be wrapping the widget, to be sure that the widget will be resized correctly when necessary (without it, the widget could still be drawn in the backround when the size is too small and overflow on the WidgetWrapper, this would be the case with WidgetSelector) - some helper methods/properties have been added to CagouWidget. Check docstrings for details - better handling of (in)visible widget in WidgetsHandler - thanks to the new wrapping ScrollView, WidgetSelect will show scroll bars if the available space is too small. - bugs fixes
author Goffi <goffi@goffi.org>
date Fri, 17 Jan 2020 18:44:35 +0100
parents a3cefa7158dc
children 4d3a0c4f2430
line wrap: on
line source

#!/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
import os
import socket
from jnius import autoclass, cast
from android import activity
from sat.core import log as logging
from urllib.parse import urlparse
from cagou.core.constants import Const as C
from cagou import G
from kivy.clock import Clock
from .base import Platform as BasePlatform


log = logging.getLogger(__name__)

service = autoclass('org.salutatoi.cagou.ServiceBackend')
mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
Intent = autoclass('android.content.Intent')
AndroidString = autoclass('java.lang.String')
Uri = autoclass('android.net.Uri')
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"


class Platform(BasePlatform):

    def __init__(self):
        super().__init__()
        # cache for callbacks to run when profile is plugged
        self.cache = []

    def init_platform(self):
        # 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 on_app_build(self, wid):
        # we don't want menu on Android
        wid.root_menus.height = 0

    def on_host_init(self, host):
        argument = ''
        service.start(mActivity, argument)

        activity.bind(on_new_intent=self.on_new_intent)
        self.cache.append((self.on_new_intent, mActivity.getIntent()))
        host.addListener('profilePlugged', self.onProfilePlugged)

    def on_initFrontendState(self):
        # XXX: we use a separated socket instead of bridge because if we
        #      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)

    def onProfilePlugged(self, profile):
        log.debug("ANDROID profilePlugged")
        for method, *args in self.cache:
            method(*args)
        del self.cache
        G.host.removeListener("profilePlugged", self.onProfilePlugged)

    def on_pause(self):
        G.host.sync = False
        self._frontend_status_socket.sendall(STATE_PAUSED)
        return True

    def on_resume(self):
        self._frontend_status_socket.sendall(STATE_RUNNING)
        G.host.sync = True

    def on_stop(self):
        self._frontend_status_socket.sendall(STATE_STOPPED)
        self._frontend_status_socket.close()

    def getPathFromUri(self, 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)

    def on_new_intent(self, intent):
        log.debug("on_new_intent")
        action = intent.getAction();
        intent_type = intent.getType();
        if action == 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 = self.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 open_url(self, url, wid=None):
        parsed_url = urlparse(url)
        if parsed_url.scheme == "geo":
            intent = Intent(Intent.ACTION_VIEW)
            intent.setData(Uri.parse(url))
            if mActivity.getPackageManager() is not None:
                activity = cast('android.app.Activity', mActivity)
                activity.startActivity(intent)
        else:
            super().open_url(self, url, wid)