diff --git a/flutter/.gitattributes b/flutter/.gitattributes
new file mode 100644
index 000000000..176a458f9
--- /dev/null
+++ b/flutter/.gitattributes
@@ -0,0 +1 @@
+* text=auto
diff --git a/flutter/.gitignore b/flutter/.gitignore
new file mode 100644
index 000000000..72307b910
--- /dev/null
+++ b/flutter/.gitignore
@@ -0,0 +1,44 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+jniLibs
+
+.vscode
\ No newline at end of file
diff --git a/flutter/.metadata b/flutter/.metadata
new file mode 100644
index 000000000..107fcb7b5
--- /dev/null
+++ b/flutter/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 8874f21e79d7ec66d0457c7ab338348e31b17f1d
+ channel: stable
+
+project_type: app
diff --git a/flutter/android/.gitignore b/flutter/android/.gitignore
new file mode 100644
index 000000000..0a741cb43
--- /dev/null
+++ b/flutter/android/.gitignore
@@ -0,0 +1,11 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle
new file mode 100644
index 000000000..79bf6426a
--- /dev/null
+++ b/flutter/android/app/build.gradle
@@ -0,0 +1,82 @@
+def keystoreProperties = new Properties()
+def keystorePropertiesFile = rootProject.file('key.properties')
+if (keystorePropertiesFile.exists()) {
+ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+}
+
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 31
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ compileOptions {
+ targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_1_8
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "com.carriez.flutter_hbb"
+ minSdkVersion 21
+ targetSdkVersion 31
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ signingConfigs {
+ release {
+ keyAlias keystoreProperties['keyAlias']
+ keyPassword keystoreProperties['keyPassword']
+ storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
+ storePassword keystoreProperties['storePassword']
+ }
+ }
+
+ 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.release
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ implementation "androidx.media:media:1.4.3"
+ implementation 'com.github.getActivity:XXPermissions:13.2'
+ implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
+}
+
+apply plugin: 'com.google.gms.google-services'
diff --git a/flutter/android/app/google-services.json b/flutter/android/app/google-services.json
new file mode 100644
index 000000000..3945e432a
--- /dev/null
+++ b/flutter/android/app/google-services.json
@@ -0,0 +1,40 @@
+{
+ "project_info": {
+ "project_number": "768133699366",
+ "firebase_url": "https://rustdesk.firebaseio.com",
+ "project_id": "rustdesk",
+ "storage_bucket": "rustdesk.appspot.com"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:768133699366:android:5fc9015370e344457993e7",
+ "android_client_info": {
+ "package_name": "com.carriez.flutter_hbb"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAPOsKcXjrAR-7Z148sYr_gdB_JQZkamTM"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/flutter/android/app/src/debug/AndroidManifest.xml b/flutter/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 000000000..64d68a588
--- /dev/null
+++ b/flutter/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c552efae5
--- /dev/null
+++ b/flutter/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt
new file mode 100644
index 000000000..328701567
--- /dev/null
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt
@@ -0,0 +1,23 @@
+package com.carriez.flutter_hbb
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.widget.Toast
+
+class BootReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if ("android.intent.action.BOOT_COMPLETED" == intent.action){
+ val it = Intent(context,MainService::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(it)
+ }else{
+ context.startService(it)
+ }
+ }
+ }
+}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
new file mode 100644
index 000000000..e061037db
--- /dev/null
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
@@ -0,0 +1,221 @@
+package com.carriez.flutter_hbb
+
+import android.accessibilityservice.AccessibilityService
+import android.accessibilityservice.GestureDescription
+import android.content.Context
+import android.graphics.Path
+import android.os.Build
+import android.util.Log
+import android.view.accessibility.AccessibilityEvent
+import androidx.annotation.Keep
+import androidx.annotation.RequiresApi
+import java.util.*
+
+const val LIFT_DOWN = 9
+const val LIFT_MOVE = 8
+const val LIFT_UP = 10
+const val RIGHT_UP = 18
+const val WHEEL_BUTTON_DOWN = 33
+const val WHEEL_BUTTON_UP = 34
+const val WHEEL_DOWN = 523331
+const val WHEEL_UP = 963
+
+const val WHEEL_STEP = 120
+const val WHEEL_DURATION = 50L
+const val LONG_TAP_DELAY = 200L
+
+class InputService : AccessibilityService() {
+
+ companion object {
+ var ctx: InputService? = null
+ val isOpen: Boolean
+ get() = ctx != null
+ }
+
+ private external fun init(ctx: Context)
+
+ init {
+ System.loadLibrary("rustdesk")
+ }
+
+ private val logTag = "input service"
+ private var leftIsDown = false
+ private var touchPath = Path()
+ private var lastTouchGestureStartTime = 0L
+ private var mouseX = 0
+ private var mouseY = 0
+ private var timer = Timer()
+ private var recentActionTask: TimerTask? = null
+
+ private val wheelActionsQueue = LinkedList()
+ private var isWheelActionsPolling = false
+
+ @Keep
+ @RequiresApi(Build.VERSION_CODES.N)
+ fun rustMouseInput(mask: Int, _x: Int, _y: Int) {
+ val x = if (_x < 0) {
+ 0
+ } else {
+ _x
+ }
+
+ val y = if (_y < 0) {
+ 0
+ } else {
+ _y
+ }
+
+ if (mask == 0 || mask == LIFT_MOVE) {
+ mouseX = x * SCREEN_INFO.scale
+ mouseY = y * SCREEN_INFO.scale
+ }
+
+ // left button down ,was up
+ if (mask == LIFT_DOWN) {
+ leftIsDown = true
+ startGesture(mouseX, mouseY)
+ return
+ }
+
+ // left down ,was down
+ if (leftIsDown) {
+ continueGesture(mouseX, mouseY)
+ }
+
+ // left up ,was down
+ if (mask == LIFT_UP) {
+ leftIsDown = false
+ endGesture(mouseX, mouseY)
+ return
+ }
+
+ if (mask == RIGHT_UP) {
+ performGlobalAction(GLOBAL_ACTION_BACK)
+ return
+ }
+
+ // long WHEEL_BUTTON_DOWN -> GLOBAL_ACTION_RECENTS
+ if (mask == WHEEL_BUTTON_DOWN) {
+ timer.purge()
+ recentActionTask = object : TimerTask() {
+ override fun run() {
+ performGlobalAction(GLOBAL_ACTION_RECENTS)
+ recentActionTask = null
+ }
+ }
+ timer.schedule(recentActionTask, LONG_TAP_DELAY)
+ }
+
+ // wheel button up
+ if (mask == WHEEL_BUTTON_UP) {
+ if (recentActionTask != null) {
+ recentActionTask!!.cancel()
+ performGlobalAction(GLOBAL_ACTION_HOME)
+ }
+ return
+ }
+
+ if (mask == WHEEL_DOWN) {
+ if (mouseY < WHEEL_STEP) {
+ return
+ }
+ val path = Path()
+ path.moveTo(mouseX.toFloat(), mouseY.toFloat())
+ path.lineTo(mouseX.toFloat(), (mouseY - WHEEL_STEP).toFloat())
+ val stroke = GestureDescription.StrokeDescription(
+ path,
+ 0,
+ WHEEL_DURATION
+ )
+ val builder = GestureDescription.Builder()
+ builder.addStroke(stroke)
+ wheelActionsQueue.offer(builder.build())
+ consumeWheelActions()
+
+ }
+
+ if (mask == WHEEL_UP) {
+ if (mouseY < WHEEL_STEP) {
+ return
+ }
+ val path = Path()
+ path.moveTo(mouseX.toFloat(), mouseY.toFloat())
+ path.lineTo(mouseX.toFloat(), (mouseY + WHEEL_STEP).toFloat())
+ val stroke = GestureDescription.StrokeDescription(
+ path,
+ 0,
+ WHEEL_DURATION
+ )
+ val builder = GestureDescription.Builder()
+ builder.addStroke(stroke)
+ wheelActionsQueue.offer(builder.build())
+ consumeWheelActions()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ private fun consumeWheelActions() {
+ if (isWheelActionsPolling) {
+ return
+ } else {
+ isWheelActionsPolling = true
+ }
+ wheelActionsQueue.poll()?.let {
+ dispatchGesture(it, null, null)
+ timer.purge()
+ timer.schedule(object : TimerTask() {
+ override fun run() {
+ isWheelActionsPolling = false
+ consumeWheelActions()
+ }
+ }, WHEEL_DURATION + 10)
+ } ?: let {
+ isWheelActionsPolling = false
+ return
+ }
+ }
+
+ private fun startGesture(x: Int, y: Int) {
+ touchPath = Path()
+ touchPath.moveTo(x.toFloat(), y.toFloat())
+ lastTouchGestureStartTime = System.currentTimeMillis()
+ }
+
+ private fun continueGesture(x: Int, y: Int) {
+ touchPath.lineTo(x.toFloat(), y.toFloat())
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ private fun endGesture(x: Int, y: Int) {
+ try {
+ touchPath.lineTo(x.toFloat(), y.toFloat())
+ var duration = System.currentTimeMillis() - lastTouchGestureStartTime
+ if (duration <= 0) {
+ duration = 1
+ }
+ val stroke = GestureDescription.StrokeDescription(
+ touchPath,
+ 0,
+ duration
+ )
+ val builder = GestureDescription.Builder()
+ builder.addStroke(stroke)
+ Log.d(logTag, "end gesture x:$x y:$y time:$duration")
+ dispatchGesture(builder.build(), null, null)
+ } catch (e: Exception) {
+ Log.e(logTag, "endGesture error:$e")
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun onServiceConnected() {
+ super.onServiceConnected()
+ ctx = this
+ Log.d(logTag, "onServiceConnected!")
+ init(this)
+ }
+
+ override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
+
+ override fun onInterrupt() {}
+}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
new file mode 100644
index 000000000..bb4e85c89
--- /dev/null
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
@@ -0,0 +1,229 @@
+package com.carriez.flutter_hbb
+
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.media.projection.MediaProjectionManager
+import android.os.Build
+import android.os.IBinder
+import android.provider.Settings
+import android.util.Log
+import android.view.WindowManager
+import androidx.annotation.RequiresApi
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodChannel
+
+const val MEDIA_REQUEST_CODE = 42
+
+class MainActivity : FlutterActivity() {
+ companion object {
+ lateinit var flutterMethodChannel: MethodChannel
+ }
+
+ private val channelTag = "mChannel"
+ private val logTag = "mMainActivity"
+ private var mediaProjectionResultIntent: Intent? = null
+ private var mainService: MainService? = null
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+ if (MainService.isReady) {
+ Intent(activity, MainService::class.java).also {
+ bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
+ }
+ }
+ flutterMethodChannel = MethodChannel(
+ flutterEngine.dartExecutor.binaryMessenger,
+ channelTag
+ ).apply {
+ // make sure result is set, otherwise flutter will await forever
+ setMethodCallHandler { call, result ->
+ when (call.method) {
+ "init_service" -> {
+ Intent(activity, MainService::class.java).also {
+ bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
+ }
+ if (MainService.isReady) {
+ result.success(false)
+ return@setMethodCallHandler
+ }
+ getMediaProjection()
+ result.success(true)
+ }
+ "start_capture" -> {
+ mainService?.let {
+ result.success(it.startCapture())
+ } ?: let {
+ result.success(false)
+ }
+ }
+ "stop_service" -> {
+ Log.d(logTag, "Stop service")
+ mainService?.let {
+ it.destroy()
+ result.success(true)
+ } ?: let {
+ result.success(false)
+ }
+ }
+ "check_permission" -> {
+ if (call.arguments is String) {
+ result.success(checkPermission(context, call.arguments as String))
+ } else {
+ result.success(false)
+ }
+ }
+ "request_permission" -> {
+ if (call.arguments is String) {
+ requestPermission(context, call.arguments as String)
+ result.success(true)
+ } else {
+ result.success(false)
+ }
+ }
+ "check_video_permission" -> {
+ mainService?.let {
+ result.success(it.checkMediaPermission())
+ } ?: let {
+ result.success(false)
+ }
+ }
+ "check_service" -> {
+ flutterMethodChannel.invokeMethod(
+ "on_state_changed",
+ mapOf("name" to "input", "value" to InputService.isOpen.toString())
+ )
+ flutterMethodChannel.invokeMethod(
+ "on_state_changed",
+ mapOf("name" to "media", "value" to MainService.isReady.toString())
+ )
+ result.success(true)
+ }
+ "init_input" -> {
+ initInput()
+ result.success(true)
+ }
+ "stop_input" -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ InputService.ctx?.disableSelf()
+ }
+ InputService.ctx = null
+ flutterMethodChannel.invokeMethod(
+ "on_state_changed",
+ mapOf("name" to "input", "value" to InputService.isOpen.toString())
+ )
+ result.success(true)
+ }
+ "cancel_notification" -> {
+ try {
+ val id = call.arguments as Int
+ mainService?.cancelNotification(id)
+ } finally {
+ result.success(true)
+ }
+ }
+ "enable_soft_keyboard" -> {
+ // https://blog.csdn.net/hanye2020/article/details/105553780
+ try {
+ if (call.arguments as Boolean) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
+ } else {
+ window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
+ }
+ } finally {
+ result.success(true)
+ }
+ }
+ else -> {
+ result.error("-1", "No such method", null)
+ }
+ }
+ }
+ }
+ }
+
+ private fun getMediaProjection() {
+ val mMediaProjectionManager =
+ getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
+ val mIntent = mMediaProjectionManager.createScreenCaptureIntent()
+ startActivityForResult(mIntent, MEDIA_REQUEST_CODE)
+ }
+
+ private fun initService() {
+ if (mediaProjectionResultIntent == null) {
+ Log.w(logTag, "initService fail,mediaProjectionResultIntent is null")
+ return
+ }
+ Log.d(logTag, "Init service")
+ val serviceIntent = Intent(this, MainService::class.java)
+ serviceIntent.action = INIT_SERVICE
+ serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent)
+
+ launchMainService(serviceIntent)
+ }
+
+ private fun launchMainService(intent: Intent) {
+ // TEST api < O
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(intent)
+ } else {
+ startService(intent)
+ }
+ }
+
+ private fun initInput() {
+ val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
+ if (intent.resolveActivity(packageManager) != null) {
+ startActivity(intent)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val inputPer = InputService.isOpen
+ Log.d(logTag, "onResume inputPer:$inputPer")
+ activity.runOnUiThread {
+ flutterMethodChannel.invokeMethod(
+ "on_state_changed",
+ mapOf("name" to "input", "value" to inputPer.toString())
+ )
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == MEDIA_REQUEST_CODE) {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ mediaProjectionResultIntent = data
+ initService()
+ } else {
+ flutterMethodChannel.invokeMethod("on_media_projection_canceled", null)
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ Log.e(logTag, "onDestroy")
+ mainService?.let {
+ unbindService(serviceConnection)
+ }
+ super.onDestroy()
+ }
+
+ private val serviceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ Log.d(logTag, "onServiceConnected")
+ val binder = service as MainService.LocalBinder
+ mainService = binder.getService()
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ Log.d(logTag, "onServiceDisconnected")
+ mainService = null
+ }
+ }
+}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
new file mode 100644
index 000000000..4a1b8c06f
--- /dev/null
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
@@ -0,0 +1,670 @@
+/**
+ * Capture screen,get video and audio,send to rust.
+ * Handle notification
+ */
+package com.carriez.flutter_hbb
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.*
+import android.app.PendingIntent.FLAG_IMMUTABLE
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.graphics.Color
+import android.graphics.PixelFormat
+import android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
+import android.hardware.display.VirtualDisplay
+import android.media.*
+import android.media.projection.MediaProjection
+import android.media.projection.MediaProjectionManager
+import android.os.*
+import android.util.DisplayMetrics
+import android.util.Log
+import android.view.Surface
+import android.view.Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
+import android.view.WindowManager
+import androidx.annotation.Keep
+import androidx.annotation.RequiresApi
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.content.ContextCompat
+import java.util.concurrent.Executors
+import kotlin.concurrent.thread
+import org.json.JSONException
+import org.json.JSONObject
+import java.nio.ByteBuffer
+import kotlin.math.max
+import kotlin.math.min
+
+const val EXTRA_MP_DATA = "mp_intent"
+const val INIT_SERVICE = "init_service"
+const val ACTION_LOGIN_REQ_NOTIFY = "ACTION_LOGIN_REQ_NOTIFY"
+const val EXTRA_LOGIN_REQ_NOTIFY = "EXTRA_LOGIN_REQ_NOTIFY"
+
+const val DEFAULT_NOTIFY_TITLE = "RustDesk"
+const val DEFAULT_NOTIFY_TEXT = "Service is running"
+const val DEFAULT_NOTIFY_ID = 1
+const val NOTIFY_ID_OFFSET = 100
+
+const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
+
+// video const
+const val MAX_SCREEN_SIZE = 1200
+
+const val VIDEO_KEY_BIT_RATE = 1024_000
+const val VIDEO_KEY_FRAME_RATE = 30
+
+// audio const
+const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
+const val AUDIO_SAMPLE_RATE = 48000
+const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
+
+class MainService : Service() {
+
+ init {
+ System.loadLibrary("rustdesk")
+ }
+
+ @Keep
+ fun rustGetByName(name: String): String {
+ return when (name) {
+ "screen_size" -> "${SCREEN_INFO.width}:${SCREEN_INFO.height}"
+ else -> ""
+ }
+ }
+
+ @Keep
+ fun rustSetByName(name: String, arg1: String, arg2: String) {
+ when (name) {
+ "try_start_without_auth" -> {
+ try {
+ val jsonObject = JSONObject(arg1)
+ val id = jsonObject["id"] as Int
+ val username = jsonObject["name"] as String
+ val peerId = jsonObject["peer_id"] as String
+ val type = if (jsonObject["is_file_transfer"] as Boolean) {
+ translate("File Connection")
+ } else {
+ translate("Screen Connection")
+ }
+ loginRequestNotification(id, type, username, peerId)
+ } catch (e: JSONException) {
+ e.printStackTrace()
+ }
+ }
+ "on_client_authorized" -> {
+ Log.d(logTag, "from rust:on_client_authorized")
+ try {
+ val jsonObject = JSONObject(arg1)
+ val id = jsonObject["id"] as Int
+ val username = jsonObject["name"] as String
+ val peerId = jsonObject["peer_id"] as String
+ val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
+ val type = if (isFileTransfer) {
+ translate("File Connection")
+ } else {
+ translate("Screen Connection")
+ }
+ if (!isFileTransfer && !isStart) {
+ startCapture()
+ }
+ onClientAuthorizedNotification(id, type, username, peerId)
+ } catch (e: JSONException) {
+ e.printStackTrace()
+ }
+
+ }
+ "stop_capture" -> {
+ Log.d(logTag, "from rust:stop_capture")
+ stopCapture()
+ }
+ else -> {
+ }
+ }
+ }
+
+ private var serviceLooper: Looper? = null
+ private var serviceHandler: Handler? = null
+
+ // jvm call rust
+ private external fun init(ctx: Context)
+ private external fun startServer()
+ private external fun onVideoFrameUpdate(buf: ByteBuffer)
+ private external fun onAudioFrameUpdate(buf: ByteBuffer)
+ private external fun translateLocale(localeName: String, input: String): String
+ private external fun refreshScreen()
+ private external fun setFrameRawEnable(name: String, value: Boolean)
+ // private external fun sendVp9(data: ByteArray)
+
+ private fun translate(input: String): String {
+ Log.d(logTag, "translate:$LOCAL_NAME")
+ return translateLocale(LOCAL_NAME, input)
+ }
+
+ companion object {
+ private var _isReady = false // media permission ready status
+ private var _isStart = false // screen capture start status
+ val isReady: Boolean
+ get() = _isReady
+ val isStart: Boolean
+ get() = _isStart
+ }
+
+ private val logTag = "LOG_SERVICE"
+ private val useVP9 = false
+ private val binder = LocalBinder()
+
+
+ // video
+ private var mediaProjection: MediaProjection? = null
+ private var surface: Surface? = null
+ private val sendVP9Thread = Executors.newSingleThreadExecutor()
+ private var videoEncoder: MediaCodec? = null
+ private var imageReader: ImageReader? = null
+ private var virtualDisplay: VirtualDisplay? = null
+
+ // audio
+ private var audioRecorder: AudioRecord? = null
+ private var audioReader: AudioReader? = null
+ private var minBufferSize = 0
+ private var audioRecordStat = false
+
+ // notification
+ private lateinit var notificationManager: NotificationManager
+ private lateinit var notificationChannel: String
+ private lateinit var notificationBuilder: NotificationCompat.Builder
+
+ override fun onCreate() {
+ super.onCreate()
+ HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
+ start()
+ serviceLooper = looper
+ serviceHandler = Handler(looper)
+ }
+ updateScreenInfo(resources.configuration.orientation)
+ initNotification()
+ startServer()
+ }
+
+ override fun onDestroy() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ InputService.ctx?.disableSelf()
+ }
+ InputService.ctx = null
+ checkMediaPermission()
+ super.onDestroy()
+ }
+
+ private fun updateScreenInfo(orientation: Int) {
+ var w: Int
+ var h: Int
+ var dpi: Int
+ val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
+
+ @Suppress("DEPRECATION")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val m = windowManager.maximumWindowMetrics
+ w = m.bounds.width()
+ h = m.bounds.height()
+ dpi = resources.configuration.densityDpi
+ } else {
+ val dm = DisplayMetrics()
+ windowManager.defaultDisplay.getRealMetrics(dm)
+ w = dm.widthPixels
+ h = dm.heightPixels
+ dpi = dm.densityDpi
+ }
+
+ val max = max(w,h)
+ val min = min(w,h)
+ if (orientation == ORIENTATION_LANDSCAPE) {
+ w = max
+ h = min
+ } else {
+ w = min
+ h = max
+ }
+ Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
+ var scale = 1
+ if (w != 0 && h != 0) {
+ if (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE) {
+ scale = 2
+ w /= scale
+ h /= scale
+ dpi /= scale
+ }
+ if (SCREEN_INFO.width != w) {
+ SCREEN_INFO.width = w
+ SCREEN_INFO.height = h
+ SCREEN_INFO.scale = scale
+ SCREEN_INFO.dpi = dpi
+ if (isStart) {
+ stopCapture()
+ refreshScreen()
+ startCapture()
+ }
+ }
+
+ }
+ }
+
+ override fun onBind(intent: Intent): IBinder {
+ Log.d(logTag, "service onBind")
+ return binder
+ }
+
+ inner class LocalBinder : Binder() {
+ init {
+ Log.d(logTag, "LocalBinder init")
+ }
+
+ fun getService(): MainService = this@MainService
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d("whichService", "this service:${Thread.currentThread()}")
+ super.onStartCommand(intent, flags, startId)
+ if (intent?.action == INIT_SERVICE) {
+ Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}")
+ createForegroundNotification()
+ val mMediaProjectionManager =
+ getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
+ intent.getParcelableExtra(EXTRA_MP_DATA)?.let {
+ mediaProjection =
+ mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
+ checkMediaPermission()
+ init(this)
+ _isReady = true
+ }
+ }
+ return START_NOT_STICKY // don't use sticky (auto restart),the new service (from auto restart) will lose control
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ updateScreenInfo(newConfig.orientation)
+ }
+
+ @SuppressLint("WrongConstant")
+ private fun createSurface(): Surface? {
+ return if (useVP9) {
+ // TODO
+ null
+ } else {
+ Log.d(logTag, "ImageReader.newInstance:INFO:$SCREEN_INFO")
+ imageReader =
+ ImageReader.newInstance(
+ SCREEN_INFO.width,
+ SCREEN_INFO.height,
+ PixelFormat.RGBA_8888,
+ 4
+ ).apply {
+ setOnImageAvailableListener({ imageReader: ImageReader ->
+ try {
+ imageReader.acquireLatestImage().use { image ->
+ if (image == null) return@setOnImageAvailableListener
+ val planes = image.planes
+ val buffer = planes[0].buffer
+ buffer.rewind()
+ onVideoFrameUpdate(buffer)
+ }
+ } catch (ignored: java.lang.Exception) {
+ }
+ }, serviceHandler)
+ }
+ Log.d(logTag, "ImageReader.setOnImageAvailableListener done")
+ imageReader?.surface
+ }
+ }
+
+ fun startCapture(): Boolean {
+ if (isStart) {
+ return true
+ }
+ if (mediaProjection == null) {
+ Log.w(logTag, "startCapture fail,mediaProjection is null")
+ return false
+ }
+ updateScreenInfo(resources.configuration.orientation)
+ Log.d(logTag, "Start Capture")
+ surface = createSurface()
+
+ if (useVP9) {
+ startVP9VideoRecorder(mediaProjection!!)
+ } else {
+ startRawVideoRecorder(mediaProjection!!)
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ startAudioRecorder()
+ }
+ checkMediaPermission()
+ _isStart = true
+ setFrameRawEnable("video",true)
+ setFrameRawEnable("audio",true)
+ return true
+ }
+
+ fun stopCapture() {
+ Log.d(logTag, "Stop Capture")
+ setFrameRawEnable("video",false)
+ setFrameRawEnable("audio",false)
+ _isStart = false
+ // release video
+ virtualDisplay?.release()
+ surface?.release()
+ imageReader?.close()
+ videoEncoder?.let {
+ it.signalEndOfInputStream()
+ it.stop()
+ it.release()
+ }
+ virtualDisplay = null
+ videoEncoder = null
+
+ // release audio
+ audioRecordStat = false
+ audioRecorder?.release()
+ audioRecorder = null
+ minBufferSize = 0
+ }
+
+ fun destroy() {
+ Log.d(logTag, "destroy service")
+ _isReady = false
+
+ stopCapture()
+ imageReader?.close()
+ imageReader = null
+
+ mediaProjection = null
+ checkMediaPermission()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ InputService.ctx?.disableSelf()
+ }
+ InputService.ctx = null
+ stopForeground(true)
+ stopSelf()
+ }
+
+ fun checkMediaPermission(): Boolean {
+ Handler(Looper.getMainLooper()).post {
+ MainActivity.flutterMethodChannel.invokeMethod(
+ "on_state_changed",
+ mapOf("name" to "media", "value" to isReady.toString())
+ )
+ }
+ Handler(Looper.getMainLooper()).post {
+ MainActivity.flutterMethodChannel.invokeMethod(
+ "on_state_changed",
+ mapOf("name" to "input", "value" to InputService.isOpen.toString())
+ )
+ }
+ return isReady
+ }
+
+ private fun startRawVideoRecorder(mp: MediaProjection) {
+ Log.d(logTag, "startRawVideoRecorder,screen info:$SCREEN_INFO")
+ if (surface == null) {
+ Log.d(logTag, "startRawVideoRecorder failed,surface is null")
+ return
+ }
+ virtualDisplay = mp.createVirtualDisplay(
+ "RustDeskVD",
+ SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
+ surface, null, null
+ )
+ }
+
+ private fun startVP9VideoRecorder(mp: MediaProjection) {
+ createMediaCodec()
+ videoEncoder?.let {
+ surface = it.createInputSurface()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ surface!!.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT)
+ }
+ it.setCallback(cb)
+ it.start()
+ virtualDisplay = mp.createVirtualDisplay(
+ "RustDeskVD",
+ SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
+ surface, null, null
+ )
+ }
+ }
+
+ private val cb: MediaCodec.Callback = object : MediaCodec.Callback() {
+ override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}
+ override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {}
+
+ override fun onOutputBufferAvailable(
+ codec: MediaCodec,
+ index: Int,
+ info: MediaCodec.BufferInfo
+ ) {
+ codec.getOutputBuffer(index)?.let { buf ->
+ sendVP9Thread.execute {
+ val byteArray = ByteArray(buf.limit())
+ buf.get(byteArray)
+ // sendVp9(byteArray)
+ codec.releaseOutputBuffer(index, false)
+ }
+ }
+ }
+
+ override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
+ Log.e(logTag, "MediaCodec.Callback error:$e")
+ }
+ }
+
+
+ private fun createMediaCodec() {
+ Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
+ videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
+ val mFormat =
+ MediaFormat.createVideoFormat(MIME_TYPE, SCREEN_INFO.width, SCREEN_INFO.height)
+ mFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_KEY_BIT_RATE)
+ mFormat.setInteger(MediaFormat.KEY_FRAME_RATE, VIDEO_KEY_FRAME_RATE)
+ mFormat.setInteger(
+ MediaFormat.KEY_COLOR_FORMAT,
+ MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
+ )
+ mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
+ try {
+ videoEncoder!!.configure(mFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
+ } catch (e: Exception) {
+ Log.e(logTag, "mEncoder.configure fail!")
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun startAudioRecorder() {
+ checkAudioRecorder()
+ if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
+ try {
+ audioRecorder!!.startRecording()
+ audioRecordStat = true
+ thread {
+ while (audioRecordStat) {
+ audioReader!!.readSync(audioRecorder!!)?.let {
+ onAudioFrameUpdate(it)
+ }
+ }
+ Log.d(logTag, "Exit audio thread")
+ }
+ } catch (e: Exception) {
+ Log.d(logTag, "startAudioRecorder fail:$e")
+ }
+ } else {
+ Log.d(logTag, "startAudioRecorder fail")
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun checkAudioRecorder() {
+ if (audioRecorder != null && audioRecorder != null && minBufferSize != 0) {
+ return
+ }
+ // read f32 to byte , length * 4
+ minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
+ AUDIO_SAMPLE_RATE,
+ AUDIO_CHANNEL_MASK,
+ AUDIO_ENCODING
+ )
+ if (minBufferSize == 0) {
+ Log.d(logTag, "get min buffer size fail!")
+ return
+ }
+ audioReader = AudioReader(minBufferSize, 4)
+ Log.d(logTag, "init audioData len:$minBufferSize")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ mediaProjection?.let {
+ val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
+ .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
+ .addMatchingUsage(AudioAttributes.USAGE_ALARM)
+ .addMatchingUsage(AudioAttributes.USAGE_GAME)
+ .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build()
+ if (ActivityCompat.checkSelfPermission(
+ this,
+ Manifest.permission.RECORD_AUDIO
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ return
+ }
+ audioRecorder = AudioRecord.Builder()
+ .setAudioFormat(
+ AudioFormat.Builder()
+ .setEncoding(AUDIO_ENCODING)
+ .setSampleRate(AUDIO_SAMPLE_RATE)
+ .setChannelMask(AUDIO_CHANNEL_MASK).build()
+ )
+ .setAudioPlaybackCaptureConfig(apcc)
+ .setBufferSizeInBytes(minBufferSize).build()
+ Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
+ return
+ }
+ }
+ Log.d(logTag, "createAudioRecorder fail")
+ }
+
+ private fun initNotification() {
+ notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channelId = "RustDesk"
+ val channelName = "RustDesk Service"
+ val channel = NotificationChannel(
+ channelId,
+ channelName, NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ description = "RustDesk Service Channel"
+ }
+ channel.lightColor = Color.BLUE
+ channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
+ notificationManager.createNotificationChannel(channel)
+ channelId
+ } else {
+ ""
+ }
+ notificationBuilder = NotificationCompat.Builder(this, notificationChannel)
+ }
+
+ @SuppressLint("UnspecifiedImmutableFlag")
+ private fun createForegroundNotification() {
+ val intent = Intent(this, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ action = Intent.ACTION_MAIN
+ addCategory(Intent.CATEGORY_LAUNCHER)
+ putExtra("type", type)
+ }
+ val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.getActivity(this, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
+ } else {
+ PendingIntent.getActivity(this, 0, intent, FLAG_UPDATE_CURRENT)
+ }
+ val notification = notificationBuilder
+ .setOngoing(true)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setDefaults(Notification.DEFAULT_ALL)
+ .setAutoCancel(true)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setContentTitle(DEFAULT_NOTIFY_TITLE)
+ .setContentText(translate(DEFAULT_NOTIFY_TEXT) + '!')
+ .setOnlyAlertOnce(true)
+ .setContentIntent(pendingIntent)
+ .setColor(ContextCompat.getColor(this, R.color.primary))
+ .setWhen(System.currentTimeMillis())
+ .build()
+ startForeground(DEFAULT_NOTIFY_ID, notification)
+ }
+
+ private fun loginRequestNotification(
+ clientID: Int,
+ type: String,
+ username: String,
+ peerId: String
+ ) {
+ val notification = notificationBuilder
+ .setOngoing(false)
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setContentTitle(translate("Do you accept?"))
+ .setContentText("$type:$username-$peerId")
+ // .setStyle(MediaStyle().setShowActionsInCompactView(0, 1))
+ // .addAction(R.drawable.check_blue, "check", genLoginRequestPendingIntent(true))
+ // .addAction(R.drawable.close_red, "close", genLoginRequestPendingIntent(false))
+ .build()
+ notificationManager.notify(getClientNotifyID(clientID), notification)
+ }
+
+ private fun onClientAuthorizedNotification(
+ clientID: Int,
+ type: String,
+ username: String,
+ peerId: String
+ ) {
+ cancelNotification(clientID)
+ val notification = notificationBuilder
+ .setOngoing(false)
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setContentTitle("$type ${translate("Established")}")
+ .setContentText("$username - $peerId")
+ .build()
+ notificationManager.notify(getClientNotifyID(clientID), notification)
+ }
+
+ private fun getClientNotifyID(clientID: Int): Int {
+ return clientID + NOTIFY_ID_OFFSET
+ }
+
+ fun cancelNotification(clientID: Int) {
+ notificationManager.cancel(getClientNotifyID(clientID))
+ }
+
+ @SuppressLint("UnspecifiedImmutableFlag")
+ private fun genLoginRequestPendingIntent(res: Boolean): PendingIntent {
+ val intent = Intent(this, MainService::class.java).apply {
+ action = ACTION_LOGIN_REQ_NOTIFY
+ putExtra(EXTRA_LOGIN_REQ_NOTIFY, res)
+ }
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.getService(this, 111, intent, FLAG_IMMUTABLE)
+ } else {
+ PendingIntent.getService(this, 111, intent, FLAG_UPDATE_CURRENT)
+ }
+ }
+
+ private fun setTextNotification(_title: String?, _text: String?) {
+ val title = _title ?: DEFAULT_NOTIFY_TITLE
+ val text = _text ?: translate(DEFAULT_NOTIFY_TEXT) + '!'
+ val notification = notificationBuilder
+ .clearActions()
+ .setStyle(null)
+ .setContentTitle(title)
+ .setContentText(text)
+ .build()
+ notificationManager.notify(DEFAULT_NOTIFY_ID, notification)
+ }
+}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt
new file mode 100644
index 000000000..7ce7d3ecc
--- /dev/null
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt
@@ -0,0 +1,116 @@
+package com.carriez.flutter_hbb
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.media.AudioRecord
+import android.media.AudioRecord.READ_BLOCKING
+import android.media.MediaCodecList
+import android.media.MediaFormat
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import androidx.annotation.RequiresApi
+import com.hjq.permissions.Permission
+import com.hjq.permissions.XXPermissions
+import java.nio.ByteBuffer
+import java.util.*
+
+@SuppressLint("ConstantLocale")
+val LOCAL_NAME = Locale.getDefault().toString()
+val SCREEN_INFO = Info(0, 0, 1, 200)
+
+data class Info(
+ var width: Int, var height: Int, var scale: Int, var dpi: Int
+)
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+fun testVP9Support(): Boolean {
+ return true
+ val res = MediaCodecList(MediaCodecList.ALL_CODECS)
+ .findEncoderForFormat(
+ MediaFormat.createVideoFormat(
+ MediaFormat.MIMETYPE_VIDEO_VP9,
+ SCREEN_INFO.width,
+ SCREEN_INFO.width
+ )
+ )
+ return res != null
+}
+
+fun requestPermission(context: Context, type: String) {
+ val permission = when (type) {
+ "audio" -> {
+ Permission.RECORD_AUDIO
+ }
+ "file" -> {
+ Permission.MANAGE_EXTERNAL_STORAGE
+ }
+ else -> {
+ return
+ }
+ }
+ XXPermissions.with(context)
+ .permission(permission)
+ .request { permissions, all ->
+ if (all) {
+ Handler(Looper.getMainLooper()).post {
+ MainActivity.flutterMethodChannel.invokeMethod(
+ "on_android_permission_result",
+ mapOf("type" to type, "result" to all)
+ )
+ }
+ }
+ }
+}
+
+fun checkPermission(context: Context, type: String): Boolean {
+ val permission = when (type) {
+ "audio" -> {
+ Permission.RECORD_AUDIO
+ }
+ "file" -> {
+ Permission.MANAGE_EXTERNAL_STORAGE
+ }
+ else -> {
+ return false
+ }
+ }
+ return XXPermissions.isGranted(context, permission)
+}
+
+class AudioReader(val bufSize: Int, private val maxFrames: Int) {
+ private var currentPos = 0
+ private val bufferPool: Array
+
+ init {
+ if (maxFrames < 0 || maxFrames > 32) {
+ throw Exception("Out of bounds")
+ }
+ if (bufSize <= 0) {
+ throw Exception("Wrong bufSize")
+ }
+ bufferPool = Array(maxFrames) {
+ ByteBuffer.allocateDirect(bufSize)
+ }
+ }
+
+ private fun next() {
+ currentPos++
+ if (currentPos >= maxFrames) {
+ currentPos = 0
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ fun readSync(audioRecord: AudioRecord): ByteBuffer? {
+ val buffer = bufferPool[currentPos]
+ val res = audioRecord.read(buffer, bufSize, READ_BLOCKING)
+ return if (res > 0) {
+ next()
+ buffer
+ } else {
+ null
+ }
+ }
+}
diff --git a/flutter/android/app/src/main/res/drawable-v21/launch_background.xml b/flutter/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 000000000..f74085f3f
--- /dev/null
+++ b/flutter/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/flutter/android/app/src/main/res/drawable/check_blue.xml b/flutter/android/app/src/main/res/drawable/check_blue.xml
new file mode 100644
index 000000000..b06974b36
--- /dev/null
+++ b/flutter/android/app/src/main/res/drawable/check_blue.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/flutter/android/app/src/main/res/drawable/close_red.xml b/flutter/android/app/src/main/res/drawable/close_red.xml
new file mode 100644
index 000000000..02ff2c8b6
--- /dev/null
+++ b/flutter/android/app/src/main/res/drawable/close_red.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/flutter/android/app/src/main/res/drawable/launch_background.xml b/flutter/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 000000000..304732f88
--- /dev/null
+++ b/flutter/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..9a6ce011d
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..20cb4f904
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..b77c65d15
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..638a672f9
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..de6dc58b4
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/values-night/styles.xml b/flutter/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 000000000..449a9f930
--- /dev/null
+++ b/flutter/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/flutter/android/app/src/main/res/values/colors.xml b/flutter/android/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..273468987
--- /dev/null
+++ b/flutter/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FF0071FF
+
\ No newline at end of file
diff --git a/flutter/android/app/src/main/res/values/strings.xml b/flutter/android/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..3e058a81b
--- /dev/null
+++ b/flutter/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ RustDesk
+ Allow other devices to control your phone using virtual touch, when RustDesk screen sharing is established
+
diff --git a/flutter/android/app/src/main/res/values/styles.xml b/flutter/android/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..d74aa35c2
--- /dev/null
+++ b/flutter/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/flutter/android/app/src/main/res/xml/accessibility_service_config.xml b/flutter/android/app/src/main/res/xml/accessibility_service_config.xml
new file mode 100644
index 000000000..fa9407128
--- /dev/null
+++ b/flutter/android/app/src/main/res/xml/accessibility_service_config.xml
@@ -0,0 +1,6 @@
+
diff --git a/flutter/android/app/src/profile/AndroidManifest.xml b/flutter/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 000000000..64d68a588
--- /dev/null
+++ b/flutter/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle
new file mode 100644
index 000000000..e34fecc69
--- /dev/null
+++ b/flutter/android/build.gradle
@@ -0,0 +1,32 @@
+buildscript {
+ ext.kotlin_version = '1.6.10'
+ repositories {
+ google()
+ jcenter()
+ maven { url 'https://jitpack.io' }
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.0.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'com.google.gms:google-services:4.3.3'
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/flutter/android/flutter_hbb_android.iml b/flutter/android/flutter_hbb_android.iml
new file mode 100644
index 000000000..18999696a
--- /dev/null
+++ b/flutter/android/flutter_hbb_android.iml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flutter/android/gradle.properties b/flutter/android/gradle.properties
new file mode 100644
index 000000000..94adc3a3f
--- /dev/null
+++ b/flutter/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/flutter/android/gradle/wrapper/gradle-wrapper.properties b/flutter/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..b8793d3c0
--- /dev/null
+++ b/flutter/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
diff --git a/flutter/android/key.jks b/flutter/android/key.jks
new file mode 100644
index 000000000..8e1763f46
Binary files /dev/null and b/flutter/android/key.jks differ
diff --git a/flutter/android/settings.gradle b/flutter/android/settings.gradle
new file mode 100644
index 000000000..44e62bcf0
--- /dev/null
+++ b/flutter/android/settings.gradle
@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/flutter/assets/android.png b/flutter/assets/android.png
new file mode 100644
index 000000000..323100330
Binary files /dev/null and b/flutter/assets/android.png differ
diff --git a/flutter/assets/gestures.ttf b/flutter/assets/gestures.ttf
new file mode 100644
index 000000000..b6cebbf43
Binary files /dev/null and b/flutter/assets/gestures.ttf differ
diff --git a/flutter/assets/insecure.png b/flutter/assets/insecure.png
new file mode 100644
index 000000000..0c954468d
Binary files /dev/null and b/flutter/assets/insecure.png differ
diff --git a/flutter/assets/insecure_relay.png b/flutter/assets/insecure_relay.png
new file mode 100644
index 000000000..878d57467
Binary files /dev/null and b/flutter/assets/insecure_relay.png differ
diff --git a/flutter/assets/linux.png b/flutter/assets/linux.png
new file mode 100644
index 000000000..456e58675
Binary files /dev/null and b/flutter/assets/linux.png differ
diff --git a/flutter/assets/mac.png b/flutter/assets/mac.png
new file mode 100644
index 000000000..4be16f369
Binary files /dev/null and b/flutter/assets/mac.png differ
diff --git a/flutter/assets/secure.png b/flutter/assets/secure.png
new file mode 100644
index 000000000..01dcb2a8a
Binary files /dev/null and b/flutter/assets/secure.png differ
diff --git a/flutter/assets/secure_relay.png b/flutter/assets/secure_relay.png
new file mode 100644
index 000000000..4119f05ba
Binary files /dev/null and b/flutter/assets/secure_relay.png differ
diff --git a/flutter/assets/win.png b/flutter/assets/win.png
new file mode 100644
index 000000000..5ce86a257
Binary files /dev/null and b/flutter/assets/win.png differ
diff --git a/flutter/build_android.sh b/flutter/build_android.sh
new file mode 100755
index 000000000..4f4038593
--- /dev/null
+++ b/flutter/build_android.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+$ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/*
+flutter build apk --target-platform android-arm64 --release --obfuscate --split-debug-info ./split-debug-info
+flutter build appbundle --target-platform android-arm64 --release --obfuscate --split-debug-info ./split-debug-info
+
+# build in linux
+# $ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/*
diff --git a/flutter/build_ios.sh b/flutter/build_ios.sh
new file mode 100755
index 000000000..6d0d627ac
--- /dev/null
+++ b/flutter/build_ios.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
diff --git a/flutter/deploy.sh b/flutter/deploy.sh
new file mode 100755
index 000000000..c18803419
--- /dev/null
+++ b/flutter/deploy.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+cd build/web/
+python3 -c 'x=open("./main.dart.js", "rt").read();import re;y=re.search("https://.*canvaskit-wasm@([\d\.]+)/bin/",x);dirname="canvaskit@"+y.groups()[0];z=x.replace(y.group(),"/"+dirname+"/");f=open("./main.dart.js", "wt");f.write(z);import os;os.system("ln -s canvaskit " + dirname);'
+python3 -c 'import hashlib;x=hashlib.sha1(open("./main.dart.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("main.dart.js", "main.dart.js?v="+x);open("index.html","wt").write(y)'
+tar czf x *
+scp x sg:/tmp/
+ssh sg "sudo tar xzf /tmp/x -C /var/www/html/web.rustdesk.com/ && /bin/rm /tmp/x && sudo chown www-data:www-data /var/www/html/web.rustdesk.com/ -R"
+/bin/rm x
+cd -
diff --git a/flutter/ios/.gitignore b/flutter/ios/.gitignore
new file mode 100644
index 000000000..151026b91
--- /dev/null
+++ b/flutter/ios/.gitignore
@@ -0,0 +1,33 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/flutter/ios/Flutter/AppFrameworkInfo.plist b/flutter/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 000000000..8d4492f97
--- /dev/null
+++ b/flutter/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 9.0
+
+
diff --git a/flutter/ios/Flutter/Debug.xcconfig b/flutter/ios/Flutter/Debug.xcconfig
new file mode 100644
index 000000000..ec97fc6f3
--- /dev/null
+++ b/flutter/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/flutter/ios/Flutter/Release.xcconfig b/flutter/ios/Flutter/Release.xcconfig
new file mode 100644
index 000000000..c4855bfe2
--- /dev/null
+++ b/flutter/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/flutter/ios/Podfile b/flutter/ios/Podfile
new file mode 100644
index 000000000..0df0a09f8
--- /dev/null
+++ b/flutter/ios/Podfile
@@ -0,0 +1,45 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+platform :ios, '11.0'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
+
+
diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock
new file mode 100644
index 000000000..947fd0d36
--- /dev/null
+++ b/flutter/ios/Podfile.lock
@@ -0,0 +1,194 @@
+PODS:
+ - device_info (0.0.1):
+ - Flutter
+ - Firebase/Analytics (8.14.0):
+ - Firebase/Core
+ - Firebase/Core (8.14.0):
+ - Firebase/CoreOnly
+ - FirebaseAnalytics (~> 8.14.0)
+ - Firebase/CoreOnly (8.14.0):
+ - FirebaseCore (= 8.14.0)
+ - firebase_analytics (9.1.5):
+ - Firebase/Analytics (= 8.14.0)
+ - firebase_core
+ - Flutter
+ - firebase_core (1.14.1):
+ - Firebase/CoreOnly (= 8.14.0)
+ - Flutter
+ - FirebaseAnalytics (8.14.0):
+ - FirebaseAnalytics/AdIdSupport (= 8.14.0)
+ - FirebaseCore (~> 8.0)
+ - FirebaseInstallations (~> 8.0)
+ - GoogleUtilities/AppDelegateSwizzler (~> 7.7)
+ - GoogleUtilities/MethodSwizzler (~> 7.7)
+ - GoogleUtilities/Network (~> 7.7)
+ - "GoogleUtilities/NSData+zlib (~> 7.7)"
+ - nanopb (~> 2.30908.0)
+ - FirebaseAnalytics/AdIdSupport (8.14.0):
+ - FirebaseCore (~> 8.0)
+ - FirebaseInstallations (~> 8.0)
+ - GoogleAppMeasurement (= 8.14.0)
+ - GoogleUtilities/AppDelegateSwizzler (~> 7.7)
+ - GoogleUtilities/MethodSwizzler (~> 7.7)
+ - GoogleUtilities/Network (~> 7.7)
+ - "GoogleUtilities/NSData+zlib (~> 7.7)"
+ - nanopb (~> 2.30908.0)
+ - FirebaseCore (8.14.0):
+ - FirebaseCoreDiagnostics (~> 8.0)
+ - GoogleUtilities/Environment (~> 7.7)
+ - GoogleUtilities/Logger (~> 7.7)
+ - FirebaseCoreDiagnostics (8.15.0):
+ - GoogleDataTransport (~> 9.1)
+ - GoogleUtilities/Environment (~> 7.7)
+ - GoogleUtilities/Logger (~> 7.7)
+ - nanopb (~> 2.30908.0)
+ - FirebaseInstallations (8.15.0):
+ - FirebaseCore (~> 8.0)
+ - GoogleUtilities/Environment (~> 7.7)
+ - GoogleUtilities/UserDefaults (~> 7.7)
+ - PromisesObjC (< 3.0, >= 1.2)
+ - Flutter (1.0.0)
+ - GoogleAppMeasurement (8.14.0):
+ - GoogleAppMeasurement/AdIdSupport (= 8.14.0)
+ - GoogleUtilities/AppDelegateSwizzler (~> 7.7)
+ - GoogleUtilities/MethodSwizzler (~> 7.7)
+ - GoogleUtilities/Network (~> 7.7)
+ - "GoogleUtilities/NSData+zlib (~> 7.7)"
+ - nanopb (~> 2.30908.0)
+ - GoogleAppMeasurement/AdIdSupport (8.14.0):
+ - GoogleAppMeasurement/WithoutAdIdSupport (= 8.14.0)
+ - GoogleUtilities/AppDelegateSwizzler (~> 7.7)
+ - GoogleUtilities/MethodSwizzler (~> 7.7)
+ - GoogleUtilities/Network (~> 7.7)
+ - "GoogleUtilities/NSData+zlib (~> 7.7)"
+ - nanopb (~> 2.30908.0)
+ - GoogleAppMeasurement/WithoutAdIdSupport (8.14.0):
+ - GoogleUtilities/AppDelegateSwizzler (~> 7.7)
+ - GoogleUtilities/MethodSwizzler (~> 7.7)
+ - GoogleUtilities/Network (~> 7.7)
+ - "GoogleUtilities/NSData+zlib (~> 7.7)"
+ - nanopb (~> 2.30908.0)
+ - GoogleDataTransport (9.1.2):
+ - GoogleUtilities/Environment (~> 7.2)
+ - nanopb (~> 2.30908.0)
+ - PromisesObjC (< 3.0, >= 1.2)
+ - GoogleUtilities/AppDelegateSwizzler (7.7.0):
+ - GoogleUtilities/Environment
+ - GoogleUtilities/Logger
+ - GoogleUtilities/Network
+ - GoogleUtilities/Environment (7.7.0):
+ - PromisesObjC (< 3.0, >= 1.2)
+ - GoogleUtilities/Logger (7.7.0):
+ - GoogleUtilities/Environment
+ - GoogleUtilities/MethodSwizzler (7.7.0):
+ - GoogleUtilities/Logger
+ - GoogleUtilities/Network (7.7.0):
+ - GoogleUtilities/Logger
+ - "GoogleUtilities/NSData+zlib"
+ - GoogleUtilities/Reachability
+ - "GoogleUtilities/NSData+zlib (7.7.0)"
+ - GoogleUtilities/Reachability (7.7.0):
+ - GoogleUtilities/Logger
+ - GoogleUtilities/UserDefaults (7.7.0):
+ - GoogleUtilities/Logger
+ - image_picker_ios (0.0.1):
+ - Flutter
+ - MTBBarcodeScanner (5.0.11)
+ - nanopb (2.30908.0):
+ - nanopb/decode (= 2.30908.0)
+ - nanopb/encode (= 2.30908.0)
+ - nanopb/decode (2.30908.0)
+ - nanopb/encode (2.30908.0)
+ - package_info (0.0.1):
+ - Flutter
+ - path_provider_ios (0.0.1):
+ - Flutter
+ - PromisesObjC (2.1.0)
+ - qr_code_scanner (0.2.0):
+ - Flutter
+ - MTBBarcodeScanner
+ - shared_preferences_ios (0.0.1):
+ - Flutter
+ - url_launcher_ios (0.0.1):
+ - Flutter
+ - wakelock (0.0.1):
+ - Flutter
+
+DEPENDENCIES:
+ - device_info (from `.symlinks/plugins/device_info/ios`)
+ - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
+ - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
+ - Flutter (from `Flutter`)
+ - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
+ - package_info (from `.symlinks/plugins/package_info/ios`)
+ - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
+ - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
+ - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
+ - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+ - wakelock (from `.symlinks/plugins/wakelock/ios`)
+
+SPEC REPOS:
+ trunk:
+ - Firebase
+ - FirebaseAnalytics
+ - FirebaseCore
+ - FirebaseCoreDiagnostics
+ - FirebaseInstallations
+ - GoogleAppMeasurement
+ - GoogleDataTransport
+ - GoogleUtilities
+ - MTBBarcodeScanner
+ - nanopb
+ - PromisesObjC
+
+EXTERNAL SOURCES:
+ device_info:
+ :path: ".symlinks/plugins/device_info/ios"
+ firebase_analytics:
+ :path: ".symlinks/plugins/firebase_analytics/ios"
+ firebase_core:
+ :path: ".symlinks/plugins/firebase_core/ios"
+ Flutter:
+ :path: Flutter
+ image_picker_ios:
+ :path: ".symlinks/plugins/image_picker_ios/ios"
+ package_info:
+ :path: ".symlinks/plugins/package_info/ios"
+ path_provider_ios:
+ :path: ".symlinks/plugins/path_provider_ios/ios"
+ qr_code_scanner:
+ :path: ".symlinks/plugins/qr_code_scanner/ios"
+ shared_preferences_ios:
+ :path: ".symlinks/plugins/shared_preferences_ios/ios"
+ url_launcher_ios:
+ :path: ".symlinks/plugins/url_launcher_ios/ios"
+ wakelock:
+ :path: ".symlinks/plugins/wakelock/ios"
+
+SPEC CHECKSUMS:
+ device_info: d7d233b645a32c40dfdc212de5cf646ca482f175
+ Firebase: 7e8fe528c161b9271d365217a74c16aaf834578e
+ firebase_analytics: 8a36c76380be1fca3bab69534cf911082e0d7ab8
+ firebase_core: cdef02fcf55872191eb0568d4c31a7a700e38582
+ FirebaseAnalytics: 2fc3876e2eb347673ad2f35e249ae7b15d6c88f5
+ FirebaseCore: b84a44ee7ba999e0f9f76d198a9c7f60a797b848
+ FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb
+ FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd
+ Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
+ GoogleAppMeasurement: 71156240babd3cc6ced03e0d54816f01a880c730
+ GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940
+ GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1
+ image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
+ MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
+ nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
+ package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
+ path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
+ PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72
+ qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
+ shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
+ url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
+ wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
+
+PODFILE CHECKSUM: a00077baecbb97321490c14848fceed3893ca92a
+
+COCOAPODS: 1.11.3
diff --git a/flutter/ios/Runner.xcodeproj/project.pbxproj b/flutter/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..953dad47b
--- /dev/null
+++ b/flutter/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,575 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 51;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 7E078EE926BAB4720036E738 /* liblibrustdesk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E078EE826BAB4710036E738 /* liblibrustdesk.a */; };
+ 7E078EEC26BADB3D0036E738 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7E078EEB26BADB3D0036E738 /* GoogleService-Info.plist */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ C362FA62593C0A953D788A2B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D81DD31BD179189F3EA0124A /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 7B0BE50AED65C219B81E506C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 7E078EE826BAB4710036E738 /* liblibrustdesk.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = liblibrustdesk.a; path = "../../target/aarch64-apple-ios/release/liblibrustdesk.a"; sourceTree = ""; };
+ 7E078EEA26BABB100036E738 /* ffi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ffi.h; sourceTree = ""; };
+ 7E078EEB26BADB3D0036E738 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
+ 7E0A73A826CAB3C100FF94B3 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
+ 94AF76B3E95A41AD3421FB7B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ C9E47BCA6B42B34FF7A143DA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ D81DD31BD179189F3EA0124A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 7E078EE926BAB4720036E738 /* liblibrustdesk.a in Frameworks */,
+ C362FA62593C0A953D788A2B /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 4064E93F6C682BED4ADBDEEA /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 94AF76B3E95A41AD3421FB7B /* Pods-Runner.debug.xcconfig */,
+ 7B0BE50AED65C219B81E506C /* Pods-Runner.release.xcconfig */,
+ C9E47BCA6B42B34FF7A143DA /* Pods-Runner.profile.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 4064E93F6C682BED4ADBDEEA /* Pods */,
+ 9F474765C702311B2610E104 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 7E0A73A826CAB3C100FF94B3 /* Runner.entitlements */,
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 7E078EEB26BADB3D0036E738 /* GoogleService-Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 7E078EEA26BABB100036E738 /* ffi.h */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ 9F474765C702311B2610E104 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 7E078EE826BAB4710036E738 /* liblibrustdesk.a */,
+ D81DD31BD179189F3EA0124A /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 7E81731B2965F8CA22E67C02 /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ 3974543C243EF943511C7BDD /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1300;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 7E078EEC26BADB3D0036E738 /* GoogleService-Info.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3974543C243EF943511C7BDD /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 7E81731B2965F8CA22E67C02 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = HZF9JMC8YN;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/";
+ PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ STRIP_STYLE = "non-global";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = HZF9JMC8YN;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/";
+ PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ STRIP_STYLE = "non-global";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = HZF9JMC8YN;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/";
+ PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ STRIP_STYLE = "non-global";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..919434a62
--- /dev/null
+++ b/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 000000000..f9b0d7c5e
--- /dev/null
+++ b/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 000000000..3db53b6e1
--- /dev/null
+++ b/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata b/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..21a3cc14c
--- /dev/null
+++ b/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 000000000..f9b0d7c5e
--- /dev/null
+++ b/flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/flutter/ios/Runner/AppDelegate.swift b/flutter/ios/Runner/AppDelegate.swift
new file mode 100644
index 000000000..203cfff11
--- /dev/null
+++ b/flutter/ios/Runner/AppDelegate.swift
@@ -0,0 +1,21 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ dummyMethodToEnforceBundling();
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ public func dummyMethodToEnforceBundling() {
+ get_rgba();
+ free_rgba(nil);
+ get_by_name("", "");
+ set_by_name("", "");
+ }
+}
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..d36b1fab2
--- /dev/null
+++ b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 000000000..eb0bf04fd
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 000000000..b17866fe4
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 000000000..70164e1a4
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 000000000..fa5fb621a
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 000000000..8999d5ed9
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 000000000..047d553ce
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 000000000..295741a1b
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 000000000..70164e1a4
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 000000000..6207bba1b
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 000000000..df9cb7d0a
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 000000000..df9cb7d0a
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 000000000..12ffdab24
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 000000000..563187384
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 000000000..834c4d8e8
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 000000000..4a3ffa808
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 000000000..0bedcf2fd
--- /dev/null
+++ b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 000000000..9da19eaca
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 000000000..9da19eaca
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 000000000..9da19eaca
Binary files /dev/null and b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 000000000..89c2725b7
--- /dev/null
+++ b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard b/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 000000000..f2e259c7c
--- /dev/null
+++ b/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flutter/ios/Runner/Base.lproj/Main.storyboard b/flutter/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 000000000..f3c28516f
--- /dev/null
+++ b/flutter/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flutter/ios/Runner/GoogleService-Info.plist b/flutter/ios/Runner/GoogleService-Info.plist
new file mode 100644
index 000000000..f39288230
--- /dev/null
+++ b/flutter/ios/Runner/GoogleService-Info.plist
@@ -0,0 +1,36 @@
+
+
+
+
+ CLIENT_ID
+ 768133699366-k1rn3ls1u2n3nklmgd9t4cmpdob0c8bn.apps.googleusercontent.com
+ REVERSED_CLIENT_ID
+ com.googleusercontent.apps.768133699366-k1rn3ls1u2n3nklmgd9t4cmpdob0c8bn
+ API_KEY
+ AIzaSyCf57HjCwSokt91CqFI0Mwf8D--ek0jvfc
+ GCM_SENDER_ID
+ 768133699366
+ PLIST_VERSION
+ 1
+ BUNDLE_ID
+ com.carriez.flutterHbb
+ PROJECT_ID
+ rustdesk
+ STORAGE_BUCKET
+ rustdesk.appspot.com
+ IS_ADS_ENABLED
+
+ IS_ANALYTICS_ENABLED
+
+ IS_APPINVITE_ENABLED
+
+ IS_GCM_ENABLED
+
+ IS_SIGNIN_ENABLED
+
+ GOOGLE_APP_ID
+ 1:768133699366:ios:c33078a6181b9d507993e7
+ DATABASE_URL
+ https://rustdesk.firebaseio.com
+
+
\ No newline at end of file
diff --git a/flutter/ios/Runner/Info.plist b/flutter/ios/Runner/Info.plist
new file mode 100644
index 000000000..3886d2456
--- /dev/null
+++ b/flutter/ios/Runner/Info.plist
@@ -0,0 +1,53 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ RustDesk
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+ ITSAppUsesNonExemptEncryption
+
+ io.flutter.embedded_views_preview
+
+ NSCameraUsageDescription
+ This app needs camera access to scan QR codes
+ NSPhotoLibraryUsageDescription
+ This app needs photo library access to get QR codes from image
+
+
diff --git a/flutter/ios/Runner/Runner-Bridging-Header.h b/flutter/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 000000000..a8c447418
--- /dev/null
+++ b/flutter/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1,3 @@
+#import "GeneratedPluginRegistrant.h"
+
+#import "ffi.h"
diff --git a/flutter/ios/Runner/Runner.entitlements b/flutter/ios/Runner/Runner.entitlements
new file mode 100644
index 000000000..ba21fbdaf
--- /dev/null
+++ b/flutter/ios/Runner/Runner.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.developer.networking.wifi-info
+
+
+
diff --git a/flutter/ios/Runner/ffi.h b/flutter/ios/Runner/ffi.h
new file mode 100644
index 000000000..701ec4b09
--- /dev/null
+++ b/flutter/ios/Runner/ffi.h
@@ -0,0 +1,4 @@
+void* get_rgba();
+void free_rgba(void*);
+void set_by_name(const char*, const char*);
+const char* get_by_name(const char*, const char*);
diff --git a/flutter/ios_arm64.sh b/flutter/ios_arm64.sh
new file mode 100755
index 000000000..01a09a4b4
--- /dev/null
+++ b/flutter/ios_arm64.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+cargo build --release --target aarch64-apple-ios
diff --git a/flutter/ios_x64.sh b/flutter/ios_x64.sh
new file mode 100755
index 000000000..455ef4199
--- /dev/null
+++ b/flutter/ios_x64.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+cargo build --release --target x86_64-apple-ios
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
new file mode 100644
index 000000000..42487379c
--- /dev/null
+++ b/flutter/lib/common.dart
@@ -0,0 +1,307 @@
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'dart:async';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+
+import 'models/model.dart';
+
+final globalKey = GlobalKey();
+final navigationBarKey = GlobalKey();
+
+var isAndroid = false;
+var isIOS = false;
+var isWeb = false;
+var isDesktop = false;
+var version = "";
+int androidVersion = 0;
+
+typedef F = String Function(String);
+typedef FMethod = String Function(String, dynamic);
+
+class Translator {
+ static late F call;
+}
+
+class MyTheme {
+ MyTheme._();
+
+ static const Color grayBg = Color(0xFFEEEEEE);
+ static const Color white = Color(0xFFFFFFFF);
+ static const Color accent = Color(0xFF0071FF);
+ static const Color accent50 = Color(0x770071FF);
+ static const Color accent80 = Color(0xAA0071FF);
+ static const Color canvasColor = Color(0xFF212121);
+ static const Color border = Color(0xFFCCCCCC);
+ static const Color idColor = Color(0xFF00B6F0);
+ static const Color darkGray = Color(0xFFB9BABC);
+}
+
+final ButtonStyle flatButtonStyle = TextButton.styleFrom(
+ minimumSize: Size(88, 36),
+ padding: EdgeInsets.symmetric(horizontal: 16.0),
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(2.0)),
+ ),
+);
+
+void showToast(String text, {Duration? duration}) {
+ SmartDialog.showToast(text, displayTime: duration);
+}
+
+void showLoading(String text, {bool clickMaskDismiss = false}) {
+ SmartDialog.dismiss();
+ SmartDialog.showLoading(
+ clickMaskDismiss: false,
+ builder: (context) {
+ return Container(
+ color: MyTheme.white,
+ constraints: BoxConstraints(maxWidth: 240),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(height: 30),
+ Center(child: CircularProgressIndicator()),
+ SizedBox(height: 20),
+ Center(
+ child: Text(Translator.call(text),
+ style: TextStyle(fontSize: 15))),
+ SizedBox(height: 20),
+ Center(
+ child: TextButton(
+ style: flatButtonStyle,
+ onPressed: () {
+ SmartDialog.dismiss();
+ backToHome();
+ },
+ child: Text(Translator.call('Cancel'),
+ style: TextStyle(color: MyTheme.accent))))
+ ]));
+ });
+}
+
+backToHome() {
+ Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
+}
+
+typedef DialogBuilder = CustomAlertDialog Function(
+ StateSetter setState, void Function([dynamic]) close);
+
+class DialogManager {
+ static int _tag = 0;
+
+ static dismissByTag(String tag, [result]) {
+ SmartDialog.dismiss(tag: tag, result: result);
+ }
+
+ static Future show(DialogBuilder builder,
+ {bool clickMaskDismiss = false,
+ bool backDismiss = false,
+ String? tag,
+ bool useAnimation = true}) async {
+ final t;
+ if (tag != null) {
+ t = tag;
+ } else {
+ _tag += 1;
+ t = _tag.toString();
+ }
+ SmartDialog.dismiss(status: SmartStatus.allToast);
+ SmartDialog.dismiss(status: SmartStatus.loading);
+ final close = ([res]) {
+ SmartDialog.dismiss(tag: t, result: res);
+ };
+ final res = await SmartDialog.show(
+ tag: t,
+ clickMaskDismiss: clickMaskDismiss,
+ backDismiss: backDismiss,
+ useAnimation: useAnimation,
+ builder: (_) => StatefulBuilder(
+ builder: (_, setState) => builder(setState, close)));
+ return res;
+ }
+}
+
+class CustomAlertDialog extends StatelessWidget {
+ CustomAlertDialog(
+ {required this.title,
+ required this.content,
+ required this.actions,
+ this.contentPadding});
+
+ final Widget title;
+ final Widget content;
+ final List actions;
+ final double? contentPadding;
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ scrollable: true,
+ title: title,
+ contentPadding:
+ EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10),
+ content: content,
+ actions: actions,
+ );
+ }
+}
+
+void msgBox(String type, String title, String text, {bool? hasCancel}) {
+ var wrap = (String text, void Function() onPressed) => ButtonTheme(
+ padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ //limits the touch area to the button area
+ minWidth: 0,
+ //wraps child's width
+ height: 0,
+ child: TextButton(
+ style: flatButtonStyle,
+ onPressed: onPressed,
+ child: Text(Translator.call(text),
+ style: TextStyle(color: MyTheme.accent))));
+
+ SmartDialog.dismiss();
+ final buttons = [
+ wrap(Translator.call('OK'), () {
+ SmartDialog.dismiss();
+ backToHome();
+ })
+ ];
+ if (hasCancel == null) {
+ hasCancel = type != 'error';
+ }
+ if (hasCancel) {
+ buttons.insert(
+ 0,
+ wrap(Translator.call('Cancel'), () {
+ SmartDialog.dismiss();
+ }));
+ }
+ DialogManager.show((setState, close) => CustomAlertDialog(
+ title: Text(translate(title), style: TextStyle(fontSize: 21)),
+ content: Text(Translator.call(text), style: TextStyle(fontSize: 15)),
+ actions: buttons));
+}
+
+Color str2color(String str, [alpha = 0xFF]) {
+ var hash = 160 << 16 + 114 << 8 + 91;
+ for (var i = 0; i < str.length; i += 1) {
+ hash = str.codeUnitAt(i) + ((hash << 5) - hash);
+ }
+ hash = hash % 16777216;
+ return Color((hash & 0xFF7FFF) | (alpha << 24));
+}
+
+const K = 1024;
+const M = K * K;
+const G = M * K;
+
+String readableFileSize(double size) {
+ if (size < K) {
+ return size.toString() + " B";
+ } else if (size < M) {
+ return (size / K).toStringAsFixed(2) + " KB";
+ } else if (size < G) {
+ return (size / M).toStringAsFixed(2) + " MB";
+ } else {
+ return (size / G).toStringAsFixed(2) + " GB";
+ }
+}
+
+/// Flutter can't not catch PointerMoveEvent when size is 1
+/// This will happen in Android AccessibilityService Input
+/// android can't init dispatching size yet ,see: https://stackoverflow.com/questions/59960451/android-accessibility-dispatchgesture-is-it-possible-to-specify-pressure-for-a
+/// use this temporary solution until flutter or android fixes the bug
+class AccessibilityListener extends StatelessWidget {
+ final Widget? child;
+ static final offset = 100;
+
+ AccessibilityListener({this.child});
+
+ @override
+ Widget build(BuildContext context) {
+ return Listener(
+ onPointerDown: (evt) {
+ if (evt.size == 1 && GestureBinding.instance != null) {
+ GestureBinding.instance!.handlePointerEvent(PointerAddedEvent(
+ pointer: evt.pointer + offset, position: evt.position));
+ GestureBinding.instance!.handlePointerEvent(PointerDownEvent(
+ pointer: evt.pointer + offset,
+ size: 0.1,
+ position: evt.position));
+ }
+ },
+ onPointerUp: (evt) {
+ if (evt.size == 1 && GestureBinding.instance != null) {
+ GestureBinding.instance!.handlePointerEvent(PointerUpEvent(
+ pointer: evt.pointer + offset,
+ size: 0.1,
+ position: evt.position));
+ GestureBinding.instance!.handlePointerEvent(PointerRemovedEvent(
+ pointer: evt.pointer + offset, position: evt.position));
+ }
+ },
+ onPointerMove: (evt) {
+ if (evt.size == 1 && GestureBinding.instance != null) {
+ GestureBinding.instance!.handlePointerEvent(PointerMoveEvent(
+ pointer: evt.pointer + offset,
+ size: 0.1,
+ delta: evt.delta,
+ position: evt.position));
+ }
+ },
+ child: child);
+ }
+}
+
+class PermissionManager {
+ static Completer? _completer;
+ static Timer? _timer;
+ static var _current = "";
+
+ static final permissions = ["audio", "file"];
+
+ static bool isWaitingFile() {
+ if (_completer != null) {
+ return !_completer!.isCompleted && _current == "file";
+ }
+ return false;
+ }
+
+ static Future check(String type) {
+ if (!permissions.contains(type))
+ return Future.error("Wrong permission!$type");
+ return FFI.invokeMethod("check_permission", type);
+ }
+
+ static Future request(String type) {
+ if (!permissions.contains(type))
+ return Future.error("Wrong permission!$type");
+
+ _current = type;
+ _completer = Completer();
+ FFI.invokeMethod("request_permission", type);
+
+ // timeout
+ _timer?.cancel();
+ _timer = Timer(Duration(seconds: 60), () {
+ if (_completer == null) return;
+ if (!_completer!.isCompleted) {
+ _completer!.complete(false);
+ }
+ _completer = null;
+ _current = "";
+ });
+ return _completer!.future;
+ }
+
+ static complete(String type, bool res) {
+ if (type != _current) {
+ res = false;
+ }
+ _timer?.cancel();
+ _completer?.complete(res);
+ _current = "";
+ }
+}
diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart
new file mode 100644
index 000000000..a81a047b4
--- /dev/null
+++ b/flutter/lib/main.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:provider/provider.dart';
+import 'package:firebase_analytics/firebase_analytics.dart';
+import 'package:firebase_core/firebase_core.dart';
+import 'common.dart';
+import 'models/model.dart';
+import 'pages/home_page.dart';
+import 'pages/server_page.dart';
+import 'pages/settings_page.dart';
+
+Future main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ var a = FFI.ffiModel.init();
+ var b = Firebase.initializeApp();
+ await a;
+ await b;
+ refreshCurrentUser();
+ toAndroidChannelInit();
+ runApp(App());
+}
+
+class App extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ final analytics = FirebaseAnalytics.instance;
+ return MultiProvider(
+ providers: [
+ ChangeNotifierProvider.value(value: FFI.ffiModel),
+ ChangeNotifierProvider.value(value: FFI.imageModel),
+ ChangeNotifierProvider.value(value: FFI.cursorModel),
+ ChangeNotifierProvider.value(value: FFI.canvasModel),
+ ],
+ child: MaterialApp(
+ navigatorKey: globalKey,
+ debugShowCheckedModeBanner: false,
+ title: 'RustDesk',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ visualDensity: VisualDensity.adaptivePlatformDensity,
+ ),
+ home: !isAndroid ? WebHomePage() : HomePage(),
+ navigatorObservers: [
+ FirebaseAnalyticsObserver(analytics: analytics),
+ FlutterSmartDialog.observer
+ ],
+ builder: FlutterSmartDialog.init(
+ builder: isAndroid
+ ? (_, child) => AccessibilityListener(
+ child: child,
+ )
+ : null)),
+ );
+ }
+}
diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart
new file mode 100644
index 000000000..8ee93ae28
--- /dev/null
+++ b/flutter/lib/models/chat_model.dart
@@ -0,0 +1,103 @@
+import 'dart:convert';
+
+import 'package:dash_chat/dash_chat.dart';
+import 'package:flutter/material.dart';
+
+import '../widgets/overlay.dart';
+import 'model.dart';
+
+class ChatModel with ChangeNotifier {
+ static final clientModeID = -1;
+
+ final Map> _messages = Map()..[clientModeID] = [];
+
+ final ChatUser me = ChatUser(
+ uid: "",
+ name: "Me",
+ );
+
+ final _scroller = ScrollController();
+
+ var _currentID = clientModeID;
+
+ ScrollController get scroller => _scroller;
+
+ Map> get messages => _messages;
+
+ int get currentID => _currentID;
+
+ ChatUser get currentUser =>
+ FFI.serverModel.clients[_currentID]?.chatUser ?? me;
+
+ changeCurrentID(int id) {
+ if (_messages.containsKey(id)) {
+ _currentID = id;
+ notifyListeners();
+ } else {
+ final chatUser = FFI.serverModel.clients[id]?.chatUser;
+ if (chatUser == null) {
+ return debugPrint(
+ "Failed to changeCurrentID,remote user doesn't exist");
+ }
+ _messages[id] = [];
+ _currentID = id;
+ }
+ }
+
+ receive(int id, String text) {
+ if (text.isEmpty) return;
+ // first message show overlay icon
+ if (chatIconOverlayEntry == null) {
+ showChatIconOverlay();
+ }
+ late final chatUser;
+ if (id == clientModeID) {
+ chatUser = ChatUser(
+ name: FFI.ffiModel.pi.username,
+ uid: FFI.getId(),
+ );
+ } else {
+ chatUser = FFI.serverModel.clients[id]?.chatUser;
+ }
+ if (chatUser == null) {
+ return debugPrint("Failed to receive msg,user doesn't exist");
+ }
+ if (!_messages.containsKey(id)) {
+ _messages[id] = [];
+ }
+ _messages[id]!.add(ChatMessage(text: text, user: chatUser));
+ _currentID = id;
+ notifyListeners();
+ scrollToBottom();
+ }
+
+ scrollToBottom() {
+ Future.delayed(Duration(milliseconds: 500), () {
+ _scroller.animateTo(_scroller.position.maxScrollExtent,
+ duration: Duration(milliseconds: 200),
+ curve: Curves.fastLinearToSlowEaseIn);
+ });
+ }
+
+ send(ChatMessage message) {
+ if (message.text != null && message.text!.isNotEmpty) {
+ _messages[_currentID]?.add(message);
+ if (_currentID == clientModeID) {
+ FFI.setByName("chat_client_mode", message.text!);
+ } else {
+ final msg = Map()
+ ..["id"] = _currentID
+ ..["text"] = message.text!;
+ FFI.setByName("chat_server_mode", jsonEncode(msg));
+ }
+ }
+ notifyListeners();
+ scrollToBottom();
+ }
+
+ close() {
+ hideChatIconOverlay();
+ hideChatWindowOverlay();
+ notifyListeners();
+ }
+}
diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart
new file mode 100644
index 000000000..10325f2b0
--- /dev/null
+++ b/flutter/lib/models/file_model.dart
@@ -0,0 +1,775 @@
+import 'dart:async';
+import 'dart:convert';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/pages/file_manager_page.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:path/path.dart' as Path;
+
+import 'model.dart';
+
+enum SortBy { Name, Type, Modified, Size }
+
+class FileModel extends ChangeNotifier {
+ var _isLocal = false;
+ var _selectMode = false;
+
+ var _localOption = DirectoryOption();
+ var _remoteOption = DirectoryOption();
+
+ var _jobId = 0;
+
+ var _jobProgress = JobProgress(); // from rust update
+
+ bool get isLocal => _isLocal;
+
+ bool get selectMode => _selectMode;
+
+ JobProgress get jobProgress => _jobProgress;
+
+ JobState get jobState => _jobProgress.state;
+
+ SortBy _sortStyle = SortBy.Name;
+
+ SortBy get sortStyle => _sortStyle;
+
+ FileDirectory _currentLocalDir = FileDirectory();
+
+ FileDirectory get currentLocalDir => _currentLocalDir;
+
+ FileDirectory _currentRemoteDir = FileDirectory();
+
+ FileDirectory get currentRemoteDir => _currentRemoteDir;
+
+ FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir;
+
+ String get currentHome => _isLocal ? _localOption.home : _remoteOption.home;
+
+ String get currentShortPath {
+ if (currentDir.path.startsWith(currentHome)) {
+ var path = currentDir.path.replaceFirst(currentHome, "");
+ if (path.length == 0) return "";
+ if (path[0] == "/" || path[0] == "\\") {
+ // remove more '/' or '\'
+ path = path.replaceFirst(path[0], "");
+ }
+ return path;
+ } else {
+ return currentDir.path.replaceFirst(currentHome, "");
+ }
+ }
+
+ bool get currentShowHidden =>
+ _isLocal ? _localOption.showHidden : _remoteOption.showHidden;
+
+ bool get currentIsWindows =>
+ _isLocal ? _localOption.isWindows : _remoteOption.isWindows;
+
+ final _fileFetcher = FileFetcher();
+
+ final _jobResultListener = JobResultListener