Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option to run ADB in no-streaming mode #1922

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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/1922.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added to ``briefcase run android`` a ``--Xadb-install`` option for passing extra arguments to ``adb install``. This makes it possible to handle various corner cases, for example, setting the ``--no-streaming`` option to make installation work for devices running recent versions of LineageOS.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These notes should be written as a description of the "exciting new feature in the release", not a rehash of the commit message.

Suggested change
Added to ``briefcase run android`` a ``--Xadb-install`` option for passing extra arguments to ``adb install``. This makes it possible to handle various corner cases, for example, setting the ``--no-streaming`` option to make installation work for devices running recent versions of LineageOS.
The ``--Xadb-install`` option can now be used to pass arguments to ``adb install`` when running apps on Android.

7 changes: 5 additions & 2 deletions src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -1493,14 +1493,17 @@ def run(self, *arguments: SubprocessArgT, quiet: bool = False) -> str:
raise InvalidDeviceError("device id", self.device) from e
raise

def install_apk(self, apk_path: str | Path):
def install_apk(self, apk_path: str | Path, extra_args: list[str] | None = None):
"""Install an APK file on an Android device.

:param apk_path: The path of the Android APK file to install.
:param extra_args: Any additional arguments to pass to adb install.
:returns: `None` on success; raises an exception on failure.
"""
command = ["install", "-r"] + (extra_args if extra_args else []) + [apk_path]

