feat(android): add Bluetooth and USB permissions, implement JavaScript interfaces for Bluetooth and USB management, and enhance Android wheel verification in CI workflow

This commit is contained in:
Ivan
2026-04-19 11:36:40 -05:00
parent 6e59548eb9
commit a6764d3d38
7 changed files with 282 additions and 35 deletions
+5
View File
@@ -16,6 +16,11 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<application
android:name="com.chaquo.python.android.PyApplication"
@@ -3,8 +3,10 @@ package com.meshchatx;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -48,6 +50,7 @@ public class MainActivity extends AppCompatActivity {
private static final int SERVER_PORT = 8000;
private static final int RUNTIME_PERMISSIONS_REQUEST_CODE = 1001;
private static final int WEB_MEDIA_PERMISSION_REQUEST_CODE = 1003;
private static final int RNODE_BLUETOOTH_PERMISSION_REQUEST_CODE = 1004;
private static final String PREFS_NAME = "meshchatx";
private static final String PREF_BATTERY_OPT_REQUESTED = "battery_opt_requested";
private static final int MAX_CONNECTION_ATTEMPTS = 120;
@@ -345,7 +348,7 @@ public class MainActivity extends AppCompatActivity {
}
}
private void addIfMissing(List<String> missingPermissions, String permission) {
void addIfMissing(List<String> missingPermissions, String permission) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
missingPermissions.add(permission);
}
@@ -553,6 +556,81 @@ public class MainActivity extends AppCompatActivity {
android.os.Process.killProcess(android.os.Process.myPid());
});
}
@JavascriptInterface
public String getPlatform() {
return "android";
}
@JavascriptInterface
public boolean hasBluetoothPermissions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return true;
}
return ContextCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_CONNECT)
== PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_SCAN)
== PackageManager.PERMISSION_GRANTED;
}
@JavascriptInterface
public void requestBluetoothPermissions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return;
}
activity.runOnUiThread(() -> {
List<String> missing = new ArrayList<>();
activity.addIfMissing(missing, Manifest.permission.BLUETOOTH_CONNECT);
activity.addIfMissing(missing, Manifest.permission.BLUETOOTH_SCAN);
if (missing.isEmpty()) {
return;
}
ActivityCompat.requestPermissions(
activity,
missing.toArray(new String[0]),
RNODE_BLUETOOTH_PERMISSION_REQUEST_CODE
);
});
}
@JavascriptInterface
public boolean hasUsbPermissions() {
// WebUSB / Web Serial polyfill drives the device picker; from the
// Android manifest standpoint USB host access is granted as soon as
// the user accepts the per-device dialog. Surface true when we
// have a UsbManager so the JS layer can short-circuit prompts.
UsbManager manager = (UsbManager) activity.getSystemService(Context.USB_SERVICE);
return manager != null;
}
@JavascriptInterface
public void requestUsbPermissions() {
// No-op on android: per-device prompts are issued by WebUSB itself.
// Method is exposed so the JS bridge contract is symmetric.
}
@JavascriptInterface
public void openBluetoothSettings() {
activity.runOnUiThread(() -> {
try {
activity.startActivity(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS));
} catch (ActivityNotFoundException ignored) {
Toast.makeText(activity, "Bluetooth settings unavailable", Toast.LENGTH_SHORT).show();
}
});
}
@JavascriptInterface
public void openUsbSettings() {
activity.runOnUiThread(() -> {
try {
activity.startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:" + activity.getPackageName())));
} catch (ActivityNotFoundException ignored) {
Toast.makeText(activity, "USB settings unavailable", Toast.LENGTH_SHORT).show();
}
});
}
}
}
@@ -0,0 +1,10 @@
package:
name: cffi
version: "2.0.0"
build:
number: 0
requirements:
host:
- chaquopy-libffi 3.3
@@ -0,0 +1,6 @@
package:
name: miniaudio
version: "1.70"
build:
number: 1
@@ -0,0 +1,63 @@
--- src-original/build_ffi_module.py 2026-04-18 01:06:29.502232803 -0500
+++ src/build_ffi_module.py 2026-04-18 01:06:45.617868060 -0500
@@ -834,33 +834,16 @@
def check_linker_need_libatomic():
+ """Chaquopy override: 32-bit Android ABIs need libatomic for 64-bit
+ atomic ops; 64-bit ABIs link them as compiler builtins. Upstream's
+ helper spawns the host ``c++`` to detect this, which is unreliable
+ inside Chaquopy's cross-compilation environment because the answer
+ reflects the build host (Linux x86_64), not the target ABI.
"""
- Test if linker on system needs libatomic.
- This has been copied from https://github.com/grpc/grpc/blob/master/setup.py#L205
- """
- code_test = (
- b"#include <atomic>\n" + b"int main() { return std::atomic<int64_t>{}; }"
- )
- cxx = shlex.split(os.environ.get("CXX", "c++"))
- cpp_test = subprocess.Popen(
- cxx + ["-x", "c++", "-std=c++14", "-"],
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
- cpp_test.communicate(input=code_test)
- if cpp_test.returncode == 0:
- return False
- # Double-check to see if -latomic actually can solve the problem.
- # https://github.com/grpc/grpc/issues/22491
- cpp_test = subprocess.Popen(
- cxx + ["-x", "c++", "-std=c++14", "-", "-latomic"],
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
- cpp_test.communicate(input=code_test)
- return cpp_test.returncode == 0
+ abi = os.environ.get("CHAQUOPY_ABI", "")
+ if abi:
+ return abi in ("armeabi-v7a", "x86", "i686")
+ return False
if os.name == "posix":
@@ -868,6 +851,8 @@
libraries = ["m", "pthread", "dl"]
if check_linker_need_libatomic():
libraries.append("atomic")
+ if os.environ.get("CHAQUOPY_ABI", ""):
+ libraries.append("OpenSLES")
if "PYMINIAUDIO_EXTRA_CFLAGS" in os.environ:
compiler_args += shlex.split(os.environ.get("PYMINIAUDIO_EXTRA_CFLAGS", ""))
__macros = [
@@ -877,6 +862,8 @@
("MA_NO_NODE_GRAPH", "1"), # high level api
("MA_NO_ENGINE", "1"), # high level api
]
+if os.environ.get("CHAQUOPY_ABI", ""):
+ __macros.append(("MA_NO_AAUDIO", "1"))
if sys.platform == "darwin":
__macros.insert(0, ("MA_NO_RUNTIME_LINKING", None))
__link_args = ["-Wl,-needed_framework,AudioToolbox"]