change main ffi to service

This commit is contained in:
csf 2022-02-08 22:44:32 +08:00
parent 1af3f3f28d
commit 96b3b6a3f9
9 changed files with 407 additions and 291 deletions

View File

@ -32,17 +32,22 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion 30 compileSdkVersion 31
ndkVersion '22.1.7171670' // * 使 NDK无法自动选择 使NDK版本 [CSF] ndkVersion '22.1.7171670' // * 使 NDK无法自动选择 使NDK版本 [CSF]
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.carriez.flutter_hbb" applicationId "com.carriez.flutter_hbb"
minSdkVersion 16 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 31
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@ -70,6 +75,8 @@ flutter {
} }
dependencies { dependencies {
implementation "androidx.media:media:1.4.3"
implementation 'com.github.getActivity:XXPermissions:13.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
} }

View File

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.carriez.flutter_hbb"> 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.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -10,6 +12,7 @@
<application <application
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
android:label="RustDesk"> android:label="RustDesk">
<service <service
android:name=".InputService" android:name=".InputService"
@ -31,7 +34,8 @@
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize"
android:exported="true">
<!-- <!--
Specifies an Android theme to apply to this Activity as soon as Specifies an Android theme to apply to this Activity as soon as

View File

@ -1,10 +1,13 @@
package com.carriez.flutter_hbb package com.carriez.flutter_hbb
import android.app.Activity import android.app.Activity
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.media.projection.MediaProjectionManager import android.media.projection.MediaProjectionManager
import android.os.Build import android.os.Build
import android.os.IBinder
import android.provider.Settings import android.provider.Settings
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log import android.util.Log
@ -14,9 +17,7 @@ import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
const val NOTIFY_TYPE_LOGIN_REQ = "NOTIFY_TYPE_LOGIN_REQ"
const val MEDIA_REQUEST_CODE = 42 const val MEDIA_REQUEST_CODE = 42
const val INPUT_REQUEST_CODE = 43
class MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
companion object { companion object {
@ -26,76 +27,14 @@ class MainActivity : FlutterActivity() {
private val channelTag = "mChannel" private val channelTag = "mChannel"
private val logTag = "mMainActivity" private val logTag = "mMainActivity"
private var mediaProjectionResultIntent: Intent? = null private var mediaProjectionResultIntent: Intent? = null
private var mainService: MainService? = null
init {
System.loadLibrary("rustdesk")
}
private external fun init(context: Context)
private external fun close()
fun rustSetByName(name: String, arg1: String, arg2: String) {
when (name) {
"try_start_without_auth" -> {
// to UI
Log.d(logTag, "from rust:got try_start_without_auth")
activity.runOnUiThread {
flutterMethodChannel.invokeMethod(name, mapOf("peerID" to arg1, "name" to arg2))
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
}
val notification = createNormalNotification(
this,
"请求控制",
"来自$arg1:$arg2 请求连接",
NOTIFY_TYPE_LOGIN_REQ
)
with(NotificationManagerCompat.from(this)) {
notify(12, notification)
}
Log.d(logTag, "kotlin invokeMethod try_start_without_auth,done")
}
"start_capture" -> {
Log.d(logTag, "from rust:start_capture")
activity.runOnUiThread {
flutterMethodChannel.invokeMethod(name, mapOf("peerID" to arg1, "name" to arg2))
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
}
// 1.开始捕捉音视频 2.通知栏
startCapture()
val notification = createNormalNotification(
this,
"开始共享屏幕",
"From:$arg2:$arg1",
NOTIFY_TYPE_START_CAPTURE
)
with(NotificationManagerCompat.from(this)) {
notify(13, notification)
}
}
"stop_capture" -> {
Log.d(logTag, "from rust:stop_capture")
stopCapture()
activity.runOnUiThread {
flutterMethodChannel.invokeMethod(name, null)
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
}
}
else -> {}
}
}
override fun onDestroy() {
Log.e(logTag, "onDestroy")
close()
stopCapture()
stopMainService()
stopService(Intent(this, MainService::class.java))
stopService(Intent(this, InputService::class.java))
super.onDestroy()
}
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
Log.d(logTag, "MainActivity configureFlutterEngine,bind to main service")
Intent(this, MainService::class.java).also {
bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
}
super.configureFlutterEngine(flutterEngine) // 必要 否则无法正确初始化flutter super.configureFlutterEngine(flutterEngine) // 必要 否则无法正确初始化flutter
checkPermissions(this) checkPermissions(this)
updateMachineInfo() updateMachineInfo()
@ -107,26 +46,46 @@ class MainActivity : FlutterActivity() {
when (call.method) { when (call.method) {
"init_service" -> { "init_service" -> {
Log.d(logTag, "event from flutter,getPer") Log.d(logTag, "event from flutter,getPer")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getMediaProjection() getMediaProjection()
}
result.success(true) result.success(true)
} }
"start_capture" -> { "start_capture" -> {
startCapture() // return bool
result.success(true) mainService?.let {
result.success(it.startCapture())
} ?: let {
result.success(false)
}
} }
"stop_service" -> { "stop_service" -> {
stopMainService() Log.d(logTag,"Stop service")
mainService?.let {
it.destroy()
result.success(true) result.success(true)
} ?: let {
result.success(false)
} }
"check_input" -> {
checkInput()
result.success(true)
} }
"check_video_permission" -> { "check_video_permission" -> {
val res = MainService.checkMediaPermission() mainService?.let {
result.success(res) result.success(it.checkMediaPermission())
} ?: let {
result.success(false)
}
}
"check_service" -> {
flutterMethodChannel.invokeMethod(
"on_permission_changed",
mapOf("name" to "input", "value" to InputService.isOpen().toString())
)
flutterMethodChannel.invokeMethod(
"on_permission_changed",
mapOf("name" to "media", "value" to mainService?.isReady.toString())
)
}
"init_input" -> {
initInput()
result.success(true)
} }
else -> {} else -> {}
} }
@ -134,7 +93,6 @@ class MainActivity : FlutterActivity() {
} }
} }
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun getMediaProjection() { private fun getMediaProjection() {
val mMediaProjectionManager = val mMediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
@ -142,15 +100,13 @@ class MainActivity : FlutterActivity() {
startActivityForResult(mIntent, MEDIA_REQUEST_CODE) startActivityForResult(mIntent, MEDIA_REQUEST_CODE)
} }
// 实际逻辑是开始监听服务 在成功获取到mediaProjection就开始 // onActivityResult中成功获取到mediaProjection就开始就调用此函数,开始初始化监听服务
private fun initService() { private fun initService() {
if (mediaProjectionResultIntent == null) { if (mediaProjectionResultIntent == null) {
Log.w(logTag, "initService fail,mediaProjectionResultIntent is null") Log.w(logTag, "initService fail,mediaProjectionResultIntent is null")
return return
} }
Log.d(logTag, "Init service") Log.d(logTag, "Init service")
// call init service to rust
init(this)
val serviceIntent = Intent(this, MainService::class.java) val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = INIT_SERVICE serviceIntent.action = INIT_SERVICE
serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent) serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent)
@ -158,35 +114,6 @@ class MainActivity : FlutterActivity() {
launchMainService(serviceIntent) launchMainService(serviceIntent)
} }
private fun startCapture() {
if (mediaProjectionResultIntent == null) {
Log.w(logTag, "startCapture fail,mediaProjectionResultIntent is null")
return
}
Log.d(logTag, "Start Capture")
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = START_CAPTURE
serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent)
launchMainService(serviceIntent)
}
private fun stopCapture() {
Log.d(logTag, "Stop Capture")
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = STOP_CAPTURE
launchMainService(serviceIntent)
}
// TODO 关闭逻辑
private fun stopMainService() {
Log.d(logTag, "Stop service")
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = STOP_SERVICE
launchMainService(serviceIntent)
}
private fun launchMainService(intent: Intent) { private fun launchMainService(intent: Intent) {
// TEST api < O // TEST api < O
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -196,7 +123,7 @@ class MainActivity : FlutterActivity() {
} }
} }
private fun checkInput() { private fun initInput() {
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
if (intent.resolveActivity(packageManager) != null) { if (intent.resolveActivity(packageManager) != null) {
startActivity(intent) startActivity(intent)
@ -208,7 +135,10 @@ class MainActivity : FlutterActivity() {
val inputPer = InputService.isOpen() val inputPer = InputService.isOpen()
Log.d(logTag, "onResume inputPer:$inputPer") Log.d(logTag, "onResume inputPer:$inputPer")
activity.runOnUiThread { activity.runOnUiThread {
flutterMethodChannel.invokeMethod("on_permission_changed",mapOf("name" to "input", "value" to inputPer.toString())) flutterMethodChannel.invokeMethod(
"on_permission_changed",
mapOf("name" to "input", "value" to inputPer.toString())
)
} }
} }
@ -225,16 +155,11 @@ class MainActivity : FlutterActivity() {
// 屏幕尺寸 控制最长边不超过1400 超过则减半并储存缩放比例 实际发送给手机端的尺寸为缩小后的尺寸 // 屏幕尺寸 控制最长边不超过1400 超过则减半并储存缩放比例 实际发送给手机端的尺寸为缩小后的尺寸
// input控制时再通过缩放比例恢复原始尺寸进行path入参 // input控制时再通过缩放比例恢复原始尺寸进行path入参
val dm = DisplayMetrics() val dm = DisplayMetrics()
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display?.getRealMetrics(dm) display?.getRealMetrics(dm)
} else { } else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
@Suppress("DEPRECATION")
windowManager.defaultDisplay.getRealMetrics(dm) windowManager.defaultDisplay.getRealMetrics(dm)
} else {
@Suppress("DEPRECATION")
windowManager.defaultDisplay.getMetrics(dm)
}
} }
var w = dm.widthPixels var w = dm.widthPixels
var h = dm.heightPixels var h = dm.heightPixels
@ -258,4 +183,25 @@ class MainActivity : FlutterActivity() {
Log.e(logTag, "Got Screen Size Fail!") Log.e(logTag, "Got Screen Size Fail!")
} }
} }
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

