view cagou/core/platform_/android.py @ 373:5d994be1161b

core: removed root menus (i.e. global menu on top of window): root menus were not really useful as most actions doable there are doable through others widgets in Cagou. For actions without equivalent in widgets (like about screen), a new menu, more discreet, will be added soon. Kivy Garden's ContextMenu is not used anymore, so it has been removed from dependencies
author Goffi <goffi@goffi.org>
date Mon, 27 Jan 2020 21:17:09 +0100
parents 1481f09c9175
children b2a87239af25
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')
PythonActivity = autoclass('org.kivy.android.PythonActivity')
mActivity = 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_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 profileAutoconnectGetCb(self, profile=None):
        if profile is not None:
            G.host.options.profile = profile
        G.host.postInit()

    def profileAutoconnectGetEb(self, failure_):
        log.error(f"Error while getting profile to autoconnect: {failure_}")
        G.host.postInit()

    def do_postInit(self):
        G.host.bridge.profileAutoconnectGet(
            callback=self.profileAutoconnectGetCb,
            errback=self.profileAutoconnectGetEb
        )
        return False

    def onProfilePlugged(self, profile):
        log.debug("ANDROID profilePlugged")
        G.host.bridge.setParam(
            "autoconnect_backend", C.BOOL_TRUE, "Connection", -1, profile,
            callback=lambda: log.info(f"profile {profile} autonnection set"),
            errback=lambda: log.error(f"can't set {profile} autonnection"))
        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 on_key_back_root(self):
        PythonActivity.moveTaskToBack(True)
        return True

    def on_key_back_share(self, share_widget):
        share_widget.close()
        PythonActivity.moveTaskToBack(True)
        return True

    def updateParamsExtra(self, extra):
        # on Android, we handle autoconnection automatically,
        # user must not modify those parameters
        extra.update(
            {
                "ignore": [
                    ["Connection", "autoconnect_backend"],
                    ["Connection", "autoconnect"],
                    ["Connection", "autodisconnect"],
                ],
            }
        )

    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)