-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add utilities for launching startup scripts
- Loading branch information
Showing
7 changed files
with
241 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
"""Android specific functions.""" | ||
# pylint: disable=invalid-name,import-error,import-outside-toplevel | ||
from pathlib import Path | ||
from typing import Optional | ||
import uuid | ||
|
||
from android import activity as android_activity | ||
from jnius import autoclass, cast | ||
from kivy.logger import Logger | ||
|
||
|
||
Context = autoclass("android.content.Context") | ||
Icon = autoclass("android.graphics.drawable.Icon") | ||
Intent = autoclass("android.content.Intent") | ||
PythonActivity = autoclass("org.kivy.android.PythonActivity") | ||
ShortcutInfoBuilder = autoclass("android.content.pm.ShortcutInfo$Builder") | ||
System = autoclass("java.lang.System") | ||
Uri = autoclass("android.net.Uri") | ||
|
||
|
||
def get_current_intent() -> Intent: | ||
"""Return the intent that started Python activity.""" | ||
return PythonActivity.mActivity.getIntent() | ||
|
||
|
||
def get_startup_script(intent: None = None) -> Optional[str]: | ||
"""Return script entrypoint that was passed to a given, or current, intent.""" | ||
if not intent: | ||
intent = get_current_intent() | ||
data = intent.getData() | ||
return data and data.toString() | ||
|
||
|
||
def restart_app(script: str = None): | ||
"""Restart app, with a script as a starting point if provided.""" | ||
Logger.info("PythonHere: restart requested with a script: %s", script) | ||
activity = PythonActivity.mActivity | ||
intent = Intent(activity.getApplicationContext(), PythonActivity) | ||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) | ||
|
||
if script: | ||
intent.setData(Uri.parse(script)) | ||
|
||
activity.startActivity(intent) | ||
System.exit(0) | ||
|
||
|
||
def bind_run_script_on_new_intent(): | ||
"""Add handler for new intent event: | ||
restart app with entrypoint of a new intent. | ||
""" | ||
|
||
def on_new_intent(intent): | ||
Logger.info("PythonHere: on_new_intent") | ||
restart_app(get_startup_script(intent)) | ||
Logger.error("PythonHere: app was not restarted") | ||
|
||
android_activity.bind(on_new_intent=on_new_intent) | ||
|
||
|
||
def create_shortcut_icon() -> Icon: | ||
"""Create icon to use for a shurtcut.""" | ||
activity = PythonActivity.mActivity | ||
Drawable = autoclass("{}.R$drawable".format(activity.getPackageName())) | ||
context = cast("android.content.Context", activity.getApplicationContext()) | ||
return Icon.createWithResource(context, Drawable.icon) | ||
|
||
|
||
def resolve_script_path(script: str) -> str: | ||
"""Resolve path against upload directory.""" | ||
from kivy.app import App | ||
|
||
if script.startswith("/"): | ||
path = Path(script) | ||
else: | ||
app = App.get_running_app() | ||
path = Path(app.upload_dir) / script | ||
return str(path.resolve(strict=True)) | ||
|
||
|
||
def pin_shortcut(script: str, label: str): | ||
"""Request a pinned shortcut creation to run a Python script.""" | ||
activity = PythonActivity.mActivity | ||
context = cast("android.content.Context", activity.getApplicationContext()) | ||
|
||
intent = Intent(activity.getApplicationContext(), PythonActivity) | ||
intent.setAction(Intent.ACTION_MAIN) | ||
intent.setData(Uri.parse(resolve_script_path(script))) | ||
|
||
shortcut = ( | ||
ShortcutInfoBuilder(context, f"pythonhere-{uuid.uuid4().hex}") | ||
.setShortLabel(label) | ||
.setLongLabel(label) | ||
.setIntent(intent) | ||
.setIcon(create_shortcut_icon()) | ||
.build() | ||
) | ||
|
||
manager = activity.getSystemService(Context.SHORTCUT_SERVICE) | ||
manager.requestPinShortcut(shortcut, None) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
"""Utilities for launching scripts.""" | ||
import os | ||
from pathlib import Path | ||
import runpy | ||
import sys | ||
|
||
|
||
from kivy.logger import Logger | ||
from kivy import platform | ||
|
||
|
||
def run_script(script: str): | ||
"""Execute given script.""" | ||
Logger.info("PythonHere: Run script %s", script) | ||
try: | ||
path = Path(script).resolve(strict=True) | ||
except FileNotFoundError: | ||
Logger.error("Script not found: %s", script) | ||
return | ||
|
||
original_cwd = str(Path.cwd()) | ||
original_sys_path = sys.path[:] | ||
try: | ||
script_dir = path.parent | ||
os.chdir(str(script_dir)) | ||
sys.path.insert(0, str(script_dir)) | ||
runpy.run_path(str(path), run_name="__main__") | ||
finally: | ||
os.chdir(original_cwd) | ||
sys.path = original_sys_path | ||
|
||
|
||
def try_startup_script(): | ||
"""Execute startup script, if it was passed to app.""" | ||
if platform != "android": | ||
return | ||
import android_here # pylint: disable=import-outside-toplevel | ||
|
||
try: | ||
android_here.bind_run_script_on_new_intent() | ||
script = android_here.get_startup_script() | ||
if script: | ||
run_script(script) | ||
except Exception: | ||
Logger.exception("PythonHere: Error while starting script") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import pytest | ||
from pathlib import Path | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"script", (None, "test.py"), | ||
) | ||
def test_restart_app(mocked_android_modules, app_instance, test_py_script, script): | ||
from android_here import restart_app | ||
restart_app(script) | ||
|
||
|
||
def test_script_path_resolved(mocked_android_modules, app_instance, test_py_script): | ||
from android_here import resolve_script_path | ||
path = resolve_script_path("test.py") | ||
assert path.startswith("/") and path.endswith("test.py") | ||
|
||
|
||
def test_absolute_script_path_resolved(mocked_android_modules, app_instance, test_py_script): | ||
from android_here import resolve_script_path | ||
assert resolve_script_path(test_py_script) == test_py_script | ||
|
||
|
||
def test_pin_shortcut(mocker, mocked_android_modules, app_instance, test_py_script): | ||
from android_here import pin_shortcut | ||
pin_shortcut("test.py", "test label") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
from launcher_here import run_script, try_startup_script | ||
|
||
|
||
def test_run_script(mocker, test_py_script): | ||
run_path = mocker.patch("runpy.run_path") | ||
run_script(test_py_script) | ||
run_path.assert_called_once() | ||
|
||
|
||
def test_run_script_not_found(mocker): | ||
run_path = mocker.patch("runpy.run_path") | ||
run_script("not_exist.py") | ||
run_path.assert_not_called() | ||
|
||
|
||
def test_try_startup_script_not_android(mocker): | ||
run_script = mocker.patch("launcher_here.run_script") | ||
try_startup_script() | ||
run_script.assert_not_called() | ||
|
||
|
||
def test_try_startup_script(mocker, mocked_android_modules): | ||
mocker.patch("launcher_here.platform", "android") | ||
run_script = mocker.patch("launcher_here.run_script") | ||
|
||
try_startup_script() | ||
run_script.assert_called_once() | ||
|
||
|
||
def test_try_startup_exception(mocker, mocked_android_modules): | ||
mocker.patch("launcher_here.platform", "android") | ||
logger_exception = mocker.patch("launcher_here.Logger.exception") | ||
run_script = mocker.patch("launcher_here.run_script", side_effect=Exception()) | ||
|
||
try_startup_script() | ||
run_script.assert_called_once() | ||
logger_exception.assert_called_once() | ||
|
||
|
||
def test_try_startup_no_script(mocker, mocked_android_modules): | ||
mocker.patch("launcher_here.platform", "android") | ||
mocker.patch("android_here.get_startup_script", return_value=None) | ||
run_script = mocker.patch("launcher_here.run_script") | ||
|
||
try_startup_script() | ||
run_script.assert_not_called() |