@ -3,10 +3,15 @@
*/ */
package com.carriez.flutter_hbb package com.carriez.flutter_hbb
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.* import android.app.*
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.PixelFormat import android.graphics.PixelFormat
import android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC import android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
import android.hardware.display.VirtualDisplay import android.hardware.display.VirtualDisplay
@ -14,27 +19,29 @@ import android.media.*
import android.media.AudioRecord.READ_BLOCKING import android.media.AudioRecord.READ_BLOCKING
import android.media.projection.MediaProjection import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager import android.media.projection.MediaProjectionManager
import android.os.Build import android.os.*
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log import android.util.Log
import android.view.Surface import android.view.Surface
import android.view.Surface.FRAME_RATE_COMPATIBILITY_DEFAULT import android.view.Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
import android.widget.Toast
import androidx.annotation.RequiresApi 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 java.util.concurrent.Executors
import kotlin.concurrent.thread import kotlin.concurrent.thread
import androidx.media.app.NotificationCompat.MediaStyle
const val EXTRA_MP_DATA = "mp_intent" const val EXTRA_MP_DATA = "mp_intent"
const val INIT_SERVICE = "init_service" const val INIT_SERVICE = "init_service"
const val START_CAPTURE = "start_capture" const val ACTION_LOGIN_REQ_NOTIFY = "ACTION_LOGIN_REQ_NOTIFY"
const val STOP_CAPTURE = "stop_capture" const val EXTRA_LOGIN_REQ_NOTIFY = "EXTRA_LOGIN_REQ_NOTIFY"
const val STOP_SERVICE = "stop_service"
const val DEFAULT_NOTIFY_TITLE = "RustDesk"
const val DEFAULT_NOTIFY_TEXT = "Service is listening"
const val NOTIFY_ID = 11
const val NOTIFY_TYPE_START_CAPTURE = "NOTIFY_TYPE_START_CAPTURE" const val NOTIFY_TYPE_START_CAPTURE = "NOTIFY_TYPE_START_CAPTURE"
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9 const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
// video const // video const
@ -44,27 +51,12 @@ const val VIDEO_KEY_BIT_RATE = 1024_000
const val VIDEO_KEY_FRAME_RATE = 30 const val VIDEO_KEY_FRAME_RATE = 30
// audio const // audio const
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30 const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
const val AUDIO_SAMPLE_RATE = 48000 const val AUDIO_SAMPLE_RATE = 48000
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
class MainService : Service() { class MainService : Service() {
companion object {
private var mediaProjection: MediaProjection? = null
fun checkMediaPermission(): Boolean {
val value = mediaProjection != null
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
"on_permission_changed",
mapOf("name" to "media", "value" to value.toString())
)
}
return value
}
}
init { init {
System.loadLibrary("rustdesk") System.loadLibrary("rustdesk")
} }
@ -100,27 +92,72 @@ class MainService : Service() {
} }
} }
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun rustSetByName(name: String, arg1: String, arg2: String) { fun rustSetByName(name: String, arg1: String, arg2: String) {
when (name) { when (name) {
"try_start_without_auth" -> {
// to UI
Log.d(logTag, "from rust:got try_start_without_auth")
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
name,
mapOf("peerID" to arg1, "name" to arg2)
)
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
}
// TODO notify
Log.d(logTag, "kotlin invokeMethod try_start_without_auth,done")
}
"start_capture" -> {
Log.d(logTag, "from rust:start_capture")
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
name,
mapOf("peerID" to arg1, "name" to arg2)
)
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
}
if (isStart) {
Log.d(logTag, "正在录制")
return
}
// 1.开始捕捉音视频 2.通知栏
startCapture()
// TODO notify
}
"stop_capture" -> {
Log.d(logTag, "from rust:stop_capture")
stopCapture()
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(name, null)
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
}
}
else -> {} else -> {}
} }
} }
// jvm call rust // jvm call rust
private external fun init(ctx: Context) private external fun init(ctx: Context)
private external fun startServer()
private external fun sendVp9(data: ByteArray) private external fun sendVp9(data: ByteArray)
private val logTag = "LOG_SERVICE" private val logTag = "LOG_SERVICE"
private val useVP9 = false private val useVP9 = false
private val binder = LocalBinder()
private var _isReady = false
private var _isStart = false
val isReady: Boolean
get() = _isReady
val isStart: Boolean
get() = _isStart
// video // video 注意 这里imageReader要成为成员变量防止被回收 https://www.cnblogs.com/yongdaimi/p/11004560.html
private var mediaProjection: MediaProjection? = null
private var surface: Surface? = null private var surface: Surface? = null
private val sendVP9Thread = Executors.newSingleThreadExecutor() private val sendVP9Thread = Executors.newSingleThreadExecutor()
private var videoEncoder: MediaCodec? = null private var videoEncoder: MediaCodec? = null
private var videoData: ByteArray? = null private var videoData: ByteArray? = null
private var imageReader: ImageReader? = private var imageReader: ImageReader? = null
null // * 注意 这里要成为成员变量,防止被回收 https://www.cnblogs.com/yongdaimi/p/11004560.html
private val videoZeroData = ByteArray(32) private val videoZeroData = ByteArray(32)
private var virtualDisplay: VirtualDisplay? = null private var virtualDisplay: VirtualDisplay? = null
@ -132,13 +169,37 @@ class MainService : Service() {
private val audioZeroData: FloatArray = FloatArray(32) // 必须是32位 如果只有8位进行ffi传输时会出错 private val audioZeroData: FloatArray = FloatArray(32) // 必须是32位 如果只有8位进行ffi传输时会出错
private var audioRecordStat = false private var audioRecordStat = false
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) // notification
private lateinit var notificationManager: NotificationManager
private lateinit var notificationChannel: String
private lateinit var notificationBuilder: NotificationCompat.Builder
override fun onCreate() {
super.onCreate()
initNotification()
}
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 { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("whichService", "this service:${Thread.currentThread()}") Log.d("whichService", "this service:${Thread.currentThread()}")
when (intent?.action) { // initService是关键的逻辑 在用户点击开始监听或者获取到视频捕捉权限的时候执行initService
INIT_SERVICE -> { // 只有init的时候通过onStartCommand 且开启前台服务
if (intent?.action == INIT_SERVICE) {
Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}") Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}")
createForegroundNotification(this) // createForegroundNotification(this)
createForegroundNotification()
val mMediaProjectionManager = val mMediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
intent.getParcelableExtra<Intent>(EXTRA_MP_DATA)?.let { intent.getParcelableExtra<Intent>(EXTRA_MP_DATA)?.let {
@ -146,88 +207,27 @@ class MainService : Service() {
mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it) mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
Log.d(logTag, "获取mMediaProjection成功$mediaProjection") Log.d(logTag, "获取mMediaProjection成功$mediaProjection")
checkMediaPermission() checkMediaPermission()
surface = createSurface()
init(this) init(this)
startServer()
_isReady = true
} ?: let { } ?: let {
Log.d(logTag, "获取mMediaProjection失败") Log.d(logTag, "获取mMediaProjection失败")
} }
} } else if (intent?.action == ACTION_LOGIN_REQ_NOTIFY) {
START_CAPTURE -> { val notifyLoginRes = intent.getBooleanExtra(EXTRA_LOGIN_REQ_NOTIFY, false)
startCapture() Log.d(logTag, "从通知栏点击了:$notifyLoginRes")
}
STOP_CAPTURE -> {
stopCapture()
}
STOP_SERVICE -> {
stopCapture()
mediaProjection = null
checkMediaPermission()
stopSelf()
}
} }
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun startCapture(): Boolean {
if (testVP9Support()) { // testVP9Support一直返回true 暂时只使用原始数据
startVideoRecorder()
} else {
Toast.makeText(this, "此设备不支持:$MIME_TYPE", Toast.LENGTH_SHORT).show()
return false
}
// 音频只支持安卓10以及以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startAudioRecorder()
}
checkMediaPermission()
return true
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun stopCapture() {
virtualDisplay?.release()
imageReader?.close()
videoEncoder?.let {
it.signalEndOfInputStream()
it.stop()
it.release()
}
audioRecorder?.startRecording()
audioRecordStat = false
// audioRecorder 如果无法重新创建 保留服务的情况不要释放
// audioRecorder?.stop()
// mediaProjection?.stop()
virtualDisplay = null
imageReader = null
videoEncoder = null
videoData = null
// audioRecorder = null
// audioData = null
}
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun createSurface(): Surface? {
private fun startVideoRecorder() { // 暂时只使用原始数据的情况
Log.d(logTag, "startVideoRecorder") return if (useVP9) {
mediaProjection?.let { mp -> // TODO
if (useVP9) { null
startVP9VideoRecorder(mp)
} else { } else {
startRawVideoRecorder(mp)
}
} ?: let {
Log.d(logTag, "startRecorder fail,mMediaProjection is null")
}
}
@SuppressLint("WrongConstant")
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun startRawVideoRecorder(mp: MediaProjection) {
Log.d(logTag, "startRawVideoRecorder,screen info:$INFO")
// 使用原始数据
imageReader = imageReader =
ImageReader.newInstance( ImageReader.newInstance(
INFO.screenWidth, INFO.screenWidth,
@ -235,7 +235,6 @@ class MainService : Service() {
PixelFormat.RGBA_8888, PixelFormat.RGBA_8888,
2 // maxImages 至少是2 2 // maxImages 至少是2
).apply { ).apply {
// 奇怪的现象必须从MainActivity调用 无法从MainService中调用 会阻塞在这个函数
setOnImageAvailableListener({ imageReader: ImageReader -> setOnImageAvailableListener({ imageReader: ImageReader ->
try { try {
imageReader.acquireLatestImage().use { image -> imageReader.acquireLatestImage().use { image ->
@ -260,15 +259,94 @@ class MainService : Service() {
}, null) }, null)
} }
Log.d(logTag, "ImageReader.setOnImageAvailableListener done") Log.d(logTag, "ImageReader.setOnImageAvailableListener done")
imageReader?.surface
}
}
fun startCapture(): Boolean {
if (mediaProjection == null) {
Log.w(logTag, "startCapture fail,mediaProjection is null")
return false
}
Log.d(logTag, "Start Capture")
if (useVP9) {
startVP9VideoRecorder(mediaProjection!!)
} else {
startRawVideoRecorder(mediaProjection!!)
}
// 音频只支持安卓10以及以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startAudioRecorder()
}
checkMediaPermission()
_isStart = true
return true
}
fun stopCapture() {
Log.d(logTag, "Stop Capture")
_isStart = false
virtualDisplay?.release()
videoEncoder?.let {
it.signalEndOfInputStream()
it.stop()
it.release()
}
audioRecorder?.startRecording()
audioRecordStat = false
// audioRecorder 如果无法重新创建 保留服务的情况不要释放
// audioRecorder?.stop()
// mediaProjection?.stop()
virtualDisplay = null
videoEncoder = null
videoData = null
// audioRecorder = null
// audioData = null
}
fun destroy() {
Log.d(logTag, "destroy service")
_isReady = false
stopCapture()
imageReader?.close()
imageReader = null
mediaProjection = null
checkMediaPermission()
stopForeground(true)
stopSelf()
}
fun checkMediaPermission(): Boolean {
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
"on_permission_changed",
mapOf("name" to "media", "value" to isReady.toString())
)
}
return isReady
}
@SuppressLint("WrongConstant")
private fun startRawVideoRecorder(mp: MediaProjection) {
Log.d(logTag, "startRawVideoRecorder,screen info:$INFO")
if(surface==null){
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
return
}
virtualDisplay = mp.createVirtualDisplay( virtualDisplay = mp.createVirtualDisplay(
"RustDesk", "RustDesk",
INFO.screenWidth, INFO.screenHeight, 200, VIRTUAL_DISPLAY_FLAG_PUBLIC, INFO.screenWidth, INFO.screenHeight, 200, VIRTUAL_DISPLAY_FLAG_PUBLIC,
imageReader?.surface, null, null surface, null, null
) )
Log.d(logTag, "startRawVideoRecorder done")
} }
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @SuppressLint("WrongConstant")
private fun startVP9VideoRecorder(mp: MediaProjection) { private fun startVP9VideoRecorder(mp: MediaProjection) {
//使用内置编码器 //使用内置编码器
createMediaCodec() createMediaCodec()
@ -287,8 +365,7 @@ class MainService : Service() {
} }
} }
private val cb: MediaCodec.Callback = @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private val cb: MediaCodec.Callback = object : MediaCodec.Callback() {
object : MediaCodec.Callback() {
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {} override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {} override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {}
@ -314,7 +391,6 @@ class MainService : Service() {
} }
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun createMediaCodec() { private fun createMediaCodec() {
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE") Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE) videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
@ -377,6 +453,13 @@ class MainService : Service() {
.addMatchingUsage(AudioAttributes.USAGE_ALARM) .addMatchingUsage(AudioAttributes.USAGE_ALARM)
.addMatchingUsage(AudioAttributes.USAGE_GAME) .addMatchingUsage(AudioAttributes.USAGE_GAME)
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build() .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build()
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
return
}
audioRecorder = AudioRecord.Builder() audioRecorder = AudioRecord.Builder()
.setAudioFormat( .setAudioFormat(
AudioFormat.Builder() AudioFormat.Builder()
@ -393,12 +476,85 @@ class MainService : Service() {
Log.d(logTag, "createAudioRecorder fail") Log.d(logTag, "createAudioRecorder fail")
} }
override fun onDestroy() { private fun initNotification() {
Log.d(logTag, "service stop:${Thread.currentThread()}") notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show() // 设置通知渠道 android8开始引入 老版本会被忽略 这个东西的作用相当于为通知分类 给用户选择通知消息的种类
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "RustDesk"
val channelName = "RustDesk Service"
val channel = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "RustDesk Service Channel"
}
channel.lightColor = Color.BLUE
channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
notificationManager.createNotificationChannel(channel)
channelId
} else {
""
}
notificationBuilder = NotificationCompat.Builder(this, notificationChannel)
} }
override fun onBind(intent: Intent): IBinder? { private fun createForegroundNotification() {
return null 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 // 不设置会造成每次都重新启动一个新的Activity
addCategory(Intent.CATEGORY_LAUNCHER)
putExtra("type", type)
}
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
FLAG_UPDATE_CURRENT
)
val notification = notificationBuilder
.setOngoing(true)
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentTitle(DEFAULT_NOTIFY_TITLE)
.setContentText(DEFAULT_NOTIFY_TEXT)
.setOnlyAlertOnce(true)
.setContentIntent(pendingIntent)
.setColor(ContextCompat.getColor(this, R.color.primary))
.setWhen(System.currentTimeMillis())
.build()
startForeground(NOTIFY_ID, notification)
}
private fun loginRequestActionNotification(type: String, name: String, id: String) {
val notification = notificationBuilder
.setContentTitle("收到${type}连接请求")
.setContentText("来自:$name-$id 是否接受")
.setStyle(MediaStyle().setShowActionsInCompactView(0, 1))
.addAction(R.drawable.check_blue, "check", genLoginRequestPendingIntent(true))
.addAction(R.drawable.close_red, "close", genLoginRequestPendingIntent(false))
.build()
notificationManager.notify(NOTIFY_ID, notification)
}
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, 0)
}
}
private fun setTextNotification(_title: String?, _text: String?) {
val title = _title ?: DEFAULT_NOTIFY_TITLE
val text = _text ?: DEFAULT_NOTIFY_TEXT
val notification = notificationBuilder
.clearActions()
.setStyle(null)
.setContentTitle(title)
.setContentText(text)
.build()
notificationManager.notify(NOTIFY_ID, notification)
} }
} }

