change main ffi to service
This commit is contained in:
parent
1af3f3f28d
commit
96b3b6a3f9
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
5
android/app/src/main/res/drawable/check_blue.xml
Normal file
5
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
android/app/src/main/res/drawable/close_red.xml
Normal file
5
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>
|
4
android/app/src/main/res/values/colors.xml
Normal file
4
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>
|
@ -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' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user