try:
self.run("install", "-r", apk_path)
self.run(*command)
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError(
f"Unable to install APK {apk_path} on {self.device}"
Expand Down
11 changes: 10 additions & 1 deletion src/briefcase/platforms/android/gradle.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,13 @@ def add_options(self, parser):
),
required=False,
)
parser.add_argument(
"--Xadb-install",
action="append",
dest="extra_adb_install_args",
help="Additional arguments passed to adb install",
required=False,
)
parser.add_argument(
"--Xemulator",
action="append",
Expand All @@ -366,6 +373,7 @@ def run_app(
test_mode: bool,
passthrough: list[str],
device_or_avd=None,
extra_adb_install_args=None,
extra_emulator_args=None,
shutdown_on_exit=False,
**kwargs,
Expand All @@ -377,6 +385,7 @@ def run_app(
:param passthrough: The list of arguments to pass to the app
:param device_or_avd: The device to target. If ``None``, the user will
be asked to re-run the command selecting a specific device.
:param extra_adb_install_args: Any additional arguments to pass to adb install.
:param extra_emulator_args: Any additional arguments to pass to the emulator.
:param shutdown_on_exit: Should the emulator be shut down on exit?
"""
Expand Down Expand Up @@ -425,7 +434,7 @@ def run_app(

# Install the latest APK file onto the device.
with self.input.wait_bar("Installing new app version..."):
adb.install_apk(self.binary_path(app))
adb.install_apk(self.binary_path(app), extra_adb_install_args)

# To start the app, we launch `org.beeware.android.MainActivity`.
with self.input.wait_bar(f"Launching {label}..."):
Expand Down
115 changes: 108 additions & 7 deletions tests/platforms/android/gradle/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,31 @@ def test_device_option(run_command):
"no_update": False,
"test_mode": False,
"passthrough": [],
"extra_adb_install_args": None,
"extra_emulator_args": None,
"shutdown_on_exit": False,
}
assert overrides == {}


def test_extra_adb_install_args_option(run_command):
"""The --Xadb-install option can be parsed."""
options, overrides = run_command.parse_options(
["--Xadb-install=--no-streaming", "--Xadb-install=-g"]
)

assert options == {
"device_or_avd": None,
"appname": None,
"update": False,
"update_requirements": False,
"update_resources": False,
"update_support": False,
"update_stub": False,
"no_update": False,
"test_mode": False,
"passthrough": [],
"extra_adb_install_args": ["--no-streaming", "-g"],
"extra_emulator_args": None,
"shutdown_on_exit": False,
}
Expand All @@ -115,6 +140,7 @@ def test_extra_emulator_args_option(run_command):
"no_update": False,
"test_mode": False,
"passthrough": [],
"extra_adb_install_args": None,
"extra_emulator_args": ["-no-window", "-no-audio"],
"shutdown_on_exit": False,
}
Expand All @@ -136,6 +162,7 @@ def test_shutdown_on_exit_option(run_command):
"no_update": False,
"test_mode": False,
"passthrough": [],
"extra_adb_install_args": None,
"extra_emulator_args": None,
"shutdown_on_exit": True,
}
Expand Down Expand Up @@ -207,7 +234,81 @@ def mock_stream_output(app, stop_func, **kwargs):

# The adb wrapper is invoked with the expected arguments
run_command.tools.mock_adb.install_apk.assert_called_once_with(
run_command.binary_path(first_app_config)
run_command.binary_path(first_app_config), None
)
run_command.tools.mock_adb.force_stop_app.assert_called_once_with(
f"{first_app_config.package_name}.{first_app_config.module_name}",
)

run_command.tools.mock_adb.start_app.assert_called_once_with(
f"{first_app_config.package_name}.{first_app_config.module_name}",
"org.beeware.android.MainActivity",
[],
)

run_command.tools.mock_adb.pidof.assert_called_once_with(
f"{first_app_config.package_name}.{first_app_config.module_name}",
quiet=True,
)
run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777")

run_command._stream_app_logs.assert_called_once_with(
first_app_config,
popen=log_popen,
test_mode=False,
clean_filter=android_log_clean_filter,
clean_output=False,
stop_func=mock.ANY,
log_stream=True,
)

# The emulator was not killed at the end of the test
run_command.tools.mock_adb.kill.assert_not_called()


def test_run_extra_adb_install_args(run_command, first_app_config):
"""An app can be run on an existing device after being installed with extra adb
install arguments."""
# Set up device selection to return a running physical device.
run_command.tools.android_sdk.select_target_device = mock.MagicMock(
return_value=("exampleDevice", "ExampleDevice", None)
)

# Set up the log streamer to return a known stream
log_popen = mock.MagicMock()
run_command.tools.mock_adb.logcat.return_value = log_popen

# To satisfy coverage, the stop function must be invoked at least once
# when invoking stream_output.
def mock_stream_output(app, stop_func, **kwargs):
stop_func()

run_command._stream_app_logs.side_effect = mock_stream_output

# Set up app config to have a `-` in the `bundle`, to ensure it gets
# normalized into a `_` via `package_name`.
first_app_config.bundle = "com.ex-ample"

# Invoke run_app
run_command.run_app(
first_app_config,
device_or_avd="exampleDevice",
extra_adb_install_args=["--no-streaming", "-g"],
test_mode=False,
passthrough=[],
)

# select_target_device was invoked with a specific device
run_command.tools.android_sdk.select_target_device.assert_called_once_with(
"exampleDevice"
)

# The ADB wrapper is created
run_command.tools.android_sdk.adb.assert_called_once_with(device="exampleDevice")

# The adb wrapper is invoked with the expected arguments
run_command.tools.mock_adb.install_apk.assert_called_once_with(
run_command.binary_path(first_app_config), ["--no-streaming", "-g"]
)
run_command.tools.mock_adb.force_stop_app.assert_called_once_with(
f"{first_app_config.package_name}.{first_app_config.module_name}",
Expand Down Expand Up @@ -279,7 +380,7 @@ def mock_stream_output(app, stop_func, **kwargs):

# The adb wrapper is invoked with the expected arguments
run_command.tools.mock_adb.install_apk.assert_called_once_with(
run_command.binary_path(first_app_config)
run_command.binary_path(first_app_config), None
)
run_command.tools.mock_adb.force_stop_app.assert_called_once_with(
f"{first_app_config.package_name}.{first_app_config.module_name}",
Expand Down Expand Up @@ -439,7 +540,7 @@ def test_run_created_emulator(run_command, first_app_config):

# The adb wrapper is invoked with the expected arguments
run_command.tools.mock_adb.install_apk.assert_called_once_with(
run_command.binary_path(first_app_config)
run_command.binary_path(first_app_config), None
)
run_command.tools.mock_adb.force_stop_app.assert_called_once_with(
f"{first_app_config.package_name}.{first_app_config.module_name}",
Expand Down Expand Up @@ -501,7 +602,7 @@ def test_run_idle_device(run_command, first_app_config):

# The adb wrapper is invoked with the expected arguments
run_command.tools.mock_adb.install_apk.assert_called_once_with(
run_command.binary_path(first_app_config)
run_command.binary_path(first_app_config), None
)
run_command.tools.mock_adb.force_stop_app.assert_called_once_with(
f"{first_app_config.package_name}.{first_app_config.module_name}",
Expand Down Expand Up @@ -602,7 +703,7 @@ def mock_stream_output(app, stop_func, **kwargs):

# The adb wrapper is invoked with the expected arguments
run_command.tools.mock_adb.install_apk.assert_called_once_with(
run_command.binary_path(first_app_config)
run_command.binary_path(first_app_config), None
)
run_command.tools.mock_adb.force_stop_app.assert_called_once_with(
f"{first_app_config.package_name}.{first_app_config.module_name}",
Expand Down Expand Up @@ -675,7 +776,7 @@ def mock_stream_output(app, stop_func, **kwargs):

# The adb wrapper is invoked with the expected arguments
run_command.tools.mock_adb.install_apk.assert_called_once_with(
run_command.binary_path(first_app_config)
run_command.binary_path(first_app_config), None
)
run_command.tools.mock_adb.force_stop_app.assert_called_once_with(
f"{first_app_config.package_name}.{first_app_config.module_name}",
Expand Down Expand Up @@ -752,7 +853,7 @@ def test_run_test_mode_created_emulator(run_command, first_app_config):

# The adb wrapper is invoked with the expected arguments
run_command.tools.mock_adb.install_apk.assert_called_once_with(
run_command.binary_path(first_app_config)
run_command.binary_path(first_app_config), None
)
run_command.tools.mock_adb.force_stop_app.assert_called_once_with(
f"{first_app_config.package_name}.{first_app_config.module_name}",
Expand Down