Browse Source
Created ScreenCaptureManager interface and ScreenCaptureManagerImpl to separate screen capture concerns from ScreenCaptureService, improving architecture: ## New Files: - ScreenCaptureManager.kt: Interface defining screen capture operations - ScreenCaptureManagerImpl.kt: Implementation handling MediaProjection lifecycle ## Changes to ScreenCaptureService.kt: - Remove direct MediaProjection/ImageReader/VirtualDisplay handling - Use ScreenCaptureManager via dependency injection pattern - Delegate screen capture operations to manager - Get screen dimensions from manager instead of storing locally - Proper manager lifecycle with release() in onDestroy() ## Benefits: - Single Responsibility Principle: Service focuses on orchestration - Testability: Screen capture logic can be unit tested in isolation - Maintainability: Clear separation of concerns - Reusability: Manager can be used by other components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>arch-001-screen-capture-manager
3 changed files with 280 additions and 90 deletions
@ -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<Int, Int>? |
|||
|
|||
/** |
|||
* Clean up all resources. Should be called when manager is no longer needed. |
|||
*/ |
|||
fun release() |
|||
} |
|||
@ -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<Int, Int>? { |
|||
return if (screenWidth > 0 && screenHeight > 0) { |
|||
Pair(screenWidth, screenHeight) |
|||
} else { |
|||
null |
|||
} |
|||
} |
|||
|
|||
override fun release() { |
|||
Log.d(TAG, "Releasing ScreenCaptureManager") |
|||
stopCapture() |
|||
imageCallback = null |
|||
mediaProjectionManager = null |
|||
} |
|||
} |
|||
Loading…
Reference in new issue