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
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ class AndroidBridge(PlatformBridge):
"C:\\Program Files (x86)\\Android\\android-sdk\\platform-tools",
]

# Dangerous (runtime) permissions the MultipeerReplicator may need. These are
# granted right after install so the test server doesn't need to block waiting
# for a system permission dialog during a test run.
__runtime_permissions: list[str] = [
"android.permission.ACCESS_FINE_LOCATION", # BLE scanning on API <= 30
"android.permission.BLUETOOTH_SCAN", # API 31+
"android.permission.BLUETOOTH_CONNECT", # API 31+
"android.permission.BLUETOOTH_ADVERTISE", # API 31+
]

def __init__(self, app_path: str, app_id: str, activity: str = "MainActivity"):
"""
Initialize the AndroidBridge with the application path, application ID, and activity name.
Expand Down Expand Up @@ -117,6 +127,39 @@ def install(self, location: str) -> None:
check=True,
capture_output=False,
)
self.__grant_runtime_permissions(location)

def __grant_runtime_permissions(self, location: str) -> None:
"""
Grant of the runtime permissions for MultipeerReplicator when using
Bluetooth, so the test server never blocks on a system permission
dialog.

Comment thread
borrrden marked this conversation as resolved.
Args:
location (str): The device location (e.g., device serial number).
"""
header(f"Granting runtime permissions to {self.__app_id} on {location}")
for permission in self.__runtime_permissions:
result = subprocess.run(
[
str(self.__adb_location),
"-s",
location,
"shell",
"pm",
"grant",
self.__app_id,
permission,
],
check=False,
capture_output=True,
text=True,
)
if result.returncode == 0:
click.echo(f" granted {permission}")
else:
message = (result.stderr or result.stdout).strip()
click.echo(f" skipped {permission} ({message})")

def run(self, location: str) -> None:
"""
Expand Down
19 changes: 19 additions & 0 deletions servers/jak/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<!-- MultipeerReplicator Bluetooth transport -->
<!-- Android 11 (API 30) and lower -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />

<!-- Android 12+ (API 31+): runtime permissions (pre-grant via adb pm grant for automation) -->
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />


<application
android:name="com.couchbase.lite.android.mobiletest.TestServerApp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ class AndroidTestApp(private val context: Context) : TestApp("Android") {
}


fun getContext(): Context = context

fun getMultipeerReplSvc(): MultipeerReplicatorService {
val mgr = multipeerReplSvc.get()
if (mgr == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.EnumSet
import java.util.EnumSet;

import com.couchbase.lite.Conflict;
import com.couchbase.lite.CouchbaseLiteException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,19 @@
import com.couchbase.lite.PeerInfo;
import com.couchbase.lite.PeerReplicatorStatus;
import com.couchbase.lite.ReplicatorActivityLevel;
import com.couchbase.lite.android.mobiletest.AndroidTestApp;
import com.couchbase.lite.mobiletest.TestApp;
import com.couchbase.lite.mobiletest.TestContext;
import com.couchbase.lite.mobiletest.errors.CblApiFailure;
import com.couchbase.lite.mobiletest.errors.ClientError;
import com.couchbase.lite.mobiletest.errors.ServerError;
import com.couchbase.lite.mobiletest.services.DatabaseService;
import com.couchbase.lite.mobiletest.services.Log;


public class MultipeerReplicatorService {
private static final String TAG = "MP_REPL_SVC";

private final Map<String, Map<PeerInfo.PeerId, PeerReplicatorStatus>> statusMap = new HashMap<>();
private final Map<String, ListenerToken> listenerTokens = new HashMap<>();

Expand Down Expand Up @@ -76,6 +81,25 @@ public String startReplicator(@NonNull TestContext ctxt, @NonNull MultipeerRepli
try { repl = new MultipeerReplicator(config); }
catch (CouchbaseLiteException e) { throw new CblApiFailure("Failed creating multipeer replicator", e); }

// Sanity check: the multipeer replicator needs dangerous runtime permissions (e.g. Bluetooth
// on API 31+). These are expected to be pre-granted on the device (see AndroidBridge.install,
// which runs "adb pm grant" after installing). If any are still missing, fail fast with the
// exact commands needed rather than letting the replicator fail later with an opaque error.
final AndroidTestApp app = (AndroidTestApp) TestApp.getApp();
final Set<String> missingPerms = repl.getMissingPermissions(app.getContext());
if (!missingPerms.isEmpty()) {
repl.close();
final String pkg = app.getContext().getPackageName();
final StringBuilder msg = new StringBuilder(
"Cannot start the multipeer replicator: the following runtime permissions are not"
+ " granted: " + missingPerms + ". Grant them on the device before running, e.g.:");
for (String perm : missingPerms) {
msg.append("\n adb shell pm grant ").append(pkg).append(' ').append(perm);
}
Log.err(TAG, msg.toString());
throw new ServerError(msg.toString());
}

final String replId = UUID.randomUUID().toString();
statusMap.put(replId, new HashMap<>());

Expand Down
Loading