Merge pull request #3425 from Heap-Hop/android_start_on_boot

Android start on boot
This commit is contained in:
RustDesk
2023-03-01 11:06:24 +08:00
committed by GitHub
49 changed files with 602 additions and 337 deletions

View File

@@ -11,22 +11,25 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<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" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:icon="@mipmap/ic_launcher"
android:label="RustDesk"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true">
android:supportsRtl="true">
<receiver
android:name=".BootReceiver"
android:enabled="false"
android:exported="false">
android:enabled="true"
android:exported="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<!--ACTION_BOOT_COMPLETED for debug test on no root device-->
<action android:name="com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED" />
</intent-filter>
</receiver>
@@ -53,8 +56,6 @@
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -62,6 +63,11 @@
</intent-filter>
</activity>
<activity
android:name=".PermissionRequestTransparentActivity"
android:excludeFromRecents="true"
android:theme="@style/Transparent" />
<service
android:name=".MainService"
android:enabled="true"
@@ -75,4 +81,4 @@
android:value="2" />
</application>
</manifest>
</manifest>

View File

@@ -1,21 +1,45 @@
package com.carriez.flutter_hbb
import android.Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
import android.Manifest.permission.SYSTEM_ALERT_WINDOW
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import android.widget.Toast
import com.hjq.permissions.XXPermissions
import io.flutter.embedding.android.FlutterActivity
const val DEBUG_BOOT_COMPLETED = "com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED"
class BootReceiver : BroadcastReceiver() {
private val logTag = "tagBootReceiver"
override fun onReceive(context: Context, intent: Intent) {
if ("android.intent.action.BOOT_COMPLETED" == intent.action){
val it = Intent(context,MainService::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
Log.d(logTag, "onReceive ${intent.action}")
if (Intent.ACTION_BOOT_COMPLETED == intent.action || DEBUG_BOOT_COMPLETED == intent.action) {
// check SharedPreferences config
val prefs = context.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
if (!prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) {
Log.d(logTag, "KEY_START_ON_BOOT_OPT is false")
return
}
Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show();
// check pre-permission
if (!XXPermissions.isGranted(context, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, SYSTEM_ALERT_WINDOW)){
Log.d(logTag, "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS or SYSTEM_ALERT_WINDOW is not granted")
return
}
val it = Intent(context, MainService::class.java).apply {
action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE
putExtra(EXT_INIT_FROM_BOOT, true)
}
Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(it)
}else{
} else {
context.startService(it)
}
}

View File

@@ -7,35 +7,29 @@ package com.carriez.flutter_hbb
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
*/
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.IBinder
import android.provider.Settings
import android.util.Log
import android.view.WindowManager
import androidx.annotation.RequiresApi
import com.hjq.permissions.XXPermissions
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
const val MEDIA_REQUEST_CODE = 42
class MainActivity : FlutterActivity() {
companion object {
lateinit var flutterMethodChannel: MethodChannel
var flutterMethodChannel: MethodChannel? = null
}
private val channelTag = "mChannel"
private val logTag = "mMainActivity"
private var mediaProjectionResultIntent: Intent? = null
private var mainService: MainService? = null
@RequiresApi(Build.VERSION_CODES.M)
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
if (MainService.isReady) {
@@ -46,169 +40,32 @@ class MainActivity : FlutterActivity() {
flutterMethodChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
channelTag
).apply {
// make sure result is set, otherwise flutter will await forever
setMethodCallHandler { call, result ->
when (call.method) {
"init_service" -> {
Intent(activity, MainService::class.java).also {
bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
}
if (MainService.isReady) {
result.success(false)
return@setMethodCallHandler
}
getMediaProjection()
result.success(true)
}
"start_capture" -> {
mainService?.let {
result.success(it.startCapture())
} ?: let {
result.success(false)
}
}
"stop_service" -> {
Log.d(logTag, "Stop service")
mainService?.let {
it.destroy()
result.success(true)
} ?: let {
result.success(false)
}
}
"check_permission" -> {
if (call.arguments is String) {
result.success(checkPermission(context, call.arguments as String))
} else {
result.success(false)
}
}
"request_permission" -> {
if (call.arguments is String) {
requestPermission(context, call.arguments as String)
result.success(true)
} else {
result.success(false)
}
}
"check_video_permission" -> {
mainService?.let {
result.success(it.checkMediaPermission())
} ?: let {
result.success(false)
}
}
"check_service" -> {
flutterMethodChannel.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
flutterMethodChannel.invokeMethod(
"on_state_changed",
mapOf("name" to "media", "value" to MainService.isReady.toString())
)
result.success(true)
}
"init_input" -> {
initInput()
result.success(true)
}
"stop_input" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
InputService.ctx?.disableSelf()
}
InputService.ctx = null
flutterMethodChannel.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
result.success(true)
}
"cancel_notification" -> {
try {
val id = call.arguments as Int
mainService?.cancelNotification(id)
} finally {
result.success(true)
}
}
"enable_soft_keyboard" -> {
// https://blog.csdn.net/hanye2020/article/details/105553780
try {
if (call.arguments as Boolean) {
window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
}
} finally {
result.success(true)
}
}
else -> {
result.error("-1", "No such method", null)
}
}
}
}
}
private fun getMediaProjection() {
val mMediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val mIntent = mMediaProjectionManager.createScreenCaptureIntent()
startActivityForResult(mIntent, MEDIA_REQUEST_CODE)
}
private fun initService() {
if (mediaProjectionResultIntent == null) {
Log.w(logTag, "initService fail,mediaProjectionResultIntent is null")
return
}
Log.d(logTag, "Init service")
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = INIT_SERVICE
serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent)
launchMainService(serviceIntent)
}
private fun launchMainService(intent: Intent) {
// TEST api < O
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
}
private fun initInput() {
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
)
initFlutterChannel(flutterMethodChannel!!)
}
override fun onResume() {
super.onResume()
val inputPer = InputService.isOpen
activity.runOnUiThread {
flutterMethodChannel.invokeMethod(
flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to inputPer.toString())
)
}
}
private fun requestMediaProjection() {
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
action = ACT_REQUEST_MEDIA_PROJECTION
}
startActivityForResult(intent, REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == MEDIA_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && data != null) {
mediaProjectionResultIntent = data
initService()
} else {
flutterMethodChannel.invokeMethod("on_media_projection_canceled", null)
}
if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) {
flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null)
}
}
@@ -232,4 +89,138 @@ class MainActivity : FlutterActivity() {
mainService = null
}
}
private fun initFlutterChannel(flutterMethodChannel: MethodChannel) {
flutterMethodChannel.setMethodCallHandler { call, result ->
// make sure result will be invoked, otherwise flutter will await forever
when (call.method) {
"init_service" -> {
Intent(activity, MainService::class.java).also {
bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
}
if (MainService.isReady) {
result.success(false)
return@setMethodCallHandler
}
requestMediaProjection()
result.success(true)
}
"start_capture" -> {
mainService?.let {
result.success(it.startCapture())
} ?: let {
result.success(false)
}
}
"stop_service" -> {
Log.d(logTag, "Stop service")
mainService?.let {
it.destroy()
result.success(true)
} ?: let {
result.success(false)
}
}
"check_permission" -> {
if (call.arguments is String) {
result.success(XXPermissions.isGranted(context, call.arguments as String))
} else {
result.success(false)
}
}
"request_permission" -> {
if (call.arguments is String) {
requestPermission(context, call.arguments as String)
result.success(true)
} else {
result.success(false)
}
}
START_ACTION -> {
if (call.arguments is String) {
startAction(context, call.arguments as String)
result.success(true)
} else {
result.success(false)
}
}
"check_video_permission" -> {
mainService?.let {
result.success(it.checkMediaPermission())
} ?: let {
result.success(false)
}
}
"check_service" -> {
Companion.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
Companion.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "media", "value" to MainService.isReady.toString())
)
result.success(true)
}
"stop_input" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
InputService.ctx?.disableSelf()
}
InputService.ctx = null
Companion.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
result.success(true)
}
"cancel_notification" -> {
if (call.arguments is Int) {
val id = call.arguments as Int
mainService?.cancelNotification(id)
} else {
result.success(true)
}
}
"enable_soft_keyboard" -> {
// https://blog.csdn.net/hanye2020/article/details/105553780
if (call.arguments as Boolean) {
window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
}
result.success(true)
}
GET_START_ON_BOOT_OPT -> {
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false))
}
SET_START_ON_BOOT_OPT -> {
if (call.arguments is Boolean) {
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
val edit = prefs.edit()
edit.putBoolean(KEY_START_ON_BOOT_OPT, call.arguments as Boolean)
edit.apply()
result.success(true)
} else {
result.success(false)
}
}
SYNC_APP_DIR_CONFIG_PATH -> {
if (call.arguments is String) {
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
val edit = prefs.edit()
edit.putString(KEY_APP_DIR_CONFIG_PATH, call.arguments as String)
edit.apply()
result.success(true)
} else {
result.success(false)
}
}
else -> {
result.error("-1", "No such method", null)
}
}
}
}
}

