mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
for merge
This commit is contained in:
11
flutter/android/.gitignore
vendored
Normal file
11
flutter/android/.gitignore
vendored
Normal 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
|
||||
82
flutter/android/app/build.gradle
Normal file
82
flutter/android/app/build.gradle
Normal 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'
|
||||
40
flutter/android/app/google-services.json
Normal file
40
flutter/android/app/google-services.json
Normal 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"
|
||||
}
|
||||
7
flutter/android/app/src/debug/AndroidManifest.xml
Normal file
7
flutter/android/app/src/debug/AndroidManifest.xml
Normal 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>
|
||||
73
flutter/android/app/src/main/AndroidManifest.xml
Normal file
73
flutter/android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
5
flutter/android/app/src/main/res/drawable/check_blue.xml
Normal file
5
flutter/android/app/src/main/res/drawable/check_blue.xml
Normal 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>
|
||||
5
flutter/android/app/src/main/res/drawable/close_red.xml
Normal file
5
flutter/android/app/src/main/res/drawable/close_red.xml
Normal 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>
|
||||
@@ -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>
|
||||
BIN
flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
BIN
flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
18
flutter/android/app/src/main/res/values-night/styles.xml
Normal file
18
flutter/android/app/src/main/res/values-night/styles.xml
Normal 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>
|
||||
4
flutter/android/app/src/main/res/values/colors.xml
Normal file
4
flutter/android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary">#FF0071FF</color>
|
||||
</resources>
|
||||
4
flutter/android/app/src/main/res/values/strings.xml
Normal file
4
flutter/android/app/src/main/res/values/strings.xml
Normal 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>
|
||||
18
flutter/android/app/src/main/res/values/styles.xml
Normal file
18
flutter/android/app/src/main/res/values/styles.xml
Normal 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>
|
||||
@@ -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"/>
|
||||
7
flutter/android/app/src/profile/AndroidManifest.xml
Normal file
7
flutter/android/app/src/profile/AndroidManifest.xml
Normal 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>
|
||||
32
flutter/android/build.gradle
Normal file
32
flutter/android/build.gradle
Normal 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
|
||||
}
|
||||
29
flutter/android/flutter_hbb_android.iml
Normal file
29
flutter/android/flutter_hbb_android.iml
Normal 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>
|
||||
3
flutter/android/gradle.properties
Normal file
3
flutter/android/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
6
flutter/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
flutter/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
BIN
flutter/android/key.jks
Normal file
Binary file not shown.
11
flutter/android/settings.gradle
Normal file
11
flutter/android/settings.gradle
Normal 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"
|
||||
Reference in New Issue
Block a user