Skip to content

Commit 8086601

Browse files
committed
[green] Harden Android Blockly runtime
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent d52e341 commit 8086601

13 files changed

Lines changed: 417 additions & 80 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,15 @@ jobs:
347347
"ANDROID_NDK_PATH=$ANDROID_NDK_PATH" \
348348
"ANDROID_NDK_LATEST_HOME=$ANDROID_NDK_LATEST_HOME" \
349349
>> "$GITHUB_ENV"
350+
- name: Production release APK build (no integration-test plugin)
351+
shell: bash
352+
run: |
353+
set -euo pipefail
354+
flutter build apk --release --flavor production --target lib/main.dart
355+
PYBLE_AAPT="$PYBLE_ANDROID_SDK_ROOT/build-tools/36.0.0/aapt"
356+
PYBLE_APK="build/app/outputs/flutter-apk/app-production-release.apk"
357+
"$PYBLE_AAPT" dump badging "$PYBLE_APK" \
358+
| grep -F "package: name='dev.pyble.pyble'"
350359
- name: Create API 34 AOSP x86_64 tablet AVD
351360
shell: bash
352361
run: |
@@ -387,6 +396,7 @@ jobs:
387396
timeout --signal=TERM --kill-after=30s 18m \
388397
flutter build apk \
389398
--debug \
399+
--flavor integration \
390400
--no-pub \
391401
--target-platform android-x64 \
392402
--target integration_test/android_smoke_test.dart \
@@ -400,7 +410,7 @@ jobs:
400410
if [ "$PREBUILD_STATUS" -ne 0 ]; then
401411
exit "$PREBUILD_STATUS"
402412
fi
403-
test -s build/app/outputs/flutter-apk/app-debug.apk
413+
test -s build/app/outputs/flutter-apk/app-integration-debug.apk
404414
- name: Boot headless emulator (KVM + software GPU)
405415
shell: bash
406416
run: |
@@ -564,7 +574,8 @@ jobs:
564574
--no-pub \
565575
--driver test_driver/integration_test.dart \
566576
--target integration_test/android_smoke_test.dart \
567-
--use-application-binary build/app/outputs/flutter-apk/app-debug.apk \
577+
--flavor integration \
578+
--use-application-binary build/app/outputs/flutter-apk/app-integration-debug.apk \
568579
--timeout 660 \
569580
-d "$PYBLE_ANDROID_SERIAL" \
570581
>"$PYBLE_ANDROID_LOG_DIR/flutter-drive.log" 2>&1 \

‎CONTRIBUTING.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,27 @@ flutter test --tags golden
8686

8787
Use a recent stable Flutter SDK matching the version pinned by CI.
8888

89+
Use the normal package directly on iOS:
90+
91+
```sh
92+
flutter run -d <ios-device>
93+
```
94+
95+
Always name an Android flavor. `production` is the normal launcher, while
96+
`integration` uses the disposable `dev.pyble.pyble.integrationtest` package:
97+
98+
```sh
99+
flutter run --flavor production --target lib/main.dart -d <android-device>
100+
flutter build apk --release --flavor production --target lib/main.dart
101+
flutter drive --flavor integration \
102+
--driver test_driver/integration_test.dart \
103+
--target integration_test/android_smoke_test.dart \
104+
-d <android-device>
105+
```
106+
107+
The current local production-flavor release build uses Gradle's debug signing
108+
configuration for device testing. It is not a distributable beta artifact.
109+
89110
### Firmware
90111

91112
Host-side protocol and release tests do not require hardware:

‎app/android/app/build.gradle.kts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,22 @@ android {
2525
versionName = flutter.versionName
2626
}
2727

