Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2351.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a `--debug` option to the `build` and `run` commands for Android.
14 changes: 13 additions & 1 deletion src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -1523,7 +1523,9 @@ def force_stop_app(self, package: str):
f"Unable to force stop app {package} on {self.device}"
) from e

def start_app(self, package: str, activity: str, passthrough: list[str]):
def start_app(
self, package: str, activity: str, passthrough: list[str], env: dict[str, str]
):
"""Start an app, specified as a package name & activity name.

If you have an APK file, and you are not sure of the package or activity
Expand All @@ -1533,6 +1535,7 @@ def start_app(self, package: str, activity: str, passthrough: list[str]):
:param package: The name of the Android package, e.g., com.username.myapp.
:param activity: The activity of the APK to start.
:param passthrough: Arguments to pass to the app.
:param env: Environment variables to pass to the app.
:returns: `None` on success; raises an exception on failure.
"""
try:
Expand All @@ -1552,6 +1555,15 @@ def start_app(self, package: str, activity: str, passthrough: list[str]):
"--es",
"org.beeware.ARGV",
shlex.quote(json.dumps(passthrough)), # Protect from Android's shell
*(
[
"--es",
"org.beeware.ENVIRON",
shlex.quote(json.dumps(env)), # Protect from Android's shell
]
if env
else []
),
)

# `adb shell am start` always exits with status zero. We look for error
Expand Down
48 changes: 42 additions & 6 deletions src/briefcase/platforms/android/gradle.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
)
from briefcase.config import AppConfig, parsed_version
from briefcase.console import ANSI_ESC_SEQ_RE_DEF
from briefcase.debuggers.base import (
AppPackagesPathMappings,
DebuggerConnectionMode,
)
from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.android_sdk import ADB, AndroidSDK
from briefcase.integrations.subprocess import SubprocessArgT
Expand Down Expand Up @@ -216,15 +220,19 @@ def output_format_template_context(self, app: AppConfig):
"androidx.swiperefreshlayout:swiperefreshlayout:1.1.0",
]

# Extract test packages, to enable features like test discovery and assertion rewriting.
extract_sources = app.test_sources or []

# In debug mode extract all source packages so that the debugger can get the source code
# at runtime. This is necessary for setting breakpoints in VSCode or when using 'll' in pdb.
if app.debugger:
extract_sources.extend(app.sources)

return {
"version_code": version_code,
"safe_formal_name": safe_formal_name(app.formal_name),
# Extract test packages to enable features like test discovery and assertion
# rewriting.
"extract_packages": ", ".join(
f'"{name}"'
for path in (app.test_sources or [])
if (name := Path(path).name)
[f'"{name}"' for path in extract_sources if (name := Path(path).name)]
),
"build_gradle_dependencies": {"implementation": dependencies},
}
Expand Down Expand Up @@ -288,6 +296,7 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]):

class GradleUpdateCommand(GradleCreateCommand, UpdateCommand):
description = "Update an existing Android Gradle project."
supports_debugger = True


class GradleOpenCommand(GradleMixin, OpenCommand):
Expand All @@ -296,6 +305,7 @@ class GradleOpenCommand(GradleMixin, OpenCommand):

class GradleBuildCommand(GradleMixin, BuildCommand):
description = "Build an Android debug APK."
supports_debugger = True

def metadata_resource_path(self, app: AppConfig):
return self.bundle_path(app) / self.path_index(app, "metadata_resource_path")
Expand Down Expand Up @@ -333,6 +343,7 @@ def build_app(self, app: AppConfig, **kwargs):

class GradleRunCommand(GradleMixin, RunCommand):
description = "Run an Android debug APK on a device (physical or virtual)."
supports_debugger = True

def verify_tools(self):
super().verify_tools()
Expand Down Expand Up @@ -378,6 +389,20 @@ def add_options(self, parser):
help="Reverse the specified port from device to host.",
)

def debugger_app_packages_path_mapping(
self, app: AppConfig
) -> AppPackagesPathMappings:
"""Get the path mappings for the app packages.

:param app: The config object for the app
:returns: The path mappings for the app packages
"""
app_packages_path = self.bundle_path(app) / "app/build/python/pip/debug/common"
return AppPackagesPathMappings(
sys_path_regex="requirements$",
host_folder=f"{app_packages_path}",
)

