Add the Fly Agent App, a Remote ID companion to DJI Fly

A Flutter app that runs next to DJI Fly without the DJI SDK. It receives the
aircraft's Remote ID (Bluetooth, BT5, Wi-Fi NaN and Beacon) in a foreground
service, streams the paired aircraft to the hub over /ws/device, and turns
take-off to landing into a logbook entry filed through /api/flights, queued
when offline. Covers phases A0-A3 of the Fly Agent plan; needs no server change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-22 19:25:52 +02:00
1 parent 415c6184b1
commit 3dd31b257a
55 files changed
+5716

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
@@ -0,0 +1,45 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.pilotvault.flyagent"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.pilotvault.flyagent"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,76 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<!-- Remote ID over Bluetooth LE. BLUETOOTH_SCAN deliberately has no
neverForLocation flag: the scan results ARE used to derive locations,
and that flag makes Android strip beacon advertisements. -->
<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.BLUETOOTH_SCAN" />
<!-- Remote ID over Wi-Fi Beacon (scan results) and Wi-Fi NaN (Aware). -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<!-- Phone GPS (pilot position) and, below Android 12/13, the gate for BLE/Wi-Fi scanning. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Keeps scanning while DJI Fly is in front. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<uses-feature android:name="android.hardware.wifi.aware" android:required="false" />
<application
android:label="PV Fly Agent"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
tools:targetApi="34">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".RidScanService"
android:exported="false"
android:foregroundServiceType="location" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<!-- To offer "Open DJI Fly" from the session screen. -->
<package android:name="dji.go.v5" />
</queries>
</manifest>
@@ -0,0 +1,110 @@
package com.pilotvault.flyagent
import android.annotation.SuppressLint
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.os.Build
import android.os.ParcelUuid
/**
* Remote ID over Bluetooth LE (ASTM F3411 / ASD-STAN EN 4709-002).
*
* The broadcast is service data under the 16-bit UUID 0xFFFA whose first byte
* is the application code 0x0D, then a message counter, then either one 25-byte
* message (BT4 legacy advertising) or a message pack (BT5 extended / Long Range).
*/
class BleRidScanner(private val context: Context) {
companion object {
const val TRANSPORT_LEGACY = "ble"
const val TRANSPORT_EXTENDED = "ble5"
private val SERVICE_UUID: ParcelUuid = ParcelUuid.fromString("0000fffa-0000-1000-8000-00805f9b34fb")
private const val APP_CODE: Byte = 0x0D
}
private var running = false
private val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) = handle(result)
override fun onBatchScanResults(results: MutableList<ScanResult>) = results.forEach(::handle)
override fun onScanFailed(errorCode: Int) {
running = false
RidHub.status(TRANSPORT_LEGACY, "failed", "scan error $errorCode")
}
}
fun capabilities(): Map<String, Boolean> {
val adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
val extended = adapter != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
adapter.isLeExtendedAdvertisingSupported
val coded = adapter != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
adapter.isLeCodedPhySupported
return mapOf("ble" to (adapter != null), "bleExtended" to extended, "bleCoded" to coded)
}
@SuppressLint("MissingPermission")
fun start() {
if (running) return
val adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
if (adapter == null) {
RidHub.status(TRANSPORT_LEGACY, "unavailable", "no Bluetooth adapter")
return
}
if (!adapter.isEnabled) {
RidHub.status(TRANSPORT_LEGACY, "unavailable", "Bluetooth is off")
return
}
val scanner = adapter.bluetoothLeScanner ?: run {
RidHub.status(TRANSPORT_LEGACY, "unavailable", "no LE scanner")
return
}
// A filter is what lets Android keep scanning with the screen off.
val filter = ScanFilter.Builder()
.setServiceData(SERVICE_UUID, byteArrayOf(APP_CODE), byteArrayOf(0xFF.toByte()))
.build()
val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && adapter.isLeExtendedAdvertisingSupported) {
// Also receive BT5 extended advertising (and Coded PHY where the radio has it).
settings.setLegacy(false).setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED)
}
try {
scanner.startScan(listOf(filter), settings.build(), callback)
running = true
RidHub.status(TRANSPORT_LEGACY, "scanning")
} catch (e: SecurityException) {
RidHub.status(TRANSPORT_LEGACY, "denied", e.message ?: "Bluetooth scan permission missing")
} catch (e: IllegalStateException) {
RidHub.status(TRANSPORT_LEGACY, "unavailable", e.message ?: "Bluetooth is off")
}
}
@SuppressLint("MissingPermission")
fun stop() {
if (!running) return
running = false
try {
(context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)
?.adapter?.bluetoothLeScanner?.stopScan(callback)
} catch (_: Exception) {
// Bluetooth was switched off underneath us; nothing left to stop.
}
RidHub.status(TRANSPORT_LEGACY, "stopped")
}
private fun handle(result: ScanResult) {
val data = result.scanRecord?.getServiceData(SERVICE_UUID) ?: return
// [app code][counter][message or pack]
if (data.size < 3 || data[0] != APP_CODE) return
val transport = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !result.isLegacy) {
TRANSPORT_EXTENDED
} else {
TRANSPORT_LEGACY
}
RidHub.frame(transport, result.device.address, result.rssi, data.copyOfRange(2, data.size))
}
}
@@ -0,0 +1,71 @@
package com.pilotvault.flyagent
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.PowerManager
import android.provider.Settings
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
/**
* Bridges the Remote ID receiver to Dart:
* * `pilotvault/rid` (methods): capabilities / start / stop / isRunning,
* battery-optimisation exemption, launching DJI Fly.
* * `pilotvault/rid/events`: raw frames and scanner status from [RidHub].
*/
class MainActivity : FlutterActivity() {
companion object {
private const val DJI_FLY_PACKAGE = "dji.go.v5"
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor.binaryMessenger
EventChannel(messenger, "pilotvault/rid/events").setStreamHandler(RidHub)
MethodChannel(messenger, "pilotvault/rid").setMethodCallHandler { call, result ->
when (call.method) {
"capabilities" -> result.success(
BleRidScanner(this).capabilities() + WifiRidScanner(this).capabilities()
)
"start" -> {
RidScanService.start(this)
result.success(true)
}
"stop" -> {
RidScanService.stop(this)
result.success(true)
}
"isRunning" -> result.success(RidScanService.running)
"isIgnoringBatteryOptimizations" -> result.success(ignoringBatteryOptimizations())
"requestIgnoreBatteryOptimizations" -> {
requestIgnoreBatteryOptimizations()
result.success(null)
}
"openDjiFly" -> result.success(openDjiFly())
else -> result.notImplemented()
}
}
}
private fun ignoringBatteryOptimizations(): Boolean {
val pm = getSystemService(POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(packageName)
}
@SuppressLint("BatteryLife")
private fun requestIgnoreBatteryOptimizations() {
if (ignoringBatteryOptimizations()) return
startActivity(
Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Uri.parse("package:$packageName"))
)
}
private fun openDjiFly(): Boolean {
val intent = packageManager.getLaunchIntentForPackage(DJI_FLY_PACKAGE) ?: return false
startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
return true
}
}
@@ -0,0 +1,50 @@
package com.pilotvault.flyagent
import android.os.Handler
import android.os.Looper
import io.flutter.plugin.common.EventChannel
/**
* Hands raw Remote ID frames from the scanners to Dart.
*
* The scanners live in [RidScanService] and outlive the Flutter activity's
* event channel listener, so they post here and this drops frames while no
* one is listening. Decoding happens in Dart (`lib/rid/odid_decoder.dart`):
* the scanners only strip the transport framing down to the first OpenDroneID
* byte, so the decoder sees the same bytes whichever transport carried them.
*/
object RidHub : EventChannel.StreamHandler {
private val main = Handler(Looper.getMainLooper())
@Volatile private var sink: EventChannel.EventSink? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
sink = events
}
override fun onCancel(arguments: Any?) {
sink = null
}
/** One received broadcast. [payload] starts at an ODID message or message pack. */
fun frame(transport: String, source: String, rssi: Int, payload: ByteArray) {
emit(
mapOf(
"kind" to "frame",
"transport" to transport,
"source" to source,
"rssi" to rssi,
"ts" to System.currentTimeMillis(),
"payload" to payload,
)
)
}
/** A scanner started, stopped or failed; shown on the receiver's debug screen. */
fun status(transport: String, state: String, detail: String = "") {
emit(mapOf("kind" to "status", "transport" to transport, "state" to state, "detail" to detail))
}
private fun emit(event: Map<String, Any>) {
main.post { sink?.success(event) }
}
}
@@ -0,0 +1,98 @@
package com.pilotvault.flyagent
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
/**
* Foreground service that owns the Remote ID scanners, so they keep running
* (and the process stays alive for the Dart uplink) while DJI Fly is in front.
*/
class RidScanService : Service() {
companion object {
private const val CHANNEL_ID = "rid_scan"
private const val NOTIFICATION_ID = 1
@Volatile var running = false
private set
fun start(context: Context) {
val intent = Intent(context, RidScanService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
fun stop(context: Context) {
context.stopService(Intent(context, RidScanService::class.java))
}
}
private lateinit var ble: BleRidScanner
private lateinit var wifi: WifiRidScanner
override fun onCreate() {
super.onCreate()
ble = BleRidScanner(this)
wifi = WifiRidScanner(this)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildNotification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION)
} else {
startForeground(NOTIFICATION_ID, notification)
}
if (!running) {
running = true
ble.start()
wifi.start()
}
return START_STICKY
}
override fun onDestroy() {
running = false
ble.stop()
wifi.stop()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
private fun buildNotification(): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
manager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, "Remote ID receiver", NotificationManager.IMPORTANCE_LOW)
)
}
val open = PendingIntent.getActivity(
this, 0,
Intent(this, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_IMMUTABLE,
)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, CHANNEL_ID)
} else {
@Suppress("DEPRECATION")
Notification.Builder(this)
}
return builder
.setContentTitle("PilotVault Fly Agent")
.setContentText("Listening for Remote ID")
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
.setContentIntent(open)
.setOngoing(true)
.build()
}
}
@@ -0,0 +1,197 @@
package com.pilotvault.flyagent
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.net.wifi.WifiManager
import android.net.wifi.aware.AttachCallback
import android.net.wifi.aware.DiscoverySessionCallback
import android.net.wifi.aware.PeerHandle
import android.net.wifi.aware.SubscribeConfig
import android.net.wifi.aware.SubscribeDiscoverySession
import android.net.wifi.aware.WifiAwareManager
import android.net.wifi.aware.WifiAwareSession
import android.os.Build
import android.os.Handler
import android.os.Looper
/**
* Remote ID over Wi-Fi: NaN (Neighbour Awareness Networking) and Beacon.
*
* NaN: a subscribe session for the service "org.opendroneid.remoteid"; the
* service-specific info is [counter][message pack].
*
* Beacon: a vendor-specific information element (id 221) with the ASD-STAN OUI
* FA:0B:BC and OUI type 0x0D, then [counter][message pack]. Information elements
* are only exposed from Android 11, and Android throttles foreground Wi-Fi scans
* to four per two minutes unless the user turns throttling off in developer
* options, so Beacon gives sparse updates. Results from other apps' scans are
* picked up too.
*/
class WifiRidScanner(private val context: Context) {
companion object {
const val TRANSPORT_NAN = "nan"
const val TRANSPORT_BEACON = "beacon"
private const val NAN_SERVICE = "org.opendroneid.remoteid"
private const val IE_VENDOR_SPECIFIC = 221
private val BEACON_OUI = byteArrayOf(0xFA.toByte(), 0x0B, 0xBC.toByte(), 0x0D)
private const val BEACON_SCAN_INTERVAL_MS = 30_000L
}
private val main = Handler(Looper.getMainLooper())
private val wifi = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
private var awareSession: WifiAwareSession? = null
private var subscribe: SubscribeDiscoverySession? = null
private var beaconRunning = false
fun capabilities(): Map<String, Boolean> = mapOf(
"nan" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)),
"beacon" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R),
)
fun start() {
startNan()
startBeacon()
}
fun stop() {
stopNan()
stopBeacon()
}
// ── NaN ────────────────────────────────────────────────────────────────
@SuppressLint("MissingPermission")
private fun startNan() {
if (awareSession != null) return
if (capabilities()["nan"] != true) {
RidHub.status(TRANSPORT_NAN, "unavailable", "this phone has no Wi-Fi Aware")
return
}
val manager = context.getSystemService(Context.WIFI_AWARE_SERVICE) as? WifiAwareManager
if (manager == null || !manager.isAvailable) {
RidHub.status(TRANSPORT_NAN, "unavailable", "Wi-Fi Aware is off (Wi-Fi or location off?)")
return
}
try {
manager.attach(object : AttachCallback() {
override fun onAttached(session: WifiAwareSession) {
awareSession = session
val config = SubscribeConfig.Builder()
.setServiceName(NAN_SERVICE)
.setSubscribeType(SubscribeConfig.SUBSCRIBE_TYPE_PASSIVE)
.build()
session.subscribe(config, object : DiscoverySessionCallback() {
override fun onSubscribeStarted(session: SubscribeDiscoverySession) {
subscribe = session
RidHub.status(TRANSPORT_NAN, "scanning")
}
override fun onServiceDiscovered(
peerHandle: PeerHandle,
serviceSpecificInfo: ByteArray?,
matchFilter: MutableList<ByteArray>?,
) {
val info = serviceSpecificInfo ?: return
if (info.size < 2) return
// NaN hides the peer's MAC; the handle is stable for the session.
RidHub.frame(TRANSPORT_NAN, "nan-${peerHandle.hashCode()}", 0, info.copyOfRange(1, info.size))
}
override fun onSessionTerminated() {
subscribe = null
RidHub.status(TRANSPORT_NAN, "stopped", "session terminated")
}
}, main)
}
override fun onAttachFailed() {
RidHub.status(TRANSPORT_NAN, "failed", "attach failed")
}
}, main)
} catch (e: SecurityException) {
RidHub.status(TRANSPORT_NAN, "denied", e.message ?: "nearby Wi-Fi permission missing")
}
}
private fun stopNan() {
subscribe?.close()
subscribe = null
if (awareSession != null) {
awareSession?.close()
awareSession = null
RidHub.status(TRANSPORT_NAN, "stopped")
}
}
// ── Beacon ─────────────────────────────────────────────────────────────
private val scanReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) = readScanResults()
}
private val scanTick = object : Runnable {
override fun run() {
if (!beaconRunning) return
@Suppress("DEPRECATION")
try {
wifi.startScan()
} catch (_: SecurityException) {
}
main.postDelayed(this, BEACON_SCAN_INTERVAL_MS)
}
}
private fun startBeacon() {
if (beaconRunning) return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
RidHub.status(TRANSPORT_BEACON, "unavailable", "needs Android 11")
return
}
if (!wifi.isWifiEnabled && !wifi.isScanAlwaysAvailable) {
RidHub.status(TRANSPORT_BEACON, "unavailable", "Wi-Fi is off")
return
}
context.registerReceiver(scanReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
beaconRunning = true
main.post(scanTick)
RidHub.status(TRANSPORT_BEACON, "scanning", "every ${BEACON_SCAN_INTERVAL_MS / 1000} s (Android throttles Wi-Fi scans)")
}
private fun stopBeacon() {
if (!beaconRunning) return
beaconRunning = false
main.removeCallbacks(scanTick)
try {
context.unregisterReceiver(scanReceiver)
} catch (_: IllegalArgumentException) {
}
RidHub.status(TRANSPORT_BEACON, "stopped")
}
@SuppressLint("MissingPermission")
private fun readScanResults() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return
val results = try {
wifi.scanResults
} catch (e: SecurityException) {
RidHub.status(TRANSPORT_BEACON, "denied", e.message ?: "location permission missing")
return
}
for (r in results) {
for (ie in r.informationElements) {
if (ie.id != IE_VENDOR_SPECIFIC) continue
val buf = ie.bytes
val bytes = ByteArray(buf.remaining()).also { buf.duplicate().get(it) }
// [OUI x3][OUI type][counter][message pack]
if (bytes.size < BEACON_OUI.size + 2) continue
if (!bytes.copyOfRange(0, BEACON_OUI.size).contentEquals(BEACON_OUI)) continue
RidHub.frame(TRANSPORT_BEACON, r.BSSID ?: "", r.level, bytes.copyOfRange(BEACON_OUI.size + 1, bytes.size))
}
}
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+10
View File
@@ -0,0 +1,10 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
# The project (E:) and the pub cache (C:) sit on different drives, which breaks
# Kotlin incremental caches ("Could not close incremental caches").
kotlin.incremental=false
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")