forked from rustdesk/rustdesk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
android floating window (rustdesk#8268)
Signed-off-by: 21pages <[email protected]>
- Loading branch information
Showing
13 changed files
with
432 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
307 changes: 307 additions & 0 deletions
307
flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,307 @@ | ||
package com.carriez.flutter_hbb | ||
|
||
import android.annotation.SuppressLint | ||
import android.app.PendingIntent | ||
import android.app.Service | ||
import android.content.Intent | ||
import android.content.res.Configuration | ||
import android.graphics.Bitmap | ||
import android.graphics.Canvas | ||
import android.graphics.PixelFormat | ||
import android.graphics.drawable.BitmapDrawable | ||
import android.graphics.drawable.Drawable | ||
import android.os.Build | ||
import android.os.IBinder | ||
import android.util.Log | ||
import android.view.Gravity | ||
import android.view.MotionEvent | ||
import android.view.View | ||
import android.view.WindowManager | ||
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | ||
import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | ||
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | ||
import android.widget.ImageView | ||
import android.widget.PopupMenu | ||
import com.caverock.androidsvg.SVG | ||
import ffi.FFI | ||
import kotlin.math.abs | ||
|
||
class FloatingWindowService : Service(), View.OnTouchListener { | ||
|
||
private lateinit var windowManager: WindowManager | ||
private lateinit var layoutParams: WindowManager.LayoutParams | ||
private lateinit var floatingView: ImageView | ||
private lateinit var originalDrawable: Drawable | ||
private lateinit var leftHalfDrawable: Drawable | ||
private lateinit var rightHalfDrawable: Drawable | ||
|
||
private var dragging = false | ||
private var lastDownX = 0f | ||
private var lastDownY = 0f | ||
|
||
companion object { | ||
private val logTag = "floatingService" | ||
private var firsCreate = true | ||
private var viewWidth = 120 | ||
private var viewHeight = 120 | ||
private const val MIN_VIEW_SIZE = 32 // size 0 does not help prevent the service from being killed | ||
private const val MAX_VIEW_SIZE = 320 | ||
private var viewTransparency = 1f // 0 means invisible but can help prevent the service from being killed | ||
private var customSvg = "" | ||
private var lastLayoutX = 0 | ||
private var lastLayoutY = 0 | ||
} | ||
|
||
override fun onBind(intent: Intent): IBinder? { | ||
return null | ||
} | ||
|
||
override fun onCreate() { | ||
super.onCreate() | ||
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager | ||
try { | ||
if (firsCreate) { | ||
firsCreate = false | ||
onFirstCreate(windowManager) | ||
} | ||
Log.d(logTag, "floating window size: $viewWidth x $viewHeight, transparency: $viewTransparency, lastLayoutX: $lastLayoutX, lastLayoutY: $lastLayoutY, customSvg: $customSvg") | ||
createView(windowManager) | ||
Log.d(logTag, "onCreate success") | ||
} catch (e: Exception) { | ||
Log.d(logTag, "onCreate failed: $e") | ||
} | ||
} | ||
|
||
@SuppressLint("ClickableViewAccessibility") | ||
private fun createView(windowManager: WindowManager) { | ||
floatingView = ImageView(this) | ||
originalDrawable = resources.getDrawable(R.drawable.floating_window, null) | ||
if (customSvg.isNotEmpty()) { | ||
try { | ||
val svg = SVG.getFromString(customSvg) | ||
Log.d(logTag, "custom svg info: ${svg.documentWidth} x ${svg.documentHeight}"); | ||
// This make the svg render clear | ||
svg.documentWidth = viewWidth * 1f | ||
svg.documentHeight = viewHeight * 1f | ||
originalDrawable = svg.renderToPicture().let { | ||
BitmapDrawable( | ||
resources, | ||
Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888) | ||
.also { bitmap -> | ||
it.draw(Canvas(bitmap)) | ||
}) | ||
} | ||
floatingView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); | ||
Log.d(logTag, "custom svg loaded") | ||
} catch (e: Exception) { | ||
e.printStackTrace() | ||
} | ||
} | ||
val originalBitmap = Bitmap.createBitmap( | ||
originalDrawable.intrinsicWidth, | ||
originalDrawable.intrinsicHeight, | ||
Bitmap.Config.ARGB_8888 | ||
) | ||
val canvas = Canvas(originalBitmap) | ||
originalDrawable.setBounds( | ||
0, | ||
0, | ||
originalDrawable.intrinsicWidth, | ||
originalDrawable.intrinsicHeight | ||
) | ||
originalDrawable.draw(canvas) | ||
val leftHalfBitmap = Bitmap.createBitmap( | ||
originalBitmap, | ||
0, | ||
0, | ||
originalDrawable.intrinsicWidth / 2, | ||
originalDrawable.intrinsicHeight | ||
) | ||
val rightHalfBitmap = Bitmap.createBitmap( | ||
originalBitmap, | ||
originalDrawable.intrinsicWidth / 2, | ||
0, | ||
originalDrawable.intrinsicWidth / 2, | ||
originalDrawable.intrinsicHeight | ||
) | ||
leftHalfDrawable = BitmapDrawable(resources, leftHalfBitmap) | ||
rightHalfDrawable = BitmapDrawable(resources, rightHalfBitmap) | ||
|
||
floatingView.setImageDrawable(rightHalfDrawable) | ||
floatingView.setOnTouchListener(this) | ||
floatingView.alpha = viewTransparency * 1f | ||
|
||
val flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE | ||
layoutParams = WindowManager.LayoutParams( | ||
viewWidth / 2, | ||
viewHeight, | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE, | ||
flags, | ||
PixelFormat.TRANSLUCENT | ||
) | ||
|
||
layoutParams.gravity = Gravity.TOP or Gravity.START | ||
layoutParams.x = lastLayoutX | ||
layoutParams.y = lastLayoutY | ||
|
||
windowManager.addView(floatingView, layoutParams) | ||
moveToScreenSide() | ||
} | ||
|
||
private fun onFirstCreate(windowManager: WindowManager) { | ||
val wh = getScreenSize(windowManager) | ||
val w = wh.first | ||
val h = wh.second | ||
// size | ||
FFI.getLocalOption("floating-window-size").let { | ||
if (it.isNotEmpty()) { | ||
try { | ||
val size = it.toInt() | ||
if (size in MIN_VIEW_SIZE..MAX_VIEW_SIZE && size <= w / 2 && size <= h / 2) { | ||
viewWidth = size | ||
viewHeight = size | ||
} | ||
} catch (e: Exception) { | ||
e.printStackTrace() | ||
} | ||
} | ||
} | ||
// transparency | ||
FFI.getLocalOption("floating-window-transparency").let { | ||
if (it.isNotEmpty()) { | ||
try { | ||
val transparency = it.toInt() | ||
if (transparency in 0..10) { | ||
viewTransparency = transparency * 1f / 10 | ||
} | ||
} catch (e: Exception) { | ||
e.printStackTrace() | ||
} | ||
} | ||
} | ||
// custom svg | ||
FFI.getLocalOption("floating-window-svg").let { | ||
if (it.isNotEmpty()) { | ||
customSvg = it | ||
} | ||
} | ||
// position | ||
lastLayoutX = 0 | ||
lastLayoutY = (wh.second - viewHeight) / 2 | ||
} | ||
|
||
override fun onDestroy() { | ||
super.onDestroy() | ||
windowManager.removeView(floatingView) | ||
} | ||
|
||
private fun performClick() { | ||
showPopupMenu() | ||
} | ||
|
||
override fun onTouch(view: View?, event: MotionEvent?): Boolean { | ||
if (viewTransparency == 0f) return false | ||
when (event?.action) { | ||
MotionEvent.ACTION_DOWN -> { | ||
dragging = false | ||
lastDownX = event.rawX | ||
lastDownY = event.rawY | ||
} | ||
MotionEvent.ACTION_UP -> { | ||
val clickDragTolerance = 10f | ||
if (abs(event.rawX - lastDownX) < clickDragTolerance && abs(event.rawY - lastDownY) < clickDragTolerance) { | ||
performClick() | ||
} else { | ||
moveToScreenSide() | ||
} | ||
} | ||
MotionEvent.ACTION_MOVE -> { | ||
val dx = event.rawX - lastDownX | ||
val dy = event.rawY - lastDownY | ||
// ignore too small fist start moving(some time is click) | ||
if (!dragging && dx*dx+dy*dy < 25) { | ||
return false | ||
} | ||
dragging = true | ||
layoutParams.x = event.rawX.toInt() | ||
layoutParams.y = event.rawY.toInt() | ||
layoutParams.width = viewWidth | ||
floatingView.setImageDrawable(originalDrawable) | ||
windowManager.updateViewLayout(view, layoutParams) | ||
lastLayoutX = layoutParams.x | ||
lastLayoutY = layoutParams.y | ||
} | ||
} | ||
return false | ||
} | ||
|
||
private fun moveToScreenSide(center: Boolean = false) { | ||
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager | ||
val wh = getScreenSize(windowManager) | ||
val w = wh.first | ||
if (layoutParams.x < w / 2) { | ||
layoutParams.x = 0 | ||
floatingView.setImageDrawable(rightHalfDrawable) | ||
} else { | ||
layoutParams.x = w - viewWidth / 2 | ||
floatingView.setImageDrawable(leftHalfDrawable) | ||
} | ||
if (center) { | ||
layoutParams.y = (wh.second - viewHeight) / 2 | ||
} | ||
layoutParams.width = viewWidth / 2 | ||
windowManager.updateViewLayout(floatingView, layoutParams) | ||
lastLayoutX = layoutParams.x | ||
lastLayoutY = layoutParams.y | ||
} | ||
|
||
override fun onConfigurationChanged(newConfig: Configuration) { | ||
super.onConfigurationChanged(newConfig) | ||
moveToScreenSide(true) | ||
} | ||
|
||
private fun showPopupMenu() { | ||
val popupMenu = PopupMenu(this, floatingView) | ||
val idShowRustDesk = 0 | ||
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk")) | ||
val idStopService = 1 | ||
popupMenu.menu.add(0, idStopService, 0, translate("Stop service")) | ||
popupMenu.setOnMenuItemClickListener { menuItem -> | ||
when (menuItem.itemId) { | ||
idShowRustDesk -> { | ||
openMainActivity() | ||
true | ||
} | ||
idStopService -> { | ||
stopMainService() | ||
true | ||
} | ||
else -> false | ||
} | ||
} | ||
popupMenu.setOnDismissListener { | ||
moveToScreenSide() | ||
} | ||
popupMenu.show() | ||
} | ||
|
||
|
||
private fun openMainActivity() { | ||
val intent = Intent(this, MainActivity::class.java) | ||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||
val pendingIntent = PendingIntent.getActivity( | ||
this, 0, intent, | ||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT | ||
) | ||
try { | ||
pendingIntent.send() | ||
} catch (e: PendingIntent.CanceledException) { | ||
e.printStackTrace() | ||
} | ||
} | ||
|
||
private fun stopMainService() { | ||
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null) | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.