def run_app(
self,
app: AppConfig,
Expand Down Expand Up @@ -450,6 +475,17 @@ def run_app(
forward_ports = forward_ports or []
reverse_ports = reverse_ports or []

env = {}
if self.console.is_debug:
env["BRIEFCASE_DEBUG"] = "1"

if app.debugger:
env["BRIEFCASE_DEBUGGER"] = app.debugger.get_env_config(self, app)
if app.debugger.connection_mode == DebuggerConnectionMode.SERVER:
forward_ports.append(app.debugger_port)
else:
reverse_ports.append(app.debugger_port)

# Forward/Reverse requested ports
with self.forward_ports(adb, forward_ports, reverse_ports):
# To start the app, we launch `org.beeware.android.MainActivity`.
Expand All @@ -458,7 +494,7 @@ def run_app(
device_start_time = adb.datetime()

adb.start_app(
package, "org.beeware.android.MainActivity", passthrough
package, "org.beeware.android.MainActivity", passthrough, env
)

# Try to get the PID for 5 seconds.
Expand Down
53 changes: 49 additions & 4 deletions tests/integrations/android_sdk/ADB/test_start_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_start_app_launches_app(adb, capsys, passthrough):

# Invoke start_app
adb.start_app(
"com.example.sample.package", "com.example.sample.activity", passthrough
"com.example.sample.package", "com.example.sample.activity", passthrough, {}
)

# Validate call parameters.
Expand All @@ -54,6 +54,45 @@ def test_start_app_launches_app(adb, capsys, passthrough):
assert "normal adb output" not in capsys.readouterr()


@pytest.mark.parametrize(
"env",
[
{"PARAM1": "VALUE1"},
{"BRIEFCASE_DEBUGGER": '{"host": "localhost", "port": 1234}'},
],
)
def test_start_app_launches_app_with_env(adb, capsys, env):
"""Invoking `start_app()` calls `run()` with the appropriate parameters."""
# Mock out the run command on an adb instance
adb.run = MagicMock(return_value="example normal adb output")

# Invoke start_app
adb.start_app("com.example.sample.package", "com.example.sample.activity", [], env)

# Validate call parameters.
adb.run.assert_called_once_with(
"shell",
"am",
"start",
"-n",
"com.example.sample.package/com.example.sample.activity",
"-a",
"android.intent.action.MAIN",
"-c",
"android.intent.category.LAUNCHER",
"--es",
"org.beeware.ARGV",
"'[]'",
"--es",
"org.beeware.ENVIRON",
shlex.quote(json.dumps(env)),
)

# Validate that the normal output of the command was not printed (since there
# was no error).
assert "normal adb output" not in capsys.readouterr()


def test_missing_activity(adb):
"""If the activity doesn't exist, the error is caught."""
# Use real `adb` output from launching an activity that does not exist.
Expand All @@ -69,7 +108,9 @@ def test_missing_activity(adb):
)

with pytest.raises(BriefcaseCommandError) as exc_info:
adb.start_app("com.example.sample.package", "com.example.sample.activity", [])
adb.start_app(
"com.example.sample.package", "com.example.sample.activity", [], {}
)

assert "Activity class not found" in str(exc_info.value)

Expand All @@ -81,7 +122,9 @@ def test_invalid_device(adb):
adb.run = MagicMock(side_effect=InvalidDeviceError("device", "exampleDevice"))

with pytest.raises(InvalidDeviceError):
adb.start_app("com.example.sample.package", "com.example.sample.activity", [])
adb.start_app(
"com.example.sample.package", "com.example.sample.activity", [], {}
)


def test_unable_to_start(adb):
Expand All @@ -92,4 +135,6 @@ def test_unable_to_start(adb):
BriefcaseCommandError,
match=r"Unable to start com.example.sample.package/com.example.sample.activity on exampleDevice",
):
adb.start_app("com.example.sample.package", "com.example.sample.activity", [])
adb.start_app(
"com.example.sample.package", "com.example.sample.activity", [], {}
)
8 changes: 8 additions & 0 deletions tests/platforms/android/gradle/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect
assert context["extract_packages"] == expected


def test_extract_packages_debugger(create_command, first_app_config, dummy_debugger):
first_app_config.test_sources = ["one", "two", "three"]
first_app_config.sources = ["four", "five", "six"]
first_app_config.debugger = dummy_debugger
context = create_command.output_format_template_context(first_app_config)
assert context["extract_packages"] == '"one", "two", "three", "four", "five", "six"'


@pytest.mark.parametrize(
("permissions", "features", "context"),
[
Expand Down
Loading