Browse Source

refactor: create MVC architecture foundation

- Add DetectionUIEvents and DetectionUICallbacks interfaces
- Create DetectionController for business logic separation
- Add FloatingOrbUI pure UI component with no business logic
- Establish clean event-driven communication pattern

Related todos: #24
refactor/mvc-architecture
Quildra 5 months ago
parent
commit
4894678953
  1. 135
      app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt
  2. 269
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingOrbUI.kt
  3. 60
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/interfaces/DetectionUIEvents.kt

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

@ -0,0 +1,135 @@
package com.quillstudios.pokegoalshelper.controllers
import android.util.Log
import com.quillstudios.pokegoalshelper.YOLOOnnxDetector
import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUIEvents
import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUICallbacks
import org.opencv.core.Mat
/**
* Controller handling detection business logic.
* Decouples UI interactions from YOLO detection implementation.
*/
class DetectionController(
private val yoloDetector: YOLOOnnxDetector
) : DetectionUIEvents {
companion object {
private const val TAG = "DetectionController"
}
private var uiCallbacks: DetectionUICallbacks? = null
private var currentSettings = DetectionSettings()
/**
* Register UI callbacks for status updates
*/
fun setUICallbacks(callbacks: DetectionUICallbacks) {
uiCallbacks = callbacks
}
/**
* Remove UI callbacks (cleanup)
*/
fun clearUICallbacks() {
uiCallbacks = null
}
// === DetectionUIEvents Implementation ===
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)
}
override fun onClassFilterChanged(className: String?) {
Log.i(TAG, "🔍 Class filter changed to: ${className ?: "ALL CLASSES"}")
currentSettings.classFilter = className
// Apply filter to YOLO detector
YOLOOnnxDetector.setClassFilter(className)
// Notify UI of settings change
uiCallbacks?.onSettingsChanged(
currentSettings.classFilter,
currentSettings.debugMode,
currentSettings.coordinateMode
)
}
override fun onDebugModeToggled() {
currentSettings.debugMode = !currentSettings.debugMode
Log.i(TAG, "📊 Debug mode toggled: ${currentSettings.debugMode}")
// Apply debug mode to YOLO detector
YOLOOnnxDetector.toggleShowAllConfidences()
// Notify UI of settings change
uiCallbacks?.onSettingsChanged(
currentSettings.classFilter,
currentSettings.debugMode,
currentSettings.coordinateMode
)
}
override fun onCoordinateModeChanged(mode: String) {
Log.i(TAG, "🔧 Coordinate mode changed to: $mode")
currentSettings.coordinateMode = mode
// Apply coordinate mode to YOLO detector
YOLOOnnxDetector.setCoordinateMode(mode)
// Notify UI of settings change
uiCallbacks?.onSettingsChanged(
currentSettings.classFilter,
currentSettings.debugMode,
currentSettings.coordinateMode
)
}
// === Business Logic Methods ===
/**
* Process detection on the given image
* This will be called by the service layer
*/
fun processDetection(inputMat: Mat): Int {
return try {
uiCallbacks?.onDetectionStarted()
val detections = yoloDetector.detect(inputMat)
val detectionCount = detections.size
Log.i(TAG, "✅ Detection completed: $detectionCount objects found")
uiCallbacks?.onDetectionCompleted(detectionCount)
detectionCount
} catch (e: Exception) {
Log.e(TAG, "❌ Detection failed", e)
uiCallbacks?.onDetectionFailed(e.message ?: "Unknown error")
0
}
}
/**
* Get current detection settings
*/
fun getCurrentSettings(): DetectionSettings {
return currentSettings.copy()
}
}
/**
* Data class representing current detection settings
*/
data class DetectionSettings(
var classFilter: String? = null,
var debugMode: Boolean = false,
var coordinateMode: String = "HYBRID"
)

269
app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingOrbUI.kt