28+
flavorDimensions += "purpose"
29+
productFlavors {
30+
create("production") {
31+
dimension = "purpose"
32+
}
33+
create("integration") {
34+
dimension = "purpose"
35+
applicationIdSuffix = ".integrationtest"
36+
}
37+
}
38+
2839
buildTypes {
2940
release {
3041
// TODO: Add your own signing config for the release build.
31-
// Signing with the debug keys for now, so `flutter run --release` works.
42+
// Signing with debug keys for now, so
43+
// `flutter run --release --flavor production` works.
3244
signingConfig = signingConfigs.getByName("debug")
3345
}
3446
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:tools="http://schemas.android.com/tools">
3+
<application
4+
android:label="PyBLE Integration Test"
5+
tools:replace="android:label" />
6+
</manifest>

‎app/android/app/src/main/AndroidManifest.xml‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,32 @@
44
The flutter_blue_plus_android plugin manifest is intentionally blank, so
55
the app declares these itself. neverForLocation: the scan is filtered to
66
the PyBLE service UUID and derives no location (startScan is called with
7-
androidUsesFineLocation: false). ACCESS_FINE_LOCATION is legacy-only,
8-
capped at Android 11 (API 30). -->
7+
androidUsesFineLocation: false). The app is BLE-first, so BLE hardware
8+
is required. Classic Bluetooth and location hardware remain optional:
9+
those permissions are only older-Android BLE scan prerequisites.
10+
Bluetooth/Fine-Location declarations are legacy-only and capped at
11+
Android 11 (API 30); Coarse Location is capped at Android 9 (API 28),
12+
matching the pinned adapter contract. -->
13+
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
14+
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
15+
<uses-feature android:name="android.hardware.location" android:required="false" />
916
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
1017
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
18+
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
19+
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
1120
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
21+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
1222
<application
1323
android:label="PyBLE"
1424
android:name="${applicationName}"
1525
android:icon="@mipmap/ic_launcher">
26+
<!-- Flutter 3.44 Impeller/Vulkan renders a blank frame on the validated
27+
Lenovo TB-J616X (MediaTek, Android 12). Keep the supported legacy
28+
OpenGL renderer until the physical Android gate passes on a later
29+
Flutter engine. See NFR-COMPAT-4. -->
30+
<meta-data
31+
android:name="io.flutter.embedding.android.EnableImpeller"
32+
android:value="false" />
1633
<activity
1734
android:name=".MainActivity"
1835
android:exported="true"

‎app/assets/blockly/pyble_blockly.js‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@
670670

671671
let workspace;
672672
let revision = 0;
673+
let hostEpoch;
673674
let snapshotScheduled = false;
674675
let pendingRestore;
675676
let resizeFrame = 0;
@@ -678,7 +679,9 @@
678679
function postMessage(message) {
679680
const channel = window.PybleBlocks;
680681
if (channel && typeof channel.postMessage === "function") {
681-
channel.postMessage(JSON.stringify(message));
682+
const envelope =
683+
message.type === "hostReady" ? message : { ...message, hostEpoch };
684+
channel.postMessage(JSON.stringify(envelope));
682685
}
683686
}
684687

@@ -892,10 +895,17 @@
892895
return value;
893896
}
894897

895-
function configureHost(messages, workspaceJson, priorRevision) {
898+
function configureHost(messages, dartHostEpoch, workspaceJson, priorRevision) {
896899
if (workspace) {
897900
throw new Error("Blockly host is already configured.");
898901
}
902+
if (
903+
!Number.isSafeInteger(dartHostEpoch) ||
904+
dartHostEpoch < 1 ||
905+
dartHostEpoch > maxRevision
906+
) {
907+
throw new TypeError("Dart host epoch must be a positive safe integer.");
908+
}
899909
if (!messages || typeof messages !== "object" || Array.isArray(messages)) {
900910
throw new TypeError("Blockly host messages must be an object.");
901911
}
@@ -1049,6 +1059,7 @@
10491059
registerGpioBlocks();
10501060
registerNeopixelBlocks();
10511061
registerTimeBlocks();
1062+
hostEpoch = dartHostEpoch;
10521063
pendingRestore = initialRestore;
10531064
initialise();
10541065
return true;
@@ -1060,6 +1071,7 @@
10601071
restore,
10611072
snapshot: publishSnapshot,
10621073
});
1074+
postMessage({ version: bridgeVersion, type: "hostReady" });
10631075

10641076
function resizeWorkspace() {
10651077
if (!workspace) {

0 commit comments

Comments
 (0)