diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt index efaade0..7386163 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt +++ b/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() diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt index 5ab7627..7d95565 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt +++ b/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?) {