View File

@@ -35,6 +35,7 @@ import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity
import java.util.concurrent.Executors
import kotlin.concurrent.thread
import org.json.JSONException
@@ -43,10 +44,6 @@ import java.nio.ByteBuffer
import kotlin.math.max
import kotlin.math.min
const val EXTRA_MP_DATA = "mp_intent"
const val INIT_SERVICE = "init_service"
const val ACTION_LOGIN_REQ_NOTIFY = "ACTION_LOGIN_REQ_NOTIFY"
const val EXTRA_LOGIN_REQ_NOTIFY = "EXTRA_LOGIN_REQ_NOTIFY"
const val DEFAULT_NOTIFY_TITLE = "RustDesk"
const val DEFAULT_NOTIFY_TEXT = "Service is running"
@@ -147,7 +144,11 @@ class MainService : Service() {
// jvm call rust
private external fun init(ctx: Context)
private external fun startServer()
/// When app start on boot, app_dir will not be passed from flutter
/// so pass a app_dir here to rust server
private external fun startServer(app_dir: String)
private external fun startService()
private external fun onVideoFrameUpdate(buf: ByteBuffer)
private external fun onAudioFrameUpdate(buf: ByteBuffer)
private external fun translateLocale(localeName: String, input: String): String
@@ -195,6 +196,7 @@ class MainService : Service() {
override fun onCreate() {
super.onCreate()
Log.d(logTag,"MainService onCreate")
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
start()
serviceLooper = looper
@@ -202,7 +204,13 @@ class MainService : Service() {
}
updateScreenInfo(resources.configuration.orientation)
initNotification()
startServer()
// keep the config dir same with flutter
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
startServer(configPath)
createForegroundNotification()
}
override fun onDestroy() {
@@ -277,22 +285,30 @@ class MainService : Service() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("whichService", "this service:${Thread.currentThread()}")
Log.d("whichService", "this service: ${Thread.currentThread()}")
super.onStartCommand(intent, flags, startId)
if (intent?.action == INIT_SERVICE) {
Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}")
if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) {
createForegroundNotification()
val mMediaProjectionManager =
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
startService()
}
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
intent.getParcelableExtra<Intent>(EXTRA_MP_DATA)?.let {
intent.getParcelableExtra<Intent>(EXT_MEDIA_PROJECTION_RES_INTENT)?.let {
mediaProjection =
mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
checkMediaPermission()
init(this)
_isReady = true
} ?: let {
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
requestMediaProjection()
}
}
return START_NOT_STICKY // don't use sticky (auto restart),the new service (from auto restart) will lose control
return START_NOT_STICKY // don't use sticky (auto restart), the new service (from auto restart) will lose control
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -300,6 +316,14 @@ class MainService : Service() {
updateScreenInfo(newConfig.orientation)
}
private fun requestMediaProjection() {
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
action = ACT_REQUEST_MEDIA_PROJECTION
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
}
@SuppressLint("WrongConstant")
private fun createSurface(): Surface? {
return if (useVP9) {
@@ -400,13 +424,13 @@ class MainService : Service() {
fun checkMediaPermission(): Boolean {
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
MainActivity.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "media", "value" to isReady.toString())
)
}
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
MainActivity.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
@@ -653,8 +677,8 @@ class MainService : Service() {
@SuppressLint("UnspecifiedImmutableFlag")
private fun genLoginRequestPendingIntent(res: Boolean): PendingIntent {
val intent = Intent(this, MainService::class.java).apply {
action = ACTION_LOGIN_REQ_NOTIFY
putExtra(EXTRA_LOGIN_REQ_NOTIFY, res)
action = ACT_LOGIN_REQ_NOTIFY
putExtra(EXT_LOGIN_REQ_NOTIFY, res)
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getService(this, 111, intent, FLAG_IMMUTABLE)

View File

@@ -0,0 +1,54 @@
package com.carriez.flutter_hbb
import android.app.Activity
import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle
import android.util.Log
class PermissionRequestTransparentActivity: Activity() {
private val logTag = "permissionRequest"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(logTag, "onCreate PermissionRequestTransparentActivity: intent.action: ${intent.action}")
when (intent.action) {
ACT_REQUEST_MEDIA_PROJECTION -> {
val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val intent = mediaProjectionManager.createScreenCaptureIntent()
startActivityForResult(intent, REQ_REQUEST_MEDIA_PROJECTION)
}
else -> finish()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQ_REQUEST_MEDIA_PROJECTION) {
if (resultCode == RESULT_OK && data != null) {
launchService(data)
} else {
setResult(RES_FAILED)
}
}
finish()
}
private fun launchService(mediaProjectionResultIntent: Intent) {
Log.d(logTag, "Launch MainService")
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE
serviceIntent.putExtra(EXT_MEDIA_PROJECTION_RES_INTENT, mediaProjectionResultIntent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
}
}

View File

@@ -1,5 +1,6 @@
package com.carriez.flutter_hbb
import android.Manifest.permission.*
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
@@ -12,8 +13,8 @@ import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
import android.provider.Settings
import android.provider.Settings.*
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService
import com.hjq.permissions.Permission
@@ -22,6 +23,31 @@ import java.nio.ByteBuffer
import java.util.*
// intent action, extra
const val ACT_REQUEST_MEDIA_PROJECTION = "REQUEST_MEDIA_PROJECTION"
const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SERVICE"
const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT"
const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT"
const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
// Activity requestCode
const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101
const val REQ_REQUEST_MEDIA_PROJECTION = 201
// Activity responseCode
const val RES_FAILED = -100
// Flutter channel
const val START_ACTION = "start_action"
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
const val KEY_APP_DIR_CONFIG_PATH = "KEY_APP_DIR_CONFIG_PATH"
@SuppressLint("ConstantLocale")
val LOCAL_NAME = Locale.getDefault().toString()
val SCREEN_INFO = Info(0, 0, 1, 200)
@@ -30,61 +56,13 @@ data class Info(
var width: Int, var height: Int, var scale: Int, var dpi: Int
)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun testVP9Support(): Boolean {
return true
val res = MediaCodecList(MediaCodecList.ALL_CODECS)
.findEncoderForFormat(
MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_VP9,
SCREEN_INFO.width,
SCREEN_INFO.width
)
)
return res != null
}
@RequiresApi(Build.VERSION_CODES.M)
fun requestPermission(context: Context, type: String) {
val permission = when (type) {
"ignore_battery_optimizations" -> {
try {
context.startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:" + context.packageName)
})
} catch (e:Exception) {
e.printStackTrace()
}
return
}
"application_details_settings" -> {
try {
context.startActivity(Intent().apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = "android.settings.APPLICATION_DETAILS_SETTINGS"
data = Uri.parse("package:" + context.packageName)
})
} catch (e:Exception) {
e.printStackTrace()
}
return
}
"audio" -> {
Permission.RECORD_AUDIO
}
"file" -> {
Permission.MANAGE_EXTERNAL_STORAGE
}
else -> {
return
}
}
XXPermissions.with(context)
.permission(permission)
.permission(type)
.request { _, all ->
if (all) {
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
MainActivity.flutterMethodChannel?.invokeMethod(
"on_android_permission_result",
mapOf("type" to type, "result" to all)
)
@@ -93,24 +71,18 @@ fun requestPermission(context: Context, type: String) {
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun checkPermission(context: Context, type: String): Boolean {
val permission = when (type) {
"ignore_battery_optimizations" -> {
val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager
return pw.isIgnoringBatteryOptimizations(context.packageName)
}
"audio" -> {
Permission.RECORD_AUDIO
}
"file" -> {
Permission.MANAGE_EXTERNAL_STORAGE
}
else -> {
return false
}
fun startAction(context: Context, action: String) {
try {
context.startActivity(Intent(action).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// don't pass package name when launch ACTION_ACCESSIBILITY_SETTINGS
if (ACTION_ACCESSIBILITY_SETTINGS != action) {
data = Uri.parse("package:" + context.packageName)
}
})
} catch (e: Exception) {
e.printStackTrace()
}
return XXPermissions.isGranted(context, permission)
}
class AudioReader(val bufSize: Int, private val maxFrames: Int) {

View File

@@ -15,4 +15,12 @@
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
<style name="Transparent" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>

View File

@@ -910,21 +910,14 @@ class AccessibilityListener extends StatelessWidget {
}
}
class PermissionManager {
class AndroidPermissionManager {
static Completer<bool>? _completer;
static Timer? _timer;
static var _current = "";
static final permissions = [
"audio",
"file",
"ignore_battery_optimizations",
"application_details_settings"
];
static bool isWaitingFile() {
if (_completer != null) {
return !_completer!.isCompleted && _current == "file";
return !_completer!.isCompleted && _current == kManageExternalStorage;
}
return false;
}
@@ -933,31 +926,33 @@ class PermissionManager {
if (isDesktop) {
return Future.value(true);
}
if (!permissions.contains(type)) {
return Future.error("Wrong permission!$type");
}
return gFFI.invokeMethod("check_permission", type);
}
// startActivity goto Android Setting's page to request permission manually by user
static void startAction(String action) {
gFFI.invokeMethod(AndroidChannel.kStartAction, action);
}
/// We use XXPermissions to request permissions,
/// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java
static Future<bool> request(String type) {
if (isDesktop) {
return Future.value(true);
}
if (!permissions.contains(type)) {
return Future.error("Wrong permission!$type");
}
gFFI.invokeMethod("request_permission", type);
if (type == "ignore_battery_optimizations") {
return Future.value(false);
// clear last task
if (_completer?.isCompleted == false) {
_completer?.complete(false);
}
_timer?.cancel();
_current = type;
_completer = Completer<bool>();
gFFI.invokeMethod("request_permission", type);
// timeout
_timer?.cancel();
_timer = Timer(Duration(seconds: 60), () {
_timer = Timer(Duration(seconds: 120), () {
if (_completer == null) return;
if (!_completer!.isCompleted) {
_completer!.complete(false);
@@ -1487,8 +1482,8 @@ connect(BuildContext context, String id,
}
} else {
if (isFileTransfer) {
if (!await PermissionManager.check("file")) {
if (!await PermissionManager.request("file")) {
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
return;
}
}

View File

@@ -59,11 +59,12 @@ const double kDesktopFileTransferMaximumWidth = 300;
const double kDesktopFileTransferRowHeight = 30.0;
const double kDesktopFileTransferHeaderHeight = 25.0;
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && Platform.isLinux
? stateGlobal.fullscreen || stateGlobal.maximize
? EdgeInsets.zero
: EdgeInsets.all(5.0)
: EdgeInsets.zero;
EdgeInsets get kDragToResizeAreaPadding =>
!kUseCompatibleUiMode && Platform.isLinux
? stateGlobal.fullscreen || stateGlobal.maximize
? EdgeInsets.zero
: EdgeInsets.all(5.0)
: EdgeInsets.zero;
// https://en.wikipedia.org/wiki/Non-breaking_space
const int $nbsp = 0x00A0;
@@ -136,6 +137,25 @@ const kRemoteAudioDualWay = 'dual-way';
const kIgnoreDpi = true;
/// Android constants
const kActionApplicationDetailsSettings =
"android.settings.APPLICATION_DETAILS_SETTINGS";
const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS";
const kRecordAudio = "android.permission.RECORD_AUDIO";
const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
const kRequestIgnoreBatteryOptimizations =
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
/// Android channel invoke type key
class AndroidChannel {
static final kStartAction = "start_action";
static final kGetStartOnBootOpt = "get_start_on_boot_opt";
static final kSetStartOnBootOpt = "set_start_on_boot_opt";
static final kSyncAppDirConfigPath = "sync_app_dir";
}
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
/// see [LogicalKeyboardKey.keyLabel]
const Map<int, String> logicalKeyMap = <int, String>{

View File

@@ -153,6 +153,7 @@ void runMainApp(bool startService) async {
void runMobileApp() async {
await initEnv(kAppTypeMain);
if (isAndroid) androidChannelInit();
platformFFI.syncAndroidServiceAppDirConfigPath();
runApp(App());
}

View File

@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../consts.dart';
import '../../models/platform_model.dart';
import '../../models/server_model.dart';
import 'home_page.dart';
@@ -40,14 +41,14 @@ class ServerPage extends StatefulWidget implements PageShape {
value: "setTemporaryPasswordLength",
enabled:
gFFI.serverModel.verificationMethod != kUsePermanentPassword,
child: Text(translate("Set temporary password length")),
child: Text(translate("One-time password length")),
),
const PopupMenuDivider(),
PopupMenuItem(
padding: const EdgeInsets.symmetric(horizontal: 0.0),
value: kUseTemporaryPassword,
child: ListTile(
title: Text(translate("Use temporary password")),
title: Text(translate("Use one-time password")),
trailing: Icon(
Icons.check,
color: gFFI.serverModel.verificationMethod ==
@@ -150,10 +151,11 @@ class _ServerPageState extends State<ServerPage> {
}
void checkService() async {
gFFI.invokeMethod("check_service"); // jvm
// for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page
if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
PermissionManager.complete("file", await PermissionManager.check("file"));
gFFI.invokeMethod("check_service");
// for Android 10/11, request MANAGE_EXTERNAL_STORAGE permission from system setting page
if (AndroidPermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
AndroidPermissionManager.complete(kManageExternalStorage,
await AndroidPermissionManager.check(kManageExternalStorage));
debugPrint("file permission finished");
}
}
@@ -567,7 +569,7 @@ void androidChannelInit() {
{
var type = arguments["type"] as String;
var result = arguments["result"] as bool;
PermissionManager.complete(type, result);
AndroidPermissionManager.complete(type, result);
break;
}
case "on_media_projection_canceled":

View File

@@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../widgets/dialog.dart';
@@ -31,18 +32,20 @@ class SettingsPage extends StatefulWidget implements PageShape {
}
const url = 'https://rustdesk.com/';
final _hasIgnoreBattery = androidVersion >= 26;
var _ignoreBatteryOpt = false;
var _enableAbr = false;
var _denyLANDiscovery = false;
var _onlyWhiteList = false;
var _enableDirectIPAccess = false;
var _enableRecordSession = false;
var _autoRecordIncomingSession = false;
var _localIP = "";
var _directAccessPort = "";
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
final _hasIgnoreBattery = androidVersion >= 26;
var _ignoreBatteryOpt = false;
var _enableStartOnBoot = false;
var _enableAbr = false;
var _denyLANDiscovery = false;
var _onlyWhiteList = false;
var _enableDirectIPAccess = false;
var _enableRecordSession = false;
var _autoRecordIncomingSession = false;
var _localIP = "";
var _directAccessPort = "";
@override
void initState() {
super.initState();
@@ -50,11 +53,34 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
() async {
var update = false;
if (_hasIgnoreBattery) {
update = await updateIgnoreBatteryStatus();
if (await checkAndUpdateIgnoreBatteryStatus()) {
update = true;
}
}
final enableAbrRes = await bind.mainGetOption(key: "enable-abr") != "N";
if (await checkAndUpdateStartOnBoot()) {
update = true;
}
// start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
var enableStartOnBoot =
await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt);
if (enableStartOnBoot) {
if (!await canStartOnBoot()) {
enableStartOnBoot = false;
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
}
}
if (enableStartOnBoot != _enableStartOnBoot) {
update = true;
_enableStartOnBoot = enableStartOnBoot;
}
final enableAbrRes = option2bool(
"enable-abr", await bind.mainGetOption(key: "enable-abr"));
if (enableAbrRes != _enableAbr) {
update = true;
_enableAbr = enableAbrRes;
@@ -125,15 +151,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
() async {
if (await updateIgnoreBatteryStatus()) {
final ibs = await checkAndUpdateIgnoreBatteryStatus();
final sob = await checkAndUpdateStartOnBoot();
if (ibs || sob) {
setState(() {});
}
}();
}
}
Future<bool> updateIgnoreBatteryStatus() async {
final res = await PermissionManager.check("ignore_battery_optimizations");
Future<bool> checkAndUpdateIgnoreBatteryStatus() async {
final res = await AndroidPermissionManager.check(
kRequestIgnoreBatteryOptimizations);
if (_ignoreBatteryOpt != res) {
_ignoreBatteryOpt = res;
return true;
@@ -142,6 +171,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
}
}
Future<bool> checkAndUpdateStartOnBoot() async {
if (!await canStartOnBoot() && _enableStartOnBoot) {
_enableStartOnBoot = false;
debugPrint(
"checkAndUpdateStartOnBoot and set _enableStartOnBoot -> false");
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
return true;
} else {
return false;
}
}
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
@@ -265,7 +306,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
]),
onToggle: (v) async {
if (v) {
PermissionManager.request("ignore_battery_optimizations");
await AndroidPermissionManager.request(
kRequestIgnoreBatteryOptimizations);
} else {
final res = await gFFI.dialogManager
.show<bool>((setState, close) => CustomAlertDialog(
@@ -282,11 +324,44 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
],
));
if (res == true) {
PermissionManager.request("application_details_settings");
AndroidPermissionManager.startAction(
kActionApplicationDetailsSettings);
}
}
}));
}
enhancementsTiles.add(SettingsTile.switchTile(
initialValue: _enableStartOnBoot,
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text("${translate('Start on Boot')} (beta)"),
Text(
'* ${translate('Start the screen sharing service on boot, requires special permissions')}',
style: Theme.of(context).textTheme.bodySmall),
]),
onToggle: (toValue) async {
if (toValue) {
// 1. request kIgnoreBatteryOptimizations
if (!await AndroidPermissionManager.check(
kRequestIgnoreBatteryOptimizations)) {
if (!await AndroidPermissionManager.request(
kRequestIgnoreBatteryOptimizations)) {
return;
}
}
// 2. request kSystemAlertWindow
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
return;
}
}
// (Optional) 3. request input permission
}
setState(() => _enableStartOnBoot = toValue);
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
}));
return SettingsList(
sections: [
@@ -387,6 +462,17 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
],
);
}
Future<bool> canStartOnBoot() async {
// start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
if (_hasIgnoreBattery && !_ignoreBatteryOpt) {
return false;
}
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
return false;
}
return true;
}
}
void showServerSettings(OverlayDialogManager dialogManager) async {

View File

@@ -30,7 +30,7 @@ typedef F4Dart = int Function(Pointer<Utf8>);
typedef F5 = Void Function(Pointer<Utf8>);
typedef F5Dart = void Function(Pointer<Utf8>);
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
// pub fn session_register_texture(id: *const char, ptr: usize)
// pub fn session_register_texture(id: *const char, ptr: usize)
typedef F6 = Void Function(Pointer<Utf8>, Uint64);
typedef F6Dart = void Function(Pointer<Utf8>, int);
@@ -56,7 +56,6 @@ class PlatformFFI {
F4Dart? _session_get_rgba_size;
F5Dart? _session_next_rgba;
F6Dart? _session_register_texture;
static get localeName => Platform.localeName;
@@ -162,7 +161,8 @@ class PlatformFFI {
dylib.lookupFunction<F4, F4Dart>("session_get_rgba_size");
_session_next_rgba =
dylib.lookupFunction<F5, F5Dart>("session_next_rgba");
_session_register_texture = dylib.lookupFunction<F6, F6Dart>("session_register_texture");
_session_register_texture =
dylib.lookupFunction<F6, F6Dart>("session_register_texture");
try {
// SYSTEM user failed
_dir = (await getApplicationDocumentsDirectory()).path;
@@ -301,4 +301,8 @@ class PlatformFFI {
if (!isAndroid) return Future<bool>(() => false);
return await _toAndroidChannel.invokeMethod(method, arguments);
}
void syncAndroidServiceAppDirConfigPath() {
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
@@ -154,7 +155,8 @@ class ServerModel with ChangeNotifier {
/// file true by default (if permission on)
checkAndroidPermission() async {
// audio
if (androidVersion < 30 || !await PermissionManager.check("audio")) {
if (androidVersion < 30 ||
!await AndroidPermissionManager.check(kRecordAudio)) {
_audioOk = false;
bind.mainSetOption(key: "enable-audio", value: "N");
} else {
@@ -163,7 +165,7 @@ class ServerModel with ChangeNotifier {
}
// file
if (!await PermissionManager.check("file")) {
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
_fileOk = false;
bind.mainSetOption(key: "enable-file-transfer", value: "N");
} else {
@@ -229,10 +231,10 @@ class ServerModel with ChangeNotifier {
}
toggleAudio() async {
if (!_audioOk && !await PermissionManager.check("audio")) {
final res = await PermissionManager.request("audio");
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
final res = await AndroidPermissionManager.request(kRecordAudio);
if (!res) {
// TODO handle fail
showToast(translate('Failed'));
return;
}
}
@@ -243,10 +245,12 @@ class ServerModel with ChangeNotifier {
}
toggleFile() async {
if (!_fileOk && !await PermissionManager.check("file")) {
final res = await PermissionManager.request("file");
if (!_fileOk &&
!await AndroidPermissionManager.check(kManageExternalStorage)) {
final res =
await AndroidPermissionManager.request(kManageExternalStorage);
if (!res) {
// TODO handle fail
showToast(translate('Failed'));
return;
}
}
@@ -344,10 +348,6 @@ class ServerModel with ChangeNotifier {
}
}
Future<void> initInput() async {
await parent.target?.invokeMethod("init_input");
}
Future<bool> setPermanentPassword(String newPW) async {
await bind.mainSetPermanentPassword(password: newPW);
await Future.delayed(Duration(milliseconds: 500));
@@ -561,7 +561,8 @@ class ServerModel with ChangeNotifier {
}
Future<void> closeAll() async {
await Future.wait(_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
await Future.wait(
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
_clients.clear();
tabController.state.value.tabs.clear();
}
@@ -684,7 +685,7 @@ String getLoginDialogTag(int id) {
showInputWarnAlert(FFI ffi) {
ffi.dialogManager.show((setState, close) {
submit() {
ffi.serverModel.initInput();
AndroidPermissionManager.startAction(kActionAccessibilitySettings);
close();
}