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/2369.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ``--forward-port`` and ``--reverse-port`` via ADB when running an Android app
23 changes: 22 additions & 1 deletion docs/reference/platforms/android/gradle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,35 @@ You may specify multiple ``--Xemulator`` arguments; each one specifies a
single argument to pass to the emulator, in the order they are specified.

``--shutdown-on-exit``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~

Instruct Briefcase to shut down the emulator when the run finishes. This is
especially useful if you are running in headless mode, as the emulator will
continue to run in the background, but there will be no visual manifestation
that it is running. It may also be useful as a cleanup mechanism when running
in a CI configuration.

``--forward-port=<port>``
~~~~~~~~~~~~~~~~~~~~~~~~~

Forward a port via ADB from the host to the Android device. This is useful when
a network service is running on the Android app that you want to connect to from
the host.

You may specify multiple ``--forward-port`` arguments; each one specifies a
single port.

``--reverse-port=<port>``
~~~~~~~~~~~~~~~~~~~~~~~~~

Reverse a port via ADB from the Android device to the host. This is useful when
a network service is running on the host that you want to connect to from the
Android app.

You may specify multiple ``--reverse-port`` arguments; each one specifies a
single port.


Application configuration
=========================

Expand Down
80 changes: 80 additions & 0 deletions src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -1628,6 +1628,86 @@ def logcat_tail(self, since: datetime):
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Error starting ADB logcat.") from e

def forward(self, host_port: int, device_port: int):
"""Use the forward command to set up arbitrary port forwarding, which
forwards requests on a specific host port to a different port on a device.

:param host_port: The port on the host that should be forwarded to the device
:param device_port: The port on the device
"""
try:
self.tools.subprocess.check_output(
[
self.tools.android_sdk.adb_path,
"-s",
self.device,
"forward",
f"tcp:{host_port}",
f"tcp:{device_port}",
],
)
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Error starting 'adb forward'.") from e

def forward_remove(self, host_port: int):
"""Remove forwarded port.

:param host_port: The port on the host that should be removed
"""
try:
self.tools.subprocess.check_output(
[
self.tools.android_sdk.adb_path,
"-s",
self.device,
"forward",
"--remove",
f"tcp:{host_port}",
],
)
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Error starting 'adb forward --remove'.") from e

def reverse(self, device_port: int, host_port: int):
"""Use the reverse command to set up arbitrary port forwarding, which
forwards requests on a specific device port to a different port on the host.

:param device_port: The port on the device that should be forwarded to the host
:param host_port: The port on the host
"""
try:
self.tools.subprocess.check_output(
[
self.tools.android_sdk.adb_path,
"-s",
self.device,
"reverse",
f"tcp:{device_port}",
f"tcp:{host_port}",
],
)
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Error starting 'adb reverse'.") from e

def reverse_remove(self, device_port: int):
"""Remove reversed port.

:param device_port: The port on the device that should be removed
"""
try:
self.tools.subprocess.check_output(
[
self.tools.android_sdk.adb_path,
"-s",
self.device,
"reverse",
"--remove",
f"tcp:{device_port}",
],
)
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Error starting 'adb reverse --remove'.") from e

