mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-15 00:41:01 +03:00
fix: android: target API 36 (#15603)
* fix: android: target API 35 Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: handle API 35 foreground service types Integrate the foreground-service and MediaProjection lifecycle changes from fufesou/rustdesk#68 while leaving storage permission handling to #15602. Co-authored-by: fufesou <linlong1266@gmail.com> Signed-off-by: fufesou <linlong1266@gmail.com> Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: bump required android sdk version to 36, per recent google requirement change. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix(android): clear microphone FGS type when capture stops Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): harden API 36 capture service lifecycle - isolate MediaProjection callbacks per session - keep foreground service types in sync with capture state - handle audio startup failures and shared frame ownership - upgrade AGP to 8.10.1 for API 36 support Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): reset capture state on FGS update failure Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): recover capture after projection failure Propagate virtual display startup failures, clean up partial video resources, and resume capture after media projection is reauthorized. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): preserve voice call during projection replacement Keep the existing capture active until a new projection is acquired, and restore the voice-call audio source when capture restarts. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): use JDK 17 in playground workflow Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): clear pending capture restart on denial Notify MainService when a recovery projection request is canceled so a later projection grant cannot restart stale capture state. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): handle audio and projection recovery failures Verify AudioRecord startup, propagate voice-call restoration failures, and clear stale capture recovery state when projection setup fails. Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
6
.github/workflows/playground.yml
vendored
6
.github/workflows/playground.yml
vendored
@@ -283,7 +283,7 @@ jobs:
|
|||||||
nasm \
|
nasm \
|
||||||
yasm \
|
yasm \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openjdk-11-jdk-headless \
|
openjdk-17-jdk-headless \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
tree \
|
tree \
|
||||||
wget
|
wget
|
||||||
@@ -365,9 +365,9 @@ jobs:
|
|||||||
- name: Build rustdesk
|
- name: Build rustdesk
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
run: |
|
run: |
|
||||||
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
|
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
||||||
# temporary use debug sign config
|
# temporary use debug sign config
|
||||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||||
case ${{ matrix.job.target }} in
|
case ${{ matrix.job.target }} in
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ protobuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 34
|
namespace "com.carriez.flutter_hbb"
|
||||||
|
compileSdkVersion 36
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@@ -99,7 +101,7 @@ android {
|
|||||||
// 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 22
|
minSdkVersion 22
|
||||||
targetSdkVersion 33
|
targetSdkVersion 36
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
@@ -128,6 +130,7 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||||
implementation "androidx.media:media:1.6.0"
|
implementation "androidx.media:media:1.6.0"
|
||||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
@@ -89,7 +91,12 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".MainService"
|
android:name=".MainService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaProjection" />
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="specialUse|mediaProjection|microphone">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="@string/foreground_service_special_use_subtype" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".FloatingWindowService"
|
android:name=".FloatingWindowService"
|
||||||
|
|||||||
@@ -18,7 +18,33 @@ 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 AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
|
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
|
||||||
private val logTag = "LOG_AUDIO_RECORD_HANDLE"
|
companion object {
|
||||||
|
private const val LOG_TAG = "LOG_AUDIO_RECORD_HANDLE"
|
||||||
|
private const val NO_ACTIVE_PUBLISHERS = 0
|
||||||
|
private var activeAudioFramePublishers = NO_ACTIVE_PUBLISHERS
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun acquireAudioFramePublisher() {
|
||||||
|
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
|
||||||
|
FFI.setFrameRawEnable("audio", true)
|
||||||
|
}
|
||||||
|
activeAudioFramePublishers++
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun releaseAudioFramePublisher() {
|
||||||
|
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
|
||||||
|
Log.e(LOG_TAG, "No active audio frame publisher to release")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activeAudioFramePublishers--
|
||||||
|
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
|
||||||
|
FFI.setFrameRawEnable("audio", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logTag = LOG_TAG
|
||||||
|
|
||||||
private var audioRecorder: AudioRecord? = null
|
private var audioRecorder: AudioRecord? = null
|
||||||
private var audioReader: AudioReader? = null
|
private var audioReader: AudioReader? = null
|
||||||
@@ -79,48 +105,94 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// read f32 to byte , length * 4
|
// read f32 to byte , length * 4
|
||||||
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
val bufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||||
AUDIO_SAMPLE_RATE,
|
AUDIO_SAMPLE_RATE,
|
||||||
AUDIO_CHANNEL_MASK,
|
AUDIO_CHANNEL_MASK,
|
||||||
AUDIO_ENCODING
|
AUDIO_ENCODING
|
||||||
)
|
)
|
||||||
if (minBufferSize == 0) {
|
if (bufferSize <= 0) {
|
||||||
Log.d(logTag, "get min buffer size fail!")
|
Log.d(logTag, "get min buffer size fail!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
audioReader = AudioReader(minBufferSize, 4)
|
audioReader = AudioReader(bufferSize, 4)
|
||||||
|
minBufferSize = bufferSize
|
||||||
Log.d(logTag, "init audioData len:$minBufferSize")
|
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
private fun releaseRecorder(recorder: AudioRecord) {
|
||||||
fun startAudioRecorder() {
|
try {
|
||||||
checkAudioReader()
|
recorder.release()
|
||||||
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
} finally {
|
||||||
try {
|
if (audioRecorder === recorder) {
|
||||||
FFI.setFrameRawEnable("audio", true)
|
audioRecorder = null
|
||||||
audioRecorder!!.startRecording()
|
|
||||||
audioRecordStat = true
|
|
||||||
audioThread = thread {
|
|
||||||
while (audioRecordStat) {
|
|
||||||
audioReader!!.readSync(audioRecorder!!)?.let {
|
|
||||||
FFI.onAudioFrameUpdate(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// let's release here rather than onDestroy to avoid threading issue
|
|
||||||
audioRecorder?.release()
|
|
||||||
audioRecorder = null
|
|
||||||
minBufferSize = 0
|
|
||||||
FFI.setFrameRawEnable("audio", false)
|
|
||||||
Log.d(logTag, "Exit audio thread")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(logTag, "startAudioRecorder fail:$e")
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.d(logTag, "startAudioRecorder fail")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun captureAudio(reader: AudioReader, recorder: AudioRecord) {
|
||||||
|
try {
|
||||||
|
while (audioRecordStat) {
|
||||||
|
reader.readSync(recorder)?.let {
|
||||||
|
FFI.onAudioFrameUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
minBufferSize = 0
|
||||||
|
try {
|
||||||
|
releaseRecorder(recorder)
|
||||||
|
} finally {
|
||||||
|
releaseAudioFramePublisher()
|
||||||
|
Log.d(logTag, "Exit audio thread")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun startAudioRecorder(): Boolean {
|
||||||
|
val recorder = audioRecorder
|
||||||
|
if (recorder == null) {
|
||||||
|
Log.d(logTag, "startAudioRecorder fail")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var audioFramePublisherAcquired = false
|
||||||
|
return try {
|
||||||
|
checkAudioReader()
|
||||||
|
val reader = audioReader
|
||||||
|
if (reader == null || minBufferSize == 0) {
|
||||||
|
releaseRecorder(recorder)
|
||||||
|
Log.d(logTag, "startAudioRecorder fail")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
recorder.startRecording()
|
||||||
|
if (recorder.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
|
||||||
|
throw IllegalStateException("AudioRecord failed to enter recording state")
|
||||||
|
}
|
||||||
|
audioRecordStat = true
|
||||||
|
val captureThread = thread(start = false) { captureAudio(reader, recorder) }
|
||||||
|
acquireAudioFramePublisher()
|
||||||
|
audioFramePublisherAcquired = true
|
||||||
|
audioThread = captureThread
|
||||||
|
captureThread.start()
|
||||||
|
true
|
||||||
|
} catch (error: Exception) {
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread = null
|
||||||
|
Log.e(logTag, "startAudioRecorder fail", error)
|
||||||
|
try {
|
||||||
|
releaseRecorder(recorder)
|
||||||
|
} finally {
|
||||||
|
if (audioFramePublisherAcquired) {
|
||||||
|
releaseAudioFramePublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isVoiceCallActive(): Boolean {
|
||||||
|
return audioRecorder?.audioSource == MediaRecorder.AudioSource.VOICE_COMMUNICATION
|
||||||
|
}
|
||||||
|
|
||||||
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
||||||
if (!isSupportVoiceCall()) {
|
if (!isSupportVoiceCall()) {
|
||||||
return false
|
return false
|
||||||
@@ -137,11 +209,9 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
if (!isSupportVoiceCall()) {
|
if (!isSupportVoiceCall()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (isVideoStart()) {
|
val switched = !isVideoStart() || switchOutVoiceCall(mediaProjection)
|
||||||
switchOutVoiceCall(mediaProjection)
|
|
||||||
}
|
|
||||||
tryReleaseAudio()
|
tryReleaseAudio()
|
||||||
return true
|
return switched
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
@@ -159,8 +229,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
Log.e(logTag, "createAudioRecorder fail")
|
Log.e(logTag, "createAudioRecorder fail")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
startAudioRecorder()
|
return startAudioRecorder()
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
@@ -177,8 +246,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
Log.e(logTag, "createAudioRecorder fail")
|
Log.e(logTag, "createAudioRecorder fail")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
startAudioRecorder()
|
return startAudioRecorder()
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryReleaseAudio() {
|
fun tryReleaseAudio() {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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.content.pm.PackageManager
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
@@ -150,7 +151,7 @@ class MainService : Service() {
|
|||||||
if (incomingVoiceCall) {
|
if (incomingVoiceCall) {
|
||||||
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
|
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
|
||||||
} else {
|
} else {
|
||||||
if (!audioRecordHandle.switchOutVoiceCall(mediaProjection)) {
|
if (!switchOutVoiceCall()) {
|
||||||
Log.e(logTag, "switchOutVoiceCall fail")
|
Log.e(logTag, "switchOutVoiceCall fail")
|
||||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
"type" to "custom-nook-nocancel-hasclose-error",
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
@@ -159,7 +160,7 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!audioRecordHandle.switchToVoiceCall(mediaProjection)) {
|
if (!switchToVoiceCall()) {
|
||||||
Log.e(logTag, "switchToVoiceCall fail")
|
Log.e(logTag, "switchToVoiceCall fail")
|
||||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
"type" to "custom-nook-nocancel-hasclose-error",
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
@@ -214,17 +215,19 @@ class MainService : Service() {
|
|||||||
|
|
||||||
// video
|
// video
|
||||||
private var mediaProjection: MediaProjection? = null
|
private var mediaProjection: MediaProjection? = null
|
||||||
private val mediaProjectionCallback = object : MediaProjection.Callback() {
|
private var mediaProjectionCallback: MediaProjection.Callback? = null
|
||||||
override fun onStop() {
|
private var captureRestartPending = false
|
||||||
Log.d(logTag, "MediaProjection stopped")
|
private var captureRestartInVoiceCall = false
|
||||||
stopCapture()
|
private val mediaProjectionResultReceiver =
|
||||||
virtualDisplay?.release()
|
object : ResultReceiver(Handler(Looper.getMainLooper())) {
|
||||||
virtualDisplay = null
|
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
|
||||||
releaseMediaProjection()
|
if (resultCode == RES_FAILED) {
|
||||||
_isReady = false
|
cancelMediaProjectionRecovery()
|
||||||
checkMediaPermission()
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
private var mediaProjectionForegroundService = false
|
||||||
|
private var microphoneForegroundService = false
|
||||||
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
|
||||||
@@ -350,8 +353,6 @@ class MainService : Service() {
|
|||||||
Log.d("whichService", "this service: ${Thread.currentThread()}")
|
Log.d("whichService", "this service: ${Thread.currentThread()}")
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) {
|
if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) {
|
||||||
createForegroundNotification()
|
|
||||||
|
|
||||||
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
||||||
FFI.startService()
|
FFI.startService()
|
||||||
}
|
}
|
||||||
@@ -360,13 +361,7 @@ class MainService : Service() {
|
|||||||
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||||
|
|
||||||
intent.getParcelableExtra<Intent>(EXT_MEDIA_PROJECTION_RES_INTENT)?.let {
|
intent.getParcelableExtra<Intent>(EXT_MEDIA_PROJECTION_RES_INTENT)?.let {
|
||||||
releaseMediaProjection()
|
replaceMediaProjection(mediaProjectionManager, it)
|
||||||
val projection =
|
|
||||||
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
|
|
||||||
projection.registerCallback(mediaProjectionCallback, Handler(Looper.getMainLooper()))
|
|
||||||
mediaProjection = projection
|
|
||||||
_isReady = true
|
|
||||||
checkMediaPermission()
|
|
||||||
} ?: let {
|
} ?: let {
|
||||||
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
|
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
|
||||||
requestMediaProjection()
|
requestMediaProjection()
|
||||||
@@ -380,18 +375,21 @@ class MainService : Service() {
|
|||||||
updateScreenInfo(newConfig.orientation)
|
updateScreenInfo(newConfig.orientation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestMediaProjection() {
|
private fun requestMediaProjection(recovery: Boolean = false) {
|
||||||
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
|
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
|
||||||
action = ACT_REQUEST_MEDIA_PROJECTION
|
action = ACT_REQUEST_MEDIA_PROJECTION
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
if (recovery) {
|
||||||
|
putExtra(EXT_MEDIA_PROJECTION_RESULT_RECEIVER, mediaProjectionResultReceiver)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseMediaProjection() {
|
@Synchronized
|
||||||
mediaProjection?.unregisterCallback(mediaProjectionCallback)
|
private fun cancelMediaProjectionRecovery() {
|
||||||
mediaProjection?.stop()
|
captureRestartPending = false
|
||||||
mediaProjection = null
|
captureRestartInVoiceCall = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
@@ -427,15 +425,149 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onVoiceCallStarted(): Boolean {
|
private fun releaseMediaProjection() {
|
||||||
return audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
val projection = mediaProjection
|
||||||
|
val callback = mediaProjectionCallback
|
||||||
|
mediaProjection = null
|
||||||
|
mediaProjectionCallback = null
|
||||||
|
if (projection != null && callback != null) {
|
||||||
|
projection.unregisterCallback(callback)
|
||||||
|
}
|
||||||
|
projection?.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun handleMediaProjectionStopped(stoppedProjection: MediaProjection) {
|
||||||
|
if (mediaProjection !== stoppedProjection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.d(logTag, "MediaProjection stopped")
|
||||||
|
setMediaProjectionForegroundService(false)
|
||||||
|
stopCapture()
|
||||||
|
virtualDisplay?.release()
|
||||||
|
virtualDisplay = null
|
||||||
|
mediaProjection = null
|
||||||
|
mediaProjectionCallback = null
|
||||||
|
_isReady = false
|
||||||
|
checkMediaPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun replaceMediaProjection(
|
||||||
|
mediaProjectionManager: MediaProjectionManager,
|
||||||
|
resultIntent: Intent,
|
||||||
|
) {
|
||||||
|
val wasCapturing = isStart
|
||||||
|
val restartCapture = wasCapturing || captureRestartPending
|
||||||
|
val restartInVoiceCall = if (wasCapturing) {
|
||||||
|
audioRecordHandle.isVoiceCallActive()
|
||||||
|
} else {
|
||||||
|
captureRestartInVoiceCall
|
||||||
|
}
|
||||||
|
val hadProjection = mediaProjection != null
|
||||||
|
if (!setMediaProjectionForegroundService(true)) {
|
||||||
|
if (!hadProjection) {
|
||||||
|
cancelMediaProjectionRecovery()
|
||||||
|
_isReady = false
|
||||||
|
checkMediaPermission()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val projection =
|
||||||
|
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, resultIntent)
|
||||||
|
if (projection == null) {
|
||||||
|
if (!hadProjection) {
|
||||||
|
cancelMediaProjectionRecovery()
|
||||||
|
_isReady = false
|
||||||
|
setMediaProjectionForegroundService(false)
|
||||||
|
checkMediaPermission()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (wasCapturing) {
|
||||||
|
stopCapture()
|
||||||
|
}
|
||||||
|
captureRestartPending = restartCapture
|
||||||
|
virtualDisplay?.release()
|
||||||
|
virtualDisplay = null
|
||||||
|
releaseMediaProjection()
|
||||||
|
val callback = object : MediaProjection.Callback() {
|
||||||
|
override fun onStop() {
|
||||||
|
handleMediaProjectionStopped(projection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
projection.registerCallback(callback, Handler(Looper.getMainLooper()))
|
||||||
|
mediaProjection = projection
|
||||||
|
mediaProjectionCallback = callback
|
||||||
|
_isReady = true
|
||||||
|
checkMediaPermission()
|
||||||
|
if (restartCapture) {
|
||||||
|
captureRestartPending = false
|
||||||
|
startCapture(restartInVoiceCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun startMicrophoneCapture(startAudio: () -> Boolean): Boolean {
|
||||||
|
if (!setMicrophoneForegroundService(true)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (startAudio()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
setMicrophoneForegroundService(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun stopMicrophoneCapture(stopAudio: () -> Boolean): Boolean {
|
||||||
|
val stopped = stopAudio()
|
||||||
|
val foregroundServiceUpdated = setMicrophoneForegroundService(false)
|
||||||
|
return stopped && foregroundServiceUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun switchToVoiceCall(): Boolean {
|
||||||
|
if (captureRestartPending) {
|
||||||
|
captureRestartInVoiceCall = true
|
||||||
|
}
|
||||||
|
return startMicrophoneCapture {
|
||||||
|
audioRecordHandle.switchToVoiceCall(mediaProjection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun switchOutVoiceCall(): Boolean {
|
||||||
|
captureRestartInVoiceCall = false
|
||||||
|
val switched = audioRecordHandle.switchOutVoiceCall(mediaProjection)
|
||||||
|
val foregroundServiceUpdated = setMicrophoneForegroundService(false)
|
||||||
|
return switched && foregroundServiceUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun onVoiceCallStarted(): Boolean {
|
||||||
|
if (captureRestartPending) {
|
||||||
|
captureRestartInVoiceCall = true
|
||||||
|
}
|
||||||
|
return startMicrophoneCapture {
|
||||||
|
audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun onVoiceCallClosed(): Boolean {
|
fun onVoiceCallClosed(): Boolean {
|
||||||
return audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
captureRestartInVoiceCall = false
|
||||||
|
return stopMicrophoneCapture {
|
||||||
|
audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startCapture(): Boolean {
|
fun startCapture(): Boolean {
|
||||||
|
return startCapture(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun startCapture(inVoiceCall: Boolean): Boolean {
|
||||||
if (isStart) {
|
if (isStart) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -443,25 +575,35 @@ class MainService : Service() {
|
|||||||
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
captureRestartInVoiceCall = inVoiceCall
|
||||||
|
|
||||||
updateScreenInfo(resources.configuration.orientation)
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
Log.d(logTag, "Start Capture")
|
Log.d(logTag, "Start Capture")
|
||||||
surface = createSurface()
|
surface = createSurface()
|
||||||
|
|
||||||
if (useVP9) {
|
val videoStarted = if (useVP9) {
|
||||||
startVP9VideoRecorder(mediaProjection!!)
|
startVP9VideoRecorder(mediaProjection!!)
|
||||||
} else {
|
} else {
|
||||||
startRawVideoRecorder(mediaProjection!!)
|
startRawVideoRecorder(mediaProjection!!)
|
||||||
}
|
}
|
||||||
|
if (!videoStarted) {
|
||||||
|
if (!captureRestartPending) {
|
||||||
|
captureRestartInVoiceCall = false
|
||||||
|
}
|
||||||
|
releaseFailedVideoCapture()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
if (!audioRecordHandle.createAudioRecorder(false, mediaProjection)) {
|
val audioStarted = if (inVoiceCall) {
|
||||||
Log.d(logTag, "createAudioRecorder fail")
|
switchToVoiceCall()
|
||||||
} else {
|
} else {
|
||||||
Log.d(logTag, "audio recorder start")
|
audioRecordHandle.createAudioRecorder(false, mediaProjection) &&
|
||||||
audioRecordHandle.startAudioRecorder()
|
audioRecordHandle.startAudioRecorder()
|
||||||
}
|
}
|
||||||
|
Log.d(logTag, if (audioStarted) "audio recorder start" else "audio recorder start failed")
|
||||||
}
|
}
|
||||||
|
captureRestartInVoiceCall = false
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
_isStart = true
|
_isStart = true
|
||||||
FFI.setFrameRawEnable("video",true)
|
FFI.setFrameRawEnable("video",true)
|
||||||
@@ -469,9 +611,24 @@ class MainService : Service() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun releaseFailedVideoCapture() {
|
||||||
|
imageReader?.close()
|
||||||
|
imageReader = null
|
||||||
|
videoEncoder?.let {
|
||||||
|
it.signalEndOfInputStream()
|
||||||
|
it.stop()
|
||||||
|
it.release()
|
||||||
|
}
|
||||||
|
videoEncoder = null
|
||||||
|
surface?.release()
|
||||||
|
surface = null
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stopCapture() {
|
fun stopCapture() {
|
||||||
Log.d(logTag, "Stop Capture")
|
Log.d(logTag, "Stop Capture")
|
||||||
|
captureRestartPending = false
|
||||||
|
captureRestartInVoiceCall = false
|
||||||
FFI.setFrameRawEnable("video",false)
|
FFI.setFrameRawEnable("video",false)
|
||||||
_isStart = false
|
_isStart = false
|
||||||
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
|
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
|
||||||
@@ -502,8 +659,11 @@ class MainService : Service() {
|
|||||||
surface?.release()
|
surface?.release()
|
||||||
|
|
||||||
// release audio
|
// release audio
|
||||||
_isAudioStart = false
|
stopMicrophoneCapture {
|
||||||
audioRecordHandle.tryReleaseAudio()
|
_isAudioStart = false
|
||||||
|
audioRecordHandle.tryReleaseAudio()
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
@@ -519,6 +679,8 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
releaseMediaProjection()
|
releaseMediaProjection()
|
||||||
|
mediaProjectionForegroundService = false
|
||||||
|
microphoneForegroundService = false
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
stopService(Intent(this, FloatingWindowService::class.java))
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
@@ -541,49 +703,70 @@ class MainService : Service() {
|
|||||||
return isReady
|
return isReady
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRawVideoRecorder(mp: MediaProjection) {
|
private fun startRawVideoRecorder(mp: MediaProjection): Boolean {
|
||||||
Log.d(logTag, "startRawVideoRecorder,screen info:$SCREEN_INFO")
|
Log.d(logTag, "startRawVideoRecorder,screen info:$SCREEN_INFO")
|
||||||
if (surface == null) {
|
val captureSurface = surface
|
||||||
|
if (captureSurface == null) {
|
||||||
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
createOrSetVirtualDisplay(mp, surface!!)
|
return createOrSetVirtualDisplay(mp, captureSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startVP9VideoRecorder(mp: MediaProjection) {
|
private fun startVP9VideoRecorder(mp: MediaProjection): Boolean {
|
||||||
createMediaCodec()
|
createMediaCodec()
|
||||||
videoEncoder?.let {
|
val encoder = videoEncoder ?: return false
|
||||||
surface = it.createInputSurface()
|
val inputSurface = encoder.createInputSurface()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
surface = inputSurface
|
||||||
surface!!.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
}
|
inputSurface.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT)
|
||||||
it.setCallback(cb)
|
|
||||||
it.start()
|
|
||||||
createOrSetVirtualDisplay(mp, surface!!)
|
|
||||||
}
|
}
|
||||||
|
encoder.setCallback(cb)
|
||||||
|
encoder.start()
|
||||||
|
return createOrSetVirtualDisplay(mp, inputSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
|
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
|
||||||
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
|
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
|
||||||
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) {
|
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface): Boolean {
|
||||||
try {
|
return try {
|
||||||
virtualDisplay?.let {
|
val existingDisplay = virtualDisplay
|
||||||
it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
|
if (existingDisplay != null) {
|
||||||
it.setSurface(s)
|
existingDisplay.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
|
||||||
} ?: let {
|
existingDisplay.setSurface(s)
|
||||||
virtualDisplay = mp.createVirtualDisplay(
|
true
|
||||||
|
} else {
|
||||||
|
val display = mp.createVirtualDisplay(
|
||||||
"RustDeskVD",
|
"RustDeskVD",
|
||||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||||
s, null, null
|
s, null, null
|
||||||
)
|
)
|
||||||
|
if (display == null) {
|
||||||
|
Log.e(logTag, "createOrSetVirtualDisplay failed")
|
||||||
|
handleVirtualDisplayFailure()
|
||||||
|
} else {
|
||||||
|
virtualDisplay = display
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation");
|
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException", e)
|
||||||
// This initiates a prompt dialog for the user to confirm screen projection.
|
handleVirtualDisplayFailure()
|
||||||
requestMediaProjection()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleVirtualDisplayFailure(): Boolean {
|
||||||
|
captureRestartPending = true
|
||||||
|
virtualDisplay?.release()
|
||||||
|
virtualDisplay = null
|
||||||
|
releaseMediaProjection()
|
||||||
|
setMediaProjectionForegroundService(false)
|
||||||
|
_isReady = false
|
||||||
|
checkMediaPermission()
|
||||||
|
requestMediaProjection(true)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private val cb: MediaCodec.Callback = object : MediaCodec.Callback() {
|
private val cb: 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) {}
|
||||||
@@ -674,7 +857,63 @@ class MainService : Service() {
|
|||||||
.setColor(ContextCompat.getColor(this, R.color.primary))
|
.setColor(ContextCompat.getColor(this, R.color.primary))
|
||||||
.setWhen(System.currentTimeMillis())
|
.setWhen(System.currentTimeMillis())
|
||||||
.build()
|
.build()
|
||||||
startForeground(DEFAULT_NOTIFY_ID, notification)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(DEFAULT_NOTIFY_ID, notification, foregroundServiceType())
|
||||||
|
} else {
|
||||||
|
startForeground(DEFAULT_NOTIFY_ID, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
private fun foregroundServiceType(): Int {
|
||||||
|
var serviceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
// Keep a valid FGS type while the unattended host is idle and no capture type is active.
|
||||||
|
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||||
|
}
|
||||||
|
if (mediaProjectionForegroundService) {
|
||||||
|
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && microphoneForegroundService) {
|
||||||
|
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||||
|
}
|
||||||
|
return serviceType
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMediaProjectionForegroundService(enabled: Boolean): Boolean {
|
||||||
|
return updateForegroundServiceTypes(enabled, microphoneForegroundService)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMicrophoneForegroundService(enabled: Boolean): Boolean {
|
||||||
|
return updateForegroundServiceTypes(mediaProjectionForegroundService, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateForegroundServiceTypes(
|
||||||
|
mediaProjectionEnabled: Boolean,
|
||||||
|
microphoneEnabled: Boolean,
|
||||||
|
): Boolean {
|
||||||
|
if (mediaProjectionForegroundService == mediaProjectionEnabled &&
|
||||||
|
microphoneForegroundService == microphoneEnabled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val previousMediaProjection = mediaProjectionForegroundService
|
||||||
|
val previousMicrophone = microphoneForegroundService
|
||||||
|
mediaProjectionForegroundService = mediaProjectionEnabled
|
||||||
|
microphoneForegroundService = microphoneEnabled
|
||||||
|
return try {
|
||||||
|
createForegroundNotification()
|
||||||
|
true
|
||||||
|
} catch (error: SecurityException) {
|
||||||
|
mediaProjectionForegroundService = previousMediaProjection
|
||||||
|
microphoneForegroundService = previousMicrophone
|
||||||
|
Log.e(logTag, "Failed to update foreground service types", error)
|
||||||
|
false
|
||||||
|
} catch (error: IllegalStateException) {
|
||||||
|
mediaProjectionForegroundService = previousMediaProjection
|
||||||
|
microphoneForegroundService = previousMicrophone
|
||||||
|
Log.e(logTag, "Failed to update foreground service types", error)
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loginRequestNotification(
|
private fun loginRequestNotification(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.Intent
|
|||||||
import android.media.projection.MediaProjectionManager
|
import android.media.projection.MediaProjectionManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.ResultReceiver
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
class PermissionRequestTransparentActivity: Activity() {
|
class PermissionRequestTransparentActivity: Activity() {
|
||||||
@@ -31,7 +32,13 @@ class PermissionRequestTransparentActivity: Activity() {
|
|||||||
if (resultCode == RESULT_OK && data != null) {
|
if (resultCode == RESULT_OK && data != null) {
|
||||||
launchService(data)
|
launchService(data)
|
||||||
} else {
|
} else {
|
||||||
setResult(RES_FAILED)
|
val resultReceiver =
|
||||||
|
intent.getParcelableExtra<ResultReceiver>(EXT_MEDIA_PROJECTION_RESULT_RECEIVER)
|
||||||
|
if (resultReceiver != null) {
|
||||||
|
resultReceiver.send(RES_FAILED, null)
|
||||||
|
} else {
|
||||||
|
setResult(RES_FAILED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SER
|
|||||||
const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
||||||
const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT"
|
const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT"
|
||||||
const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT"
|
const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT"
|
||||||
|
const val EXT_MEDIA_PROJECTION_RESULT_RECEIVER = "MEDIA_PROJECTION_RESULT_RECEIVER"
|
||||||
const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
||||||
|
|
||||||
// Activity requestCode
|
// Activity requestCode
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">RustDesk</string>
|
<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>
|
<string name="accessibility_service_description">Allow other devices to control your phone using virtual touch, when RustDesk screen sharing is established</string>
|
||||||
|
<string name="foreground_service_special_use_subtype">Keeps the RustDesk remote desktop host available for authorized unattended connections and foreground notifications without starting screen capture before user approval.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,3 +1,29 @@
|
|||||||
|
def legacyPluginNamespaces = [
|
||||||
|
external_path: 'com.pinciat.external_path',
|
||||||
|
flutter_keyboard_visibility: 'com.jrai.flutter_keyboard_visibility',
|
||||||
|
qr_code_scanner: 'net.touchcapture.qr.flutterqr',
|
||||||
|
sqflite: 'com.tekartik.sqflite',
|
||||||
|
uni_links: 'name.avioli.unilinks',
|
||||||
|
]
|
||||||
|
|
||||||
|
def java8JvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
def java8KotlinJvmTargets = [
|
||||||
|
app: java8JvmTarget,
|
||||||
|
external_path: java8JvmTarget,
|
||||||
|
qr_code_scanner: java8JvmTarget,
|
||||||
|
]
|
||||||
|
|
||||||
|
def configureKotlinJvmTarget = { Project project, String kotlinJvmTarget ->
|
||||||
|
project.plugins.withId('kotlin-android') {
|
||||||
|
project.tasks.configureEach { task ->
|
||||||
|
if (!task.hasProperty('kotlinOptions')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.kotlinOptions.jvmTarget = kotlinJvmTarget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -9,6 +35,16 @@ allprojects {
|
|||||||
rootProject.buildDir = '../build'
|
rootProject.buildDir = '../build'
|
||||||
subprojects {
|
subprojects {
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
def legacyNamespace = legacyPluginNamespaces[project.name]
|
||||||
|
if (legacyNamespace != null) {
|
||||||
|
project.plugins.withId('com.android.library') {
|
||||||
|
project.android.namespace = legacyNamespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def kotlinJvmTarget = java8KotlinJvmTargets[project.name]
|
||||||
|
if (kotlinJvmTarget != null) {
|
||||||
|
configureKotlinJvmTarget(project, kotlinJvmTarget)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
subprojects {
|
subprojects {
|
||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "7.3.1" apply false
|
id "com.android.application" version "8.10.1" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user