Browse Source

refactor: integrate MVC architecture into ScreenCaptureService

- Replace old UI code with FloatingOrbUI component
- Add DetectionController with callback system
- Implement convertImageToMat helper function
- Connect UI events to business logic through controller
- Remove direct YOLO detector calls from UI

Related todos: #24
refactor/mvc-architecture
Quildra 5 months ago
parent
commit
5551a758c5
  1. 224
      app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt
  2. 18
      app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt

224
app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt

@ -21,6 +21,8 @@ import android.view.Gravity
import android.widget.Button
import android.widget.LinearLayout
import androidx.core.app.NotificationCompat
import com.quillstudios.pokegoalshelper.controllers.DetectionController
import com.quillstudios.pokegoalshelper.ui.FloatingOrbUI
import org.opencv.android.Utils
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
@ -98,9 +100,9 @@ class ScreenCaptureService : Service() {
private var screenDensity = 0
private var detectionOverlay: DetectionOverlay? = null
// Floating button overlay
private var overlayButton: View? = null
private var windowManager: WindowManager? = null
// MVC Components
private lateinit var detectionController: DetectionController
private var floatingOrbUI: FloatingOrbUI? = null
private val handler = Handler(Looper.getMainLooper())
private var captureInterval = 2000L // Capture every 2 seconds
@ -148,6 +150,14 @@ class ScreenCaptureService : Service() {
} else {
Log.i(TAG, "✅ ONNX YOLO detector initialized for screen capture")
}
// Initialize MVC components
detectionController = DetectionController(yoloDetector!!)
floatingOrbUI = FloatingOrbUI(this, detectionController)
detectionController.setUICallbacks(floatingOrbUI!!)
detectionController.setDetectionRequestCallback { triggerManualDetection() }
Log.d(TAG, "✅ MVC architecture initialized")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -268,9 +278,9 @@ class ScreenCaptureService : Service() {
return
}
Log.d(TAG, "Screen capture setup complete, creating manual trigger button")
// Create floating detection button instead of auto-capture
createFloatingButton()
Log.d(TAG, "Screen capture setup complete, showing floating orb UI")
// Show the floating orb UI
floatingOrbUI?.show()
} catch (e: Exception) {
Log.e(TAG, "Error starting screen capture", e)
@ -283,7 +293,7 @@ class ScreenCaptureService : Service() {
handler.removeCallbacks(captureRunnable)
hideDetectionOverlay()
removeFloatingButton()
floatingOrbUI?.hide()
latestImage?.close()
latestImage = null
virtualDisplay?.release()
@ -1058,12 +1068,7 @@ class ScreenCaptureService : Service() {
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
// Create a container for multiple buttons
val buttonContainer = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(0x80000000.toInt()) // Semi-transparent black background
setPadding(8, 8, 8, 8)
}
createFloatingOrb()
// Main detect button
val detectButton = Button(this).apply {
@ -1196,9 +1201,137 @@ class ScreenCaptureService : Service() {
Log.e(TAG, "❌ Error creating floating button", e)
}
}
private fun createFloatingOrb() {
// Create the main floating orb button
val orbButton = Button(this).apply {
text = "🎯"
textSize = 20f
setBackgroundResource(android.R.drawable.btn_default)
background.setTint(0xFF4CAF50.toInt()) // Green
setTextColor(0xFFFFFFFF.toInt())
// Make it circular
width = 120
height = 120
layoutParams = ViewGroup.LayoutParams(120, 120)
setOnClickListener {
if (isMenuExpanded) {
collapseMenu()
} else {
expandMenu()
}
}
}
overlayButton = orbButton
val params = WindowManager.LayoutParams(
120, 120,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
@Suppress("DEPRECATION")
WindowManager.LayoutParams.TYPE_PHONE
},
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP or Gravity.START
x = 50
y = 200
}
windowManager?.addView(overlayButton, params)
}
private fun expandMenu() {
if (isMenuExpanded) return
// Create the expanded menu
val menuContainer = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(0xE0000000.toInt()) // Semi-transparent black
setPadding(16, 16, 16, 16)
}
// Add menu buttons
val buttons = listOf(
Triple("🔍 DETECT", 0xFF4CAF50.toInt()) { triggerManualDetection() },
Triple("SHINY", 0xFFFFD700.toInt()) { YOLOOnnxDetector.setClassFilter("shiny_icon"); triggerManualDetection() },
Triple("POKEBALL", 0xFFE91E63.toInt()) { YOLOOnnxDetector.setClassFilter("ball_icon_cherishball"); triggerManualDetection() },
Triple("ALL", 0xFF607D8B.toInt()) { YOLOOnnxDetector.setClassFilter(null); triggerManualDetection() },
Triple("DEBUG", 0xFFFF5722.toInt()) { YOLOOnnxDetector.toggleShowAllConfidences(); triggerManualDetection() }
)
buttons.forEach { (text, color, action) ->
val button = Button(this).apply {
this.text = text
textSize = 12f
setBackgroundColor(color)
setTextColor(0xFFFFFFFF.toInt())
layoutParams = LinearLayout.LayoutParams(160, 60).apply {
setMargins(0, 0, 0, 8)
}
setOnClickListener {
action()
collapseMenu()
}
}
menuContainer.addView(button)
}
expandedMenu = menuContainer
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
@Suppress("DEPRECATION")
WindowManager.LayoutParams.TYPE_PHONE
},
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP or Gravity.START
x = 180 // Position next to the orb
y = 200
}
windowManager?.addView(expandedMenu, params)
isMenuExpanded = true
// Change orb appearance
(overlayButton as? Button)?.apply {
text = ""
background.setTint(0xFFFF5722.toInt()) // Orange-red
}
}
private fun collapseMenu() {
if (!isMenuExpanded) return
expandedMenu?.let { windowManager?.removeView(it) }
expandedMenu = null
isMenuExpanded = false
// Reset orb appearance
(overlayButton as? Button)?.apply {
text = "🎯"
background.setTint(0xFF4CAF50.toInt()) // Green
}
}
private fun removeFloatingButton() {
try {
// Collapse menu first if expanded
if (isMenuExpanded) {
collapseMenu()
}
overlayButton?.let { button ->
windowManager?.removeView(button)
overlayButton = null
@ -1210,35 +1343,63 @@ class ScreenCaptureService : Service() {
}
}
private fun convertImageToMat(image: Image): Mat? {
return try {
val planes = image.planes
val buffer = planes[0].buffer
val pixelStride = planes[0].pixelStride
val rowStride = planes[0].rowStride
val rowPadding = rowStride - pixelStride * screenWidth
// Create bitmap from image
val bitmap = Bitmap.createBitmap(
screenWidth + rowPadding / pixelStride,
screenHeight,
Bitmap.Config.ARGB_8888
)
bitmap.copyPixelsFromBuffer(buffer)
// Convert bitmap to Mat
val mat = Mat()
Utils.bitmapToMat(bitmap, mat)
// Convert from RGBA to RGB (YOLO expects RGB)
val rgbMat = Mat()
Imgproc.cvtColor(mat, rgbMat, Imgproc.COLOR_RGBA2RGB)
// Clean up
mat.release()
bitmap.recycle()
rgbMat
} catch (e: Exception) {
Log.e(TAG, "❌ Error converting image to Mat", e)
null
}
}
private fun triggerManualDetection() {
Log.d(TAG, "🔍 Manual detection triggered!")
Log.d(TAG, "🔍 Manual detection triggered via MVC!")
latestImage?.let { image ->
try {
// Update main button to show processing (find the first button in the LinearLayout)
val mainButton = (overlayButton as? LinearLayout)?.getChildAt(0) as? Button
mainButton?.text = "⏳ PROCESSING..."
mainButton?.isEnabled = false
// Convert image to Mat for processing
val mat = convertImageToMat(image)
// Process the image
processImage(image)
if (mat != null) {
// Use controller to process detection (this will notify UI via callbacks)
detectionController.processDetection(mat)
mat.release()
} else {
Log.e(TAG, "❌ Failed to convert image to Mat")
}
// Close the image after processing to free the buffer
image.close()
latestImage = null
// Reset button after processing
handler.postDelayed({
val resetButton = (overlayButton as? LinearLayout)?.getChildAt(0) as? Button
resetButton?.text = "🔍 DETECT"
resetButton?.isEnabled = true
}, 2000)
} catch (e: Exception) {
Log.e(TAG, "❌ Error in manual detection", e)
val errorButton = (overlayButton as? LinearLayout)?.getChildAt(0) as? Button
errorButton?.text = "🔍 DETECT"
errorButton?.isEnabled = true
}
} ?: run {
Log.w(TAG, "⚠️ No image available for detection")
@ -1248,7 +1409,8 @@ class ScreenCaptureService : Service() {
override fun onDestroy() {
super.onDestroy()
hideDetectionOverlay()
removeFloatingButton()
floatingOrbUI?.hide()
detectionController.clearUICallbacks()
yoloDetector?.release()
ocrExecutor.shutdown()
stopScreenCapture()

18
app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt

@ -39,11 +39,19 @@ class DetectionController(
override fun onDetectionRequested() {
Log.d(TAG, "🔍 Detection requested via controller")
uiCallbacks?.onDetectionStarted()
// Business logic will be handled by the service layer
// For now, just notify completion
uiCallbacks?.onDetectionCompleted(0)
// The actual detection will be triggered by the service layer
// This event will be handled by the service which has access to the image
detectionRequestCallback?.invoke()
}
private var detectionRequestCallback: (() -> Unit)? = null
/**
* Set callback for when UI requests detection
* This allows the service layer to handle the actual detection
*/
fun setDetectionRequestCallback(callback: () -> Unit) {
detectionRequestCallback = callback
}
override fun onClassFilterChanged(className: String?) {

Loading…
Cancel
Save