def pidof(self, package: str, **kwargs) -> str | None:
"""Obtain the PID of a running app by package name.

Expand Down
141 changes: 97 additions & 44 deletions src/briefcase/platforms/android/gradle.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import contextlib
import datetime
import re
import subprocess
Expand All @@ -18,7 +19,7 @@
from briefcase.config import AppConfig, parsed_version
from briefcase.console import ANSI_ESC_SEQ_RE_DEF
from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.android_sdk import AndroidSDK
from briefcase.integrations.android_sdk import ADB, AndroidSDK
from briefcase.integrations.subprocess import SubprocessArgT


Expand Down Expand Up @@ -358,6 +359,20 @@ def add_options(self, parser):
help="Shutdown the emulator on exit",
required=False,
)
parser.add_argument(
"--forward-port",
action="append",
dest="forward_ports",
type=int,
help="Forward the specified port from host to device.",
)
parser.add_argument(
"--reverse-port",
action="append",
dest="reverse_ports",
type=int,
help="Reverse the specified port from device to host.",
)

def run_app(
self,
Expand All @@ -366,6 +381,8 @@ def run_app(
device_or_avd=None,
extra_emulator_args=None,
shutdown_on_exit=False,
forward_ports: list[int] | None = None,
reverse_ports: list[int] | None = None,
**kwargs,
):
"""Start the application.
Expand All @@ -376,6 +393,8 @@ def run_app(
be asked to re-run the command selecting a specific device.
:param extra_emulator_args: Any additional arguments to pass to the emulator.
:param shutdown_on_exit: Should the emulator be shut down on exit?
:param forward_ports: A list of ports to forward for the app.
:param reverse_ports: A list of ports to reversed for the app.
"""
device, name, avd = self.tools.android_sdk.select_target_device(device_or_avd)

Expand Down Expand Up @@ -424,55 +443,89 @@ def run_app(
with self.console.wait_bar("Installing new app version..."):
adb.install_apk(self.binary_path(app))

# To start the app, we launch `org.beeware.android.MainActivity`.
with self.console.wait_bar(f"Launching {label}..."):
# capture the earliest time for device logging in case PID not found
device_start_time = adb.datetime()

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

# Try to get the PID for 5 seconds.
pid = None
fail_time = datetime.datetime.now() + datetime.timedelta(seconds=5)
while not pid and datetime.datetime.now() < fail_time:
# Try to get the PID; run in quiet mode because we may
# need to do this a lot in the next 5 seconds.
pid = adb.pidof(package, quiet=2)
if not pid:
time.sleep(0.01)

if pid:
self.console.info(
"Following device log output (type CTRL-C to stop log)...",
prefix=app.app_name,
)
# Start adb's logcat in a way that lets us stream the logs
log_popen = adb.logcat(pid=pid)

# Stream the app logs.
self._stream_app_logs(
app,
popen=log_popen,
clean_filter=android_log_clean_filter,
clean_output=False,
# Check for the PID in quiet mode so logs aren't corrupted.
stop_func=lambda: not adb.pid_exists(pid=pid, quiet=2),
log_stream=True,
)
else:
self.console.error("Unable to find PID for app", prefix=app.app_name)
self.console.error("Logs for launch attempt follow...")
self.console.error("=" * 75)

# Show the log from the start time of the app
adb.logcat_tail(since=device_start_time)
forward_ports = forward_ports or []
reverse_ports = reverse_ports or []

# Forward/Reverse requested ports
with self.forward_ports(adb, forward_ports, reverse_ports):
# To start the app, we launch `org.beeware.android.MainActivity`.
with self.console.wait_bar(f"Launching {label}..."):
# capture the earliest time for device logging in case PID not found
device_start_time = adb.datetime()

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

# Try to get the PID for 5 seconds.
pid = None
fail_time = datetime.datetime.now() + datetime.timedelta(seconds=5)
while not pid and datetime.datetime.now() < fail_time:
# Try to get the PID; run in quiet mode because we may
# need to do this a lot in the next 5 seconds.
pid = adb.pidof(package, quiet=2)
if not pid:
time.sleep(0.01)

if pid:
self.console.info(
"Following device log output (type CTRL-C to stop log)...",
prefix=app.app_name,
)
# Start adb's logcat in a way that lets us stream the logs
log_popen = adb.logcat(pid=pid)

# Stream the app logs.
self._stream_app_logs(
app,
popen=log_popen,
clean_filter=android_log_clean_filter,
clean_output=False,
# Check for the PID in quiet mode so logs aren't corrupted.
stop_func=lambda: not adb.pid_exists(pid=pid, quiet=2),
log_stream=True,
)
else:
self.console.error(
"Unable to find PID for app", prefix=app.app_name
)
self.console.error("Logs for launch attempt follow...")
self.console.error("=" * 75)

# Show the log from the start time of the app
adb.logcat_tail(since=device_start_time)

raise BriefcaseCommandError(
f"Problem starting app {app.app_name!r}"
)

raise BriefcaseCommandError(f"Problem starting app {app.app_name!r}")
finally:
if shutdown_on_exit:
with self.tools.console.wait_bar("Stopping emulator..."):
adb.kill()

@contextlib.contextmanager
def forward_ports(
self, adb: ADB, forward_ports: list[int], reverse_ports: list[int]
):
"""Establish a port forwarding/reversion.

:param adb: The ADB wrapper for the device
:param forward_ports: Ports to forward via ADB
:param reverse_ports: Ports to reverse via ADB
"""
for port in forward_ports:
adb.forward(port, port)
for port in reverse_ports:
adb.reverse(port, port)

yield

for port in forward_ports:
adb.forward_remove(port)
for port in reverse_ports:
adb.reverse_remove(port)


class GradlePackageCommand(GradleMixin, PackageCommand):
description = "Create an Android App Bundle and APK in release mode."
Expand Down
Loading