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