diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt index fff0f3a..0600d80 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt +++ b/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt @@ -24,6 +24,8 @@ import android.widget.LinearLayout import androidx.core.app.NotificationCompat import com.quillstudios.pokegoalshelper.controllers.DetectionController import com.quillstudios.pokegoalshelper.ui.EnhancedFloatingFAB +import com.quillstudios.pokegoalshelper.capture.ScreenCaptureManager +import com.quillstudios.pokegoalshelper.capture.ScreenCaptureManagerImpl import org.opencv.android.Utils import org.opencv.core.* import org.opencv.imgproc.Imgproc @@ -104,13 +106,7 @@ class ScreenCaptureService : Service() { // ONNX YOLO detector instance private var yoloDetector: YOLOOnnxDetector? = null - private var mediaProjection: MediaProjection? = null - private var virtualDisplay: VirtualDisplay? = null - private var imageReader: ImageReader? = null - private var mediaProjectionManager: MediaProjectionManager? = null - private var screenWidth = 0 - private var screenHeight = 0 - private var screenDensity = 0 + private lateinit var screenCaptureManager: ScreenCaptureManager private var detectionOverlay: DetectionOverlay? = null // MVC Components @@ -135,26 +131,14 @@ class ScreenCaptureService : Service() { } } - private val mediaProjectionCallback = object : MediaProjection.Callback() { - override fun onStop() { - Log.d(TAG, "MediaProjection stopped") - stopScreenCapture() - } - - override fun onCapturedContentResize(width: Int, height: Int) { - Log.d(TAG, "Screen size changed: ${width}x${height}") - } - - override fun onCapturedContentVisibilityChanged(isVisible: Boolean) { - Log.d(TAG, "Content visibility changed: $isVisible") - } - } override fun onCreate() { super.onCreate() createNotificationChannel() - getScreenMetrics() - mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + + // Initialize screen capture manager + screenCaptureManager = ScreenCaptureManagerImpl(this, handler) + screenCaptureManager.setImageCallback { image -> handleCapturedImage(image) } // Initialize ONNX YOLO detector yoloDetector = YOLOOnnxDetector(this) @@ -236,22 +220,6 @@ class ScreenCaptureService : Service() { } } - private fun getScreenMetrics() { - val displayMetrics = DisplayMetrics() - val windowManager = getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager - windowManager.defaultDisplay.getMetrics(displayMetrics) - - screenWidth = displayMetrics.widthPixels - screenHeight = displayMetrics.heightPixels - screenDensity = displayMetrics.densityDpi - - Log.d(TAG, "Screen metrics: ${screenWidth}x${screenHeight}, density: $screenDensity") - - // Get status bar height for coordinate adjustment - val statusBarHeight = getStatusBarHeight() - Log.d(TAG, "Status bar height: ${statusBarHeight}px") - } - private fun getStatusBarHeight(): Int { var result = 0 val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") @@ -287,36 +255,9 @@ class ScreenCaptureService : Service() { Log.d(TAG, "Starting foreground service") startForeground(NOTIFICATION_ID, notification) - Log.d(TAG, "Getting MediaProjection") - mediaProjection = mediaProjectionManager?.getMediaProjection(Activity.RESULT_OK, resultData) - - if (mediaProjection == null) { - Log.e(TAG, "Failed to get MediaProjection") - stopSelf() - return - } - - Log.d(TAG, "Registering MediaProjection callback") - mediaProjection?.registerCallback(mediaProjectionCallback, handler) - - Log.d(TAG, "Creating ImageReader: ${screenWidth}x${screenHeight}") - imageReader = ImageReader.newInstance(screenWidth, screenHeight, PixelFormat.RGBA_8888, 3) // Increased buffer count - imageReader?.setOnImageAvailableListener(onImageAvailableListener, handler) - - Log.d(TAG, "Creating VirtualDisplay") - virtualDisplay = mediaProjection?.createVirtualDisplay( - "ScreenCapture", - screenWidth, - screenHeight, - screenDensity, - DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, - imageReader?.surface, - null, - null - ) - - if (virtualDisplay == null) { - Log.e(TAG, "Failed to create VirtualDisplay") + // Use the screen capture manager to start capture + if (!screenCaptureManager.startCapture(resultData)) { + Log.e(TAG, "Failed to start screen capture via manager") stopSelf() return } @@ -339,35 +280,30 @@ class ScreenCaptureService : Service() { enhancedFloatingFAB?.hide() latestImage?.close() latestImage = null - virtualDisplay?.release() - imageReader?.close() - mediaProjection?.unregisterCallback(mediaProjectionCallback) - mediaProjection?.stop() - - virtualDisplay = null - imageReader = null - mediaProjection = null + + // Use the screen capture manager to stop capture + screenCaptureManager.stopCapture() stopForeground(true) stopSelf() } - private val onImageAvailableListener = ImageReader.OnImageAvailableListener { reader -> + /** + * Handle captured images from the ScreenCaptureManager + */ + private fun handleCapturedImage(image: Image) { try { - val image = reader.acquireLatestImage() - if (image != null) { - if (autoProcessing) { - processImage(image) - image.close() - } else { - // Store the latest image for manual processing - latestImage?.close() // Release previous image - latestImage = image - // Don't close the image yet - it will be closed in triggerManualDetection - } + if (autoProcessing) { + processImage(image) + image.close() + } else { + // Store the latest image for manual processing + latestImage?.close() // Release previous image + latestImage = image + // Don't close the image yet - it will be closed in triggerManualDetection } } catch (e: Exception) { - Log.e(TAG, "Error in onImageAvailableListener", e) + Log.e(TAG, "Error handling captured image", e) } } @@ -384,6 +320,14 @@ class ScreenCaptureService : Service() { var croppedBitmap: Bitmap? = null try { + // Get screen dimensions from the manager + val screenDimensions = screenCaptureManager.getScreenDimensions() + if (screenDimensions == null) { + Log.e(TAG, "Screen dimensions not available from manager") + return + } + val (screenWidth, screenHeight) = screenDimensions + val planes = image.planes val buffer = planes[0].buffer val pixelStride = planes[0].pixelStride @@ -1161,6 +1105,14 @@ class ScreenCaptureService : Service() { var mat: Mat? = null return try { + // Get screen dimensions from the manager + val screenDimensions = screenCaptureManager.getScreenDimensions() + if (screenDimensions == null) { + Log.e(TAG, "Screen dimensions not available from manager") + return null + } + val (screenWidth, screenHeight) = screenDimensions + val planes = image.planes val buffer = planes[0].buffer val pixelStride = planes[0].pixelStride @@ -1254,6 +1206,12 @@ class ScreenCaptureService : Service() { enhancedFloatingFAB?.hide() detectionController.clearUICallbacks() yoloDetector?.release() + + // Release screen capture manager + if (::screenCaptureManager.isInitialized) { + screenCaptureManager.release() + } + // Proper executor shutdown with timeout ocrExecutor.shutdown() try { diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/capture/ScreenCaptureManager.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/capture/ScreenCaptureManager.kt new file mode 100644 index 0000000..f6c4aec --- /dev/null +++ b/app/src/main/java/com/quillstudios/pokegoalshelper/capture/ScreenCaptureManager.kt @@ -0,0 +1,46 @@ +package com.quillstudios.pokegoalshelper.capture + +import android.content.Intent +import android.media.Image + +/** + * Interface for managing screen capture functionality. + * Separates screen capture concerns from the main service. + */ +interface ScreenCaptureManager { + + /** + * Start screen capture with the provided MediaProjection result data. + * @param resultData The Intent result from MediaProjection permission request + * @return true if capture started successfully, false otherwise + */ + fun startCapture(resultData: Intent): Boolean + + /** + * Stop screen capture and clean up resources. + */ + fun stopCapture() + + /** + * Set callback to receive captured images. + * @param callback Function to handle captured images + */ + fun setImageCallback(callback: (Image) -> Unit) + + /** + * Check if capture is currently active. + * @return true if capturing, false otherwise + */ + fun isCapturing(): Boolean + + /** + * Get the current screen dimensions. + * @return Pair of (width, height) or null if not initialized + */ + fun getScreenDimensions(): Pair? + + /** + * Clean up all resources. Should be called when manager is no longer needed. + */ + fun release() +} \ No newline at end of file diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/capture/ScreenCaptureManagerImpl.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/capture/ScreenCaptureManagerImpl.kt new file mode 100644 index 0000000..d5570c3 --- /dev/null +++ b/app/src/main/java/com/quillstudios/pokegoalshelper/capture/ScreenCaptureManagerImpl.kt @@ -0,0 +1,186 @@ +package com.quillstudios.pokegoalshelper.capture + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.PixelFormat +import android.hardware.display.DisplayManager +import android.hardware.display.VirtualDisplay +import android.media.Image +import android.media.ImageReader +import android.media.projection.MediaProjection +import android.media.projection.MediaProjectionManager +import android.os.Handler +import android.util.DisplayMetrics +import android.util.Log +import android.view.WindowManager + +/** + * Implementation of ScreenCaptureManager that handles MediaProjection-based screen capture. + * Extracted from ScreenCaptureService for better separation of concerns. + */ +class ScreenCaptureManagerImpl( + private val context: Context, + private val handler: Handler +) : ScreenCaptureManager { + + companion object { + private const val TAG = "ScreenCaptureManager" + private const val BUFFER_COUNT = 3 + } + + private var mediaProjectionManager: MediaProjectionManager? = null + private var mediaProjection: MediaProjection? = null + private var virtualDisplay: VirtualDisplay? = null + private var imageReader: ImageReader? = null + + private var screenWidth = 0 + private var screenHeight = 0 + private var screenDensity = 0 + + private var imageCallback: ((Image) -> Unit)? = null + private var isActive = false + + init { + mediaProjectionManager = context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + initializeScreenMetrics() + } + + private fun initializeScreenMetrics() { + val displayMetrics = DisplayMetrics() + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + windowManager.defaultDisplay.getMetrics(displayMetrics) + + screenWidth = displayMetrics.widthPixels + screenHeight = displayMetrics.heightPixels + screenDensity = displayMetrics.densityDpi + + Log.d(TAG, "Screen metrics initialized: ${screenWidth}x${screenHeight}, density: $screenDensity") + } + + private val mediaProjectionCallback = object : MediaProjection.Callback() { + override fun onStop() { + Log.d(TAG, "MediaProjection stopped") + stopCapture() + } + + override fun onCapturedContentResize(width: Int, height: Int) { + Log.d(TAG, "Screen size changed: ${width}x${height}") + } + + override fun onCapturedContentVisibilityChanged(isVisible: Boolean) { + Log.d(TAG, "Content visibility changed: $isVisible") + } + } + + private val onImageAvailableListener = ImageReader.OnImageAvailableListener { reader -> + try { + val image = reader.acquireLatestImage() + if (image != null) { + imageCallback?.invoke(image) + } + } catch (e: Exception) { + Log.e(TAG, "Error in onImageAvailableListener", e) + } + } + + override fun startCapture(resultData: Intent): Boolean { + if (isActive) { + Log.w(TAG, "Capture already active") + return true + } + + try { + Log.d(TAG, "Starting screen capture") + + mediaProjection = mediaProjectionManager?.getMediaProjection(Activity.RESULT_OK, resultData) + if (mediaProjection == null) { + Log.e(TAG, "Failed to get MediaProjection") + return false + } + + Log.d(TAG, "Registering MediaProjection callback") + mediaProjection?.registerCallback(mediaProjectionCallback, handler) + + Log.d(TAG, "Creating ImageReader: ${screenWidth}x${screenHeight}") + imageReader = ImageReader.newInstance(screenWidth, screenHeight, PixelFormat.RGBA_8888, BUFFER_COUNT) + imageReader?.setOnImageAvailableListener(onImageAvailableListener, handler) + + Log.d(TAG, "Creating VirtualDisplay") + virtualDisplay = mediaProjection?.createVirtualDisplay( + "ScreenCapture", + screenWidth, + screenHeight, + screenDensity, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, + imageReader?.surface, + null, + null + ) + + if (virtualDisplay == null) { + Log.e(TAG, "Failed to create VirtualDisplay") + cleanupResources() + return false + } + + isActive = true + Log.d(TAG, "Screen capture started successfully") + return true + + } catch (e: Exception) { + Log.e(TAG, "Error starting screen capture", e) + cleanupResources() + return false + } + } + + override fun stopCapture() { + if (!isActive) { + Log.d(TAG, "Capture not active, nothing to stop") + return + } + + Log.d(TAG, "Stopping screen capture") + cleanupResources() + isActive = false + Log.d(TAG, "Screen capture stopped") + } + + private fun cleanupResources() { + try { + virtualDisplay?.release() + imageReader?.close() + mediaProjection?.unregisterCallback(mediaProjectionCallback) + mediaProjection?.stop() + } catch (e: Exception) { + Log.e(TAG, "Error during cleanup", e) + } finally { + virtualDisplay = null + imageReader = null + mediaProjection = null + } + } + + override fun setImageCallback(callback: (Image) -> Unit) { + this.imageCallback = callback + Log.d(TAG, "Image callback set") + } + + override fun isCapturing(): Boolean = isActive + + override fun getScreenDimensions(): Pair? { + return if (screenWidth > 0 && screenHeight > 0) { + Pair(screenWidth, screenHeight) + } else { + null + } + } + + override fun release() { + Log.d(TAG, "Releasing ScreenCaptureManager") + stopCapture() + imageCallback = null + mediaProjectionManager = null + } +} \ No newline at end of file