View File

@ -1,27 +1,25 @@
package com.carriez.flutter_hbb package com.carriez.flutter_hbb
import android.Manifest
import android.app.* import android.app.*
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Icon
import android.media.MediaCodecList import android.media.MediaCodecList
import android.media.MediaFormat import android.media.MediaFormat
import android.os.Build import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import com.hjq.permissions.Permission
import java.util.* import com.hjq.permissions.XXPermissions
val INFO = Info("", "", 0, 0) val INFO = Info("", "", 0, 0)
data class Info(var username:String, var hostname:String, var screenWidth:Int, var screenHeight:Int, data class Info(
var scale:Int = 1) var username: String, var hostname: String, var screenWidth: Int, var screenHeight: Int,
var scale: Int = 1
)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun testVP9Support(): Boolean { fun testVP9Support(): Boolean {
@ -65,7 +63,12 @@ fun createForegroundNotification(ctx:Service) {
ctx.startForeground(11, notification) ctx.startForeground(11, notification)
} }
fun createNormalNotification(ctx: Context,title:String,text:String,type:String): Notification { fun createNormalNotification(
ctx: Context,
title: String,
text: String,
type: String
): Notification {
val channelId = val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "RustDeskNormal" val channelId = "RustDeskNormal"
@ -101,29 +104,13 @@ fun createNormalNotification(ctx: Context,title:String,text:String,type:String):
.build() .build()
} }
const val MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
fun checkPermissions(context: Context) { fun checkPermissions(context: Context) {
val permissions: MutableList<String> = LinkedList() XXPermissions.with(context)
addPermission(context,permissions, Manifest.permission.WRITE_EXTERNAL_STORAGE) .permission(Permission.RECORD_AUDIO)
addPermission(context,permissions, Manifest.permission.RECORD_AUDIO) .permission(Permission.MANAGE_EXTERNAL_STORAGE)
addPermission(context,permissions, Manifest.permission.INTERNET) .request { permissions, all ->
addPermission(context,permissions, Manifest.permission.READ_PHONE_STATE) if (all) {
if (permissions.isNotEmpty()) { Log.d("loglog", "获取存储权限成功:$permissions")
ActivityCompat.requestPermissions(
context as Activity, permissions.toTypedArray(),
MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE
)
} }
} }
private fun addPermission(context:Context,permissionList: MutableList<String>, permission: String) {
if (ContextCompat.checkSelfPermission(
context,
permission
) !== PackageManager.PERMISSION_GRANTED
) {
permissionList.add(permission)
}
} }

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,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#FF0071FF</color>
</resources>

View File

@ -3,6 +3,7 @@ buildscript {
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url 'https://jitpack.io' }
} }
dependencies { dependencies {
@ -16,6 +17,7 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url 'https://jitpack.io' }
} }
} }