for merge

This commit is contained in:
rustdesk
2022-05-12 16:50:30 +08:00
parent 9a3e6c63cd
commit 6de0fa781c
149 changed files with 0 additions and 357 deletions

11
flutter/android/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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'

View File

@@ -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"
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.carriez.flutter_hbb">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.carriez.flutter_hbb">
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />-->
<application
android:icon="@mipmap/ic_launcher"
android:label="RustDesk"
android:requestLegacyExternalStorage="true">
<receiver
android:name=".BootReceiver"
android:enabled="false"
android:exported="false">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service
android:name=".InputService"
android:enabled="true"
android:exported="false"
android:label="RustDesk Input"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MainService"
android:enabled="true"
android:foregroundServiceType="mediaProjection" />
<!--
Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
-->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@@ -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)
}
}
}
}

View File

@@ -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<GestureDescription>()
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() {}
}

View File

@@ -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
}
}
}

View File

@@ -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<Intent>(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)
}
}

View File

@@ -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<ByteBuffer>
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
}
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#0071FF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#D74E4E"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#FF0071FF</color>
</resources>

View File

@@ -0,0 +1,4 @@
<resources>
<string name="app_name">RustDesk</string>
<string name="accessibility_service_description">Allow other devices to control your phone using virtual touch, when RustDesk screen sharing is established</string>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,6 @@
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowsChanged"
android:accessibilityFlags="flagDefault"
android:notificationTimeout="50"
android:description="@string/accessibility_service_description"
android:canPerformGestures="true"/>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.carriez.flutter_hbb">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -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
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android" name="Android">
<configuration>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/gen" />
<option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/gen" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/app/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/app/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/app/src/main/assets" />
<option name="LIBS_FOLDER_RELATIVE_PATH" value="/app/src/main/libs" />
<option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/app/src/main/proguard_logs" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/app/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/app/src/main/kotlin" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
</content>
<orderEntry type="jdk" jdkName="Android API 29 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Flutter for Android" level="project" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
</component>
</module>

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -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

BIN
flutter/android/key.jks Normal file

Binary file not shown.

View File

@@ -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"