Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.
4 changes: 2 additions & 2 deletions docs/en/how-to/debugging/pdb.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ To debug an app in development mode, add `breakpoint()` to your code somewhere t

/// warning | Note

This is currently an **experimental feature** that is only supported on Windows, macOS and iOS.
This is currently an **experimental feature** that is only supported on Windows, macOS, iOS and Android.

///

To debug a bundled app, add `breakpoint()` somewhere in your code where the debugger should halt.

Your app must then be modified to include a bootstrap that will connect to the VS Code debugger. This is done by passing the `--debug debugpy` option to `briefcase build`:
Your app must then be modified to include a bootstrap that will connect to the VS Code debugger. This is done by passing the `--debug pdb` option to `briefcase build`:

```console
$ briefcase build --debug pdb
Expand Down
2 changes: 1 addition & 1 deletion docs/en/how-to/debugging/vscode.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ To start a debug session, open the debug view in VS Code using the sidebar, sele

/// warning | Experimental feature

This is currently an **experimental feature** that is only supported on Windows, macOS and iOS.
This is currently an **experimental feature** that is only supported on Windows, macOS, iOS and Android.

///

Expand Down
2 changes: 1 addition & 1 deletion docs/en/reference/commands/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Currently the following debuggers are supported:

If calling only `--debug` without selecting a debugger explicitly, `pdb` is used as default.

This is an **experimental** new feature, that is currently only supported on Windows, macOS and iOS.
This is an **experimental** new feature, that is currently only supported on Windows, macOS, iOS and Android.

This option may slow down the app a little bit.

Expand Down
2 changes: 1 addition & 1 deletion docs/en/reference/commands/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Currently the following debuggers are supported:

If calling only `--debug` without selecting a debugger explicitly, `pdb` is used as default.

This is an **experimental** new feature, that is currently only supported on Windows, macOS and iOS.
This is an **experimental** new feature, that is currently only supported on Windows, macOS, iOS and Android.

The selected debugger in `run --debug <debugger>` has to match the selected debugger in `build --debug <debugger>`.

Expand Down
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
65 changes: 56 additions & 9 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 @@ -69,7 +73,7 @@ def android_log_clean_filter(line):
class GradleMixin:
output_format = "gradle"
platform = "android"
platform_target_version = "0.3.15"
platform_target_version = "0.3.27"

@property
def packaging_formats(self):
Expand Down Expand Up @@ -219,13 +223,6 @@ def output_format_template_context(self, app: AppConfig):
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)
),
"build_gradle_dependencies": {"implementation": dependencies},
}

Expand Down Expand Up @@ -314,6 +311,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 @@ -322,10 +320,14 @@ 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")

def extract_packages_path(self, app: AppConfig):
return self.bundle_path(app) / self.path_index(app, "extract_packages_path")

def update_app_metadata(self, app: AppConfig):
with (
self.console.wait_bar("Setting main module..."),
Expand All @@ -341,6 +343,25 @@ def update_app_metadata(self, app: AppConfig):
"""
)

with (
self.console.wait_bar("Setting packages to extract..."),
self.extract_packages_path(app).open("w", encoding="utf-8") as f,
):
if app.debugger:
# In debug mode include the .py files and extract all of them so
# that the debugger can get the source code at runtime. This is
# e.g. necessary for setting breakpoints in VS Code.
extract_packages = ["*"]
else:
# Extract test packages, to enable features like test discovery and
# assertion rewriting.
extract_sources = app.test_sources or []
extract_packages = [
name for path in extract_sources if (name := Path(path).name)
]

f.write("\n".join(extract_packages))

def build_app(self, app: AppConfig, **kwargs):
"""Build an application.

Expand All @@ -359,6 +380,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 @@ -404,6 +426,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 @@ -476,6 +512,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 @@ -484,7 +531,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", [], {}
)
13 changes: 13 additions & 0 deletions tests/platforms/android/gradle/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def first_app_generated(first_app_config, tmp_path):
app_packages_path="app_packages"
support_path="support"
metadata_resource_path="res/briefcase.xml"
extract_packages_path = "app/extract-packages.txt"
""",
)

Expand All @@ -64,4 +65,16 @@ def first_app_generated(first_app_config, tmp_path):
/ "briefcase.xml",
"""<resources></resources>""",
)

create_file(
tmp_path
/ "base_path"
/ "build"
/ "first-app"
/ "android"
/ "gradle"
/ "app"
/ "extract-packages.txt",
"something-to-be-overwritten",
)
return first_app_config
Loading