@ -0,0 +1,269 @@
package com.quillstudios.pokegoalshelper.ui
import android.content.Context
import android.graphics.PixelFormat
import android.os.Build
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Button
import android.widget.LinearLayout
import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUIEvents
import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUICallbacks
/**
* Floating orb UI component that handles user interactions.
* Implements CalcIV-style expandable menu system.
*
* This is pure UI logic - no business logic or direct detector calls.
*/
class FloatingOrbUI(
private val context: Context,
private val detectionEvents: DetectionUIEvents
) : DetectionUICallbacks {
companion object {
private const val TAG = "FloatingOrbUI"
private const val ORB_SIZE = 120
private const val MENU_BUTTON_WIDTH = 160
private const val MENU_BUTTON_HEIGHT = 60
}
private var windowManager: WindowManager? = null
private var orbButton: View? = null
private var expandedMenu: View? = null
private var isMenuExpanded = false
private var isProcessing = false
/**
* Initialize and show the floating orb
*/
fun show() {
try {
if (orbButton != null) return // Already shown
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
createFloatingOrb()
Log.d(TAG, "✅ Floating orb UI shown")
} catch (e: Exception) {
Log.e(TAG, "❌ Error showing floating orb", e)
}
}
/**
* Hide and cleanup the floating orb
*/
fun hide() {
try {
if (isMenuExpanded) {
collapseMenu()
}
orbButton?.let {
windowManager?.removeView(it)
orbButton = null
}
windowManager = null
Log.d(TAG, "🗑️ Floating orb UI hidden")
} catch (e: Exception) {
Log.e(TAG, "❌ Error hiding floating orb", e)
}
}
// === DetectionUICallbacks Implementation ===
override fun onDetectionStarted() {
isProcessing = true
updateOrbAppearance()
// Auto-collapse menu during processing
if (isMenuExpanded) {
collapseMenu()
}
}
override fun onDetectionCompleted(detectionCount: Int) {
isProcessing = false
updateOrbAppearance()
Log.d(TAG, "🎯 Detection completed: $detectionCount objects")
}
override fun onDetectionFailed(error: String) {
isProcessing = false
updateOrbAppearance()
Log.e(TAG, "❌ Detection failed: $error")
}
override fun onSettingsChanged(filterClass: String?, debugMode: Boolean, coordinateMode: String) {
Log.d(TAG, "⚙️ Settings updated - Filter: $filterClass, Debug: $debugMode, Mode: $coordinateMode")
// UI could update visual indicators here if needed
}
// === Private UI Methods ===
private fun createFloatingOrb() {
orbButton = Button(context).apply {
text = "🎯"
textSize = 20f
setBackgroundResource(android.R.drawable.btn_default)
background.setTint(0xFF4CAF50.toInt()) // Green
setTextColor(0xFFFFFFFF.toInt())
width = ORB_SIZE
height = ORB_SIZE
layoutParams = ViewGroup.LayoutParams(ORB_SIZE, ORB_SIZE)
setOnClickListener { handleOrbClick() }
}
val params = WindowManager.LayoutParams(
ORB_SIZE, ORB_SIZE,
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(orbButton, params)
}
private fun handleOrbClick() {
if (isProcessing) {
Log.d(TAG, "⚠️ Ignoring click - detection in progress")
return
}
if (isMenuExpanded) {
collapseMenu()
} else {
expandMenu()
}
}
private fun expandMenu() {
if (isMenuExpanded || isProcessing) return
val menuContainer = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(0xE0000000.toInt()) // Semi-transparent black
setPadding(16, 16, 16, 16)
}
// Define menu options with their actions
val menuItems = listOf(
MenuOption("🔍 DETECT", 0xFF4CAF50.toInt()) {
detectionEvents.onDetectionRequested()
},
MenuOption("SHINY", 0xFFFFD700.toInt()) {
detectionEvents.onClassFilterChanged("shiny_icon")
detectionEvents.onDetectionRequested()
},
MenuOption("POKEBALL", 0xFFE91E63.toInt()) {
detectionEvents.onClassFilterChanged("ball_icon_cherishball")
detectionEvents.onDetectionRequested()
},
MenuOption("ALL", 0xFF607D8B.toInt()) {
detectionEvents.onClassFilterChanged(null)
detectionEvents.onDetectionRequested()
},
MenuOption("DEBUG", 0xFFFF5722.toInt()) {
detectionEvents.onDebugModeToggled()
detectionEvents.onDetectionRequested()
}
)
menuItems.forEach { option ->
val button = createMenuButton(option)
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
updateOrbAppearance()
}
private fun collapseMenu() {
if (!isMenuExpanded) return
expandedMenu?.let { windowManager?.removeView(it) }
expandedMenu = null
isMenuExpanded = false
updateOrbAppearance()
}
private fun createMenuButton(option: MenuOption): Button {
return Button(context).apply {
text = option.text
textSize = 12f
setBackgroundColor(option.color)
setTextColor(0xFFFFFFFF.toInt())
layoutParams = LinearLayout.LayoutParams(MENU_BUTTON_WIDTH, MENU_BUTTON_HEIGHT).apply {
setMargins(0, 0, 0, 8)
}
setOnClickListener {
option.action()
collapseMenu()
}
}
}
private fun updateOrbAppearance() {
(orbButton as? Button)?.apply {
when {
isProcessing -> {
text = ""
background.setTint(0xFFFF9800.toInt()) // Orange
}
isMenuExpanded -> {
text = ""
background.setTint(0xFFFF5722.toInt()) // Orange-red
}
else -> {
text = "🎯"
background.setTint(0xFF4CAF50.toInt()) // Green
}
}
}
}
/**
* Data class for menu options
*/
private data class MenuOption(
val text: String,
val color: Int,
val action: () -> Unit
)
}

60
app/src/main/java/com/quillstudios/pokegoalshelper/ui/interfaces/DetectionUIEvents.kt

@ -0,0 +1,60 @@
package com.quillstudios.pokegoalshelper.ui.interfaces
/**
* Interface for UI events related to detection functionality.
* UI components implement this to communicate with business logic controllers.
*/
interface DetectionUIEvents {
/**
* Triggered when user requests manual detection
*/
fun onDetectionRequested()
/**
* Triggered when user changes class filter
* @param className Name of class to filter, or null for all classes
*/
fun onClassFilterChanged(className: String?)
/**
* Triggered when user toggles debug mode
*/
fun onDebugModeToggled()
/**
* Triggered when user changes coordinate transformation mode
* @param mode Transformation mode (DIRECT, LETTERBOX, HYBRID)
*/
fun onCoordinateModeChanged(mode: String)
}
/**
* Interface for callbacks from business logic back to UI.
* UI components implement this to receive status updates.
*/
interface DetectionUICallbacks {
/**
* Called when detection starts processing
*/
fun onDetectionStarted()
/**
* Called when detection completes successfully
* @param detectionCount Number of objects detected
*/
fun onDetectionCompleted(detectionCount: Int)
/**
* Called when detection fails
* @param error Error message
*/
fun onDetectionFailed(error: String)
/**
* Called when settings change
* @param filterClass Current class filter (null if showing all)
* @param debugMode Current debug mode state
* @param coordinateMode Current coordinate transformation mode
*/
fun onSettingsChanged(filterClass: String?, debugMode: Boolean, coordinateMode: String)
}
Loading…
Cancel
Save