|
|
|
@ -12,6 +12,7 @@ import android.graphics.PixelFormat |
|
|
|
import android.graphics.drawable.GradientDrawable |
|
|
|
import android.graphics.drawable.GradientDrawable |
|
|
|
import android.os.Build |
|
|
|
import android.os.Build |
|
|
|
import android.os.IBinder |
|
|
|
import android.os.IBinder |
|
|
|
|
|
|
|
import android.os.SystemClock |
|
|
|
import android.provider.Settings |
|
|
|
import android.provider.Settings |
|
|
|
import android.view.Gravity |
|
|
|
import android.view.Gravity |
|
|
|
import android.view.LayoutInflater |
|
|
|
import android.view.LayoutInflater |
|
|
|
@ -29,9 +30,10 @@ import kotlinx.coroutines.flow.StateFlow |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Оверлей PTT поверх других приложений: плавающая круглая кнопка, drag-and-drop. |
|
|
|
* Оверлей PTT поверх других приложений: плавающая круглая кнопка, drag-and-drop. |
|
|
|
* Удержание активирует передачу (RadioAudioEngine.talkBegin/talkEnd), отпускание — |
|
|
|
* Удержание > PTT_HOLD_MS активирует передачу (RadioAudioEngine.talkBegin/talkEnd), |
|
|
|
* завершает. Работает пока включена опция «PTT поверх приложений» и разрешено |
|
|
|
* отпускание — завершает. Двойной тап фиксирует передачу (latch) без удержания, |
|
|
|
* «поверх других приложений» (SYSTEM_ALERT_WINDOW). |
|
|
|
* повторный двойной тап или drag — отбой. Работает пока включена опция |
|
|
|
|
|
|
|
* «PTT поверх приложений» и разрешено «поверх других приложений» (SYSTEM_ALERT_WINDOW). |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
class RadioOverlayService : Service() { |
|
|
|
class RadioOverlayService : Service() { |
|
|
|
|
|
|
|
|
|
|
|
@ -41,6 +43,7 @@ class RadioOverlayService : Service() { |
|
|
|
const val COLOR_IDLE = 0xFF81C784.toInt() |
|
|
|
const val COLOR_IDLE = 0xFF81C784.toInt() |
|
|
|
const val COLOR_TALKING = 0xFFE53935.toInt() |
|
|
|
const val COLOR_TALKING = 0xFFE53935.toInt() |
|
|
|
const val DRAG_LOCK_MS = 300L |
|
|
|
const val DRAG_LOCK_MS = 300L |
|
|
|
|
|
|
|
const val PTT_HOLD_MS = 250L |
|
|
|
|
|
|
|
|
|
|
|
private val _isShown = MutableStateFlow(false) |
|
|
|
private val _isShown = MutableStateFlow(false) |
|
|
|
val isShown: StateFlow<Boolean> = _isShown |
|
|
|
val isShown: StateFlow<Boolean> = _isShown |
|
|
|
@ -135,7 +138,7 @@ class RadioOverlayService : Service() { |
|
|
|
val wm = windowManager ?: return |
|
|
|
val wm = windowManager ?: return |
|
|
|
val v = TextView(this).apply { |
|
|
|
val v = TextView(this).apply { |
|
|
|
text = "\uD83C\uDF99" |
|
|
|
text = "\uD83C\uDF99" |
|
|
|
textSize = 30f |
|
|
|
textSize = 21f |
|
|
|
gravity = android.view.Gravity.CENTER |
|
|
|
gravity = android.view.Gravity.CENTER |
|
|
|
background = GradientDrawable().apply { |
|
|
|
background = GradientDrawable().apply { |
|
|
|
shape = GradientDrawable.OVAL |
|
|
|
shape = GradientDrawable.OVAL |
|
|
|
@ -143,6 +146,7 @@ class RadioOverlayService : Service() { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
val touchSlop = (ViewConfiguration.get(this).scaledTouchSlop * 2.5f).toInt() |
|
|
|
val touchSlop = (ViewConfiguration.get(this).scaledTouchSlop * 2.5f).toInt() |
|
|
|
|
|
|
|
val doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout().toLong() |
|
|
|
val lp = WindowManager.LayoutParams( |
|
|
|
val lp = WindowManager.LayoutParams( |
|
|
|
WindowManager.LayoutParams.WRAP_CONTENT, |
|
|
|
WindowManager.LayoutParams.WRAP_CONTENT, |
|
|
|
WindowManager.LayoutParams.WRAP_CONTENT, |
|
|
|
WindowManager.LayoutParams.WRAP_CONTENT, |
|
|
|
@ -153,7 +157,7 @@ class RadioOverlayService : Service() { |
|
|
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, |
|
|
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, |
|
|
|
PixelFormat.TRANSLUCENT |
|
|
|
PixelFormat.TRANSLUCENT |
|
|
|
).apply { |
|
|
|
).apply { |
|
|
|
width = dp(96); height = dp(96) |
|
|
|
width = dp(67); height = dp(67) |
|
|
|
gravity = Gravity.TOP or Gravity.START |
|
|
|
gravity = Gravity.TOP or Gravity.START |
|
|
|
x = 16; y = 200 |
|
|
|
x = 16; y = 200 |
|
|
|
} |
|
|
|
} |
|
|
|
@ -162,6 +166,9 @@ class RadioOverlayService : Service() { |
|
|
|
private var touchX = 0f; private var touchY = 0f |
|
|
|
private var touchX = 0f; private var touchY = 0f |
|
|
|
private var dragging = false |
|
|
|
private var dragging = false |
|
|
|
private var dragAllowed = true |
|
|
|
private var dragAllowed = true |
|
|
|
|
|
|
|
private var latched = false |
|
|
|
|
|
|
|
private var lastTapUpTime = 0L |
|
|
|
|
|
|
|
private val talkBeginRunnable = Runnable { beginTalk() } |
|
|
|
|
|
|
|
|
|
|
|
override fun onTouch(view: View, event: MotionEvent): Boolean { |
|
|
|
override fun onTouch(view: View, event: MotionEvent): Boolean { |
|
|
|
when (event.action) { |
|
|
|
when (event.action) { |
|
|
|
@ -172,7 +179,8 @@ class RadioOverlayService : Service() { |
|
|
|
touchY = event.rawY |
|
|
|
touchY = event.rawY |
|
|
|
dragging = false |
|
|
|
dragging = false |
|
|
|
dragAllowed = true |
|
|
|
dragAllowed = true |
|
|
|
beginTalk() |
|
|
|
view.removeCallbacks(talkBeginRunnable) |
|
|
|
|
|
|
|
view.postDelayed(talkBeginRunnable, PTT_HOLD_MS) |
|
|
|
return true |
|
|
|
return true |
|
|
|
} |
|
|
|
} |
|
|
|
MotionEvent.ACTION_MOVE -> { |
|
|
|
MotionEvent.ACTION_MOVE -> { |
|
|
|
@ -185,6 +193,8 @@ class RadioOverlayService : Service() { |
|
|
|
dragAllowed = false |
|
|
|
dragAllowed = false |
|
|
|
} else if (abs(event.rawX - touchX) > touchSlop || abs(event.rawY - touchY) > touchSlop) { |
|
|
|
} else if (abs(event.rawX - touchX) > touchSlop || abs(event.rawY - touchY) > touchSlop) { |
|
|
|
dragging = true |
|
|
|
dragging = true |
|
|
|
|
|
|
|
view.removeCallbacks(talkBeginRunnable) |
|
|
|
|
|
|
|
latched = false |
|
|
|
endTalkImmediate() |
|
|
|
endTalkImmediate() |
|
|
|
lp.x = startX + (event.rawX - touchX).toInt() |
|
|
|
lp.x = startX + (event.rawX - touchX).toInt() |
|
|
|
lp.y = startY + (event.rawY - touchY).toInt() |
|
|
|
lp.y = startY + (event.rawY - touchY).toInt() |
|
|
|
@ -193,8 +203,27 @@ class RadioOverlayService : Service() { |
|
|
|
} |
|
|
|
} |
|
|
|
return true |
|
|
|
return true |
|
|
|
} |
|
|
|
} |
|
|
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { |
|
|
|
MotionEvent.ACTION_UP -> { |
|
|
|
endTalk() |
|
|
|
view.removeCallbacks(talkBeginRunnable) |
|
|
|
|
|
|
|
val now = SystemClock.uptimeMillis() |
|
|
|
|
|
|
|
val isDouble = lastTapUpTime != 0L && (now - lastTapUpTime) <= doubleTapTimeout |
|
|
|
|
|
|
|
if (latched) { |
|
|
|
|
|
|
|
latched = false |
|
|
|
|
|
|
|
lastTapUpTime = 0L |
|
|
|
|
|
|
|
endTalk() |
|
|
|
|
|
|
|
} else if (isDouble) { |
|
|
|
|
|
|
|
latched = true |
|
|
|
|
|
|
|
lastTapUpTime = 0L |
|
|
|
|
|
|
|
beginTalk() |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
lastTapUpTime = now |
|
|
|
|
|
|
|
endTalk() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return true |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
MotionEvent.ACTION_CANCEL -> { |
|
|
|
|
|
|
|
view.removeCallbacks(talkBeginRunnable) |
|
|
|
|
|
|
|
if (!latched) endTalk() |
|
|
|
return true |
|
|
|
return true |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|