Compare commits

...

7 Commits

Author SHA1 Message Date
Dan 9e55ffa944 - Temp checkpoint 5 months ago
Dan 3ed31a0d81 fix: correct BGR to RGB color channel ordering in long screenshots 5 months ago
Dan e25eadc566 fix: resolve bitmap recycling issue in long screenshot capture 5 months ago
Dan e4ecfa6997 fix: resolve MediaProjection VirtualDisplay conflict in LongScreenshotCapture 5 months ago
Dan 17f0a1f202 fix: implement lazy initialization for LongScreenshotCapture 5 months ago
Dan 2d98941f63 fix: resolve LongScreenshotCapture initialization issue 5 months ago
Dan 39635fedff feat: implement PGH-30 LongScreenshotCapture core system 5 months ago
  1. 246
      app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt
  2. 548
      app/src/main/java/com/quillstudios/pokegoalshelper/capture/LongScreenshotCapture.kt
  3. 12
      app/src/main/java/com/quillstudios/pokegoalshelper/capture/ScreenCaptureManager.kt
  4. 10
      app/src/main/java/com/quillstudios/pokegoalshelper/capture/ScreenCaptureManagerImpl.kt
  5. 47
      app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt
  6. 574
      app/src/main/java/com/quillstudios/pokegoalshelper/stitching/ImageStitcher.kt
  7. 132
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/EnhancedFloatingFAB.kt
  8. 79
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/interfaces/DetectionUIEvents.kt

246
app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt

@ -28,6 +28,7 @@ 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 com.quillstudios.pokegoalshelper.capture.LongScreenshotCapture
import com.quillstudios.pokegoalshelper.ml.MLInferenceEngine
import com.quillstudios.pokegoalshelper.ml.YOLOInferenceEngine
import com.quillstudios.pokegoalshelper.ml.Detection as MLDetection
@ -139,6 +140,9 @@ class ScreenCaptureService : Service() {
private lateinit var detectionController: DetectionController
private var enhancedFloatingFAB: EnhancedFloatingFAB? = null
// Long Screenshot System
private var longScreenshotCapture: LongScreenshotCapture? = null
private val handler = Handler(Looper.getMainLooper())
private var captureInterval = DEFAULT_CAPTURE_INTERVAL_MS
private var autoProcessing = false // Disable automatic processing
@ -187,13 +191,25 @@ class ScreenCaptureService : Service() {
// Initialize MVC components
detectionController = DetectionController(mlInferenceEngine!!)
detectionController.setDetectionRequestCallback { triggerManualDetection() }
detectionController.setLongScreenshotCallbacks(
startCallback = { startLongScreenshot() },
captureCallback = { captureLongScreenshot() },
finishCallback = { finishLongScreenshot() },
finishAndStitchCallback = { finishLongScreenshotAndStitch() },
cancelCallback = { cancelLongScreenshot() }
)
// Initialize enhanced floating FAB
enhancedFloatingFAB = EnhancedFloatingFAB(
context = this,
onDetectionRequested = { triggerDetection() },
onToggleOverlay = { toggleOverlay() },
onReturnToApp = { returnToMainApp() }
onReturnToApp = { returnToMainApp() },
onLongScreenshotStart = { detectionController.onLongScreenshotStart() },
onLongScreenshotCapture = { detectionController.onLongScreenshotCapture() },
onLongScreenshotFinish = { detectionController.onLongScreenshotFinish() },
onLongScreenshotFinishAndStitch = { detectionController.onLongScreenshotFinishAndStitch() },
onLongScreenshotCancel = { detectionController.onLongScreenshotCancel() }
)
PGHLog.d(TAG, "✅ MVC architecture initialized")
@ -319,6 +335,9 @@ class ScreenCaptureService : Service() {
return
}
// Long screenshot system will be initialized lazily when first used
PGHLog.d(TAG, "📸 Long screenshot system prepared for lazy initialization")
PGHLog.d(TAG, "Screen capture setup complete")
// Show floating overlay
enhancedFloatingFAB?.show()
@ -1203,6 +1222,228 @@ class ScreenCaptureService : Service() {
}
}
// === Long Screenshot Event Handlers ===
private fun startLongScreenshot()
{
try
{
PGHLog.i(TAG, "🚀 Starting long screenshot collection")
// Try to initialize if not already done
if (longScreenshotCapture == null) {
PGHLog.i(TAG, "🔧 Long screenshot not initialized, attempting to initialize now...")
initializeLongScreenshotSystem()
}
longScreenshotCapture?.let { capture ->
// Set up callbacks
capture.setProgressCallback { count ->
handler.post {
enhancedFloatingFAB?.updateLongScreenshotProgress(count)
PGHLog.i(TAG, "📸 Long screenshot progress: $count screenshots")
}
}
capture.setErrorCallback { error ->
handler.post {
PGHLog.e(TAG, "❌ Long screenshot error: $error")
enhancedFloatingFAB?.exitLongScreenshotModeExternal()
}
}
// Start collection
val started = capture.startCollection()
if (!started)
{
PGHLog.e(TAG, "❌ Failed to start long screenshot collection")
enhancedFloatingFAB?.exitLongScreenshotModeExternal()
} else {
PGHLog.i(TAG, "✅ Long screenshot collection started successfully")
}
} ?: run {
PGHLog.e(TAG, "❌ Long screenshot system could not be initialized")
enhancedFloatingFAB?.exitLongScreenshotModeExternal()
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error starting long screenshot", e)
enhancedFloatingFAB?.exitLongScreenshotModeExternal()
}
}
private fun initializeLongScreenshotSystem()
{
try
{
PGHLog.d(TAG, "🔧 Initializing long screenshot system...")
val screenDimensions = screenCaptureManager.getScreenDimensions()
val mediaProjection = screenCaptureManager.getMediaProjection()
val screenDensity = screenCaptureManager.getScreenDensity()
PGHLog.d(TAG, "🔍 Long screenshot init check - dimensions=$screenDimensions, mediaProjection=$mediaProjection, density=$screenDensity")
if (screenDimensions != null && mediaProjection != null) {
val (screenWidth, screenHeight) = screenDimensions
PGHLog.d(TAG, "🔧 Creating LongScreenshotCapture instance...")
longScreenshotCapture = LongScreenshotCapture(this, handler)
// Initialize with the shared MediaProjection
PGHLog.d(TAG, "🔧 Initializing with MediaProjection: ${screenWidth}x${screenHeight}, density=$screenDensity")
val initialized = longScreenshotCapture!!.initialize(
mediaProjection, screenWidth, screenHeight, screenDensity
)
if (initialized) {
PGHLog.i(TAG, "✅ Long screenshot system initialized successfully")
} else {
PGHLog.e(TAG, "❌ Failed to initialize long screenshot system")
longScreenshotCapture = null
}
} else {
PGHLog.w(TAG, "⚠️ Cannot initialize long screenshot - missing MediaProjection ($mediaProjection) or screen dimensions ($screenDimensions)")
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error initializing long screenshot system", e)
longScreenshotCapture = null
}
}
private fun captureLongScreenshot()
{
try
{
PGHLog.d(TAG, "📸 Capturing long screenshot frame")
// Get the current image from the existing screen capture system
latestImage?.let { image ->
// Convert the image to Mat first, then to Bitmap
val mat = convertImageToMat(image)
if (mat != null) {
// Convert BGR Mat to RGB for proper color channels
val rgbMat = Mat()
Imgproc.cvtColor(mat, rgbMat, Imgproc.COLOR_BGR2RGB)
// Convert Mat to Bitmap
val bitmap = Bitmap.createBitmap(rgbMat.cols(), rgbMat.rows(), Bitmap.Config.ARGB_8888)
Utils.matToBitmap(rgbMat, bitmap)
// Create a copy of the bitmap for long screenshot processing
val bitmapCopy = bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, false)
// Pass the bitmap copy to the long screenshot system
val captured = longScreenshotCapture?.captureFrame(bitmapCopy) ?: false
if (!captured) {
PGHLog.w(TAG, "⚠️ Failed to capture long screenshot frame")
// Clean up the copy if capture failed
bitmapCopy.recycle()
}
// Clean up original resources
bitmap.recycle()
mat.release()
rgbMat.release()
} else {
PGHLog.w(TAG, "⚠️ Failed to convert image to Mat for long screenshot")
}
} ?: run {
PGHLog.w(TAG, "⚠️ No image available for long screenshot capture")
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error capturing long screenshot frame", e)
}
}
private fun finishLongScreenshot()
{
try
{
PGHLog.i(TAG, "🏁 Finishing long screenshot collection")
val screenshots = longScreenshotCapture?.finishCollection() ?: emptyList()
PGHLog.i(TAG, "✅ Long screenshot collection finished with ${screenshots.size} screenshots")
// Log the result without stitching
screenshots.forEach { screenshot ->
PGHLog.i(TAG, "📄 Screenshot: ${screenshot.filename} (${screenshot.width}x${screenshot.height})")
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error finishing long screenshot", e)
}
}
private fun finishLongScreenshotAndStitch()
{
try
{
PGHLog.i(TAG, "🧩 Finishing long screenshot collection with stitching")
// Set up stitching callbacks
setupStitchingCallbacks()
// Start the finish and stitch process
longScreenshotCapture?.finishCollectionAndStitch()
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error starting stitching process", e)
}
}
private fun setupStitchingCallbacks()
{
longScreenshotCapture?.setStitchingProgressCallback { progress, message ->
PGHLog.i(TAG, "🧩 Stitching progress: ${(progress * 100).toInt()}% - $message")
// TODO: Update UI with progress
}
longScreenshotCapture?.setStitchingCompleteCallback { result ->
if (result.success)
{
PGHLog.i(TAG, "✅ Stitching completed successfully!")
PGHLog.i(TAG, "📄 Output: ${result.outputPath}")
PGHLog.i(TAG, "📊 Quality: ${result.qualityScore}")
PGHLog.i(TAG, "📐 Dimensions: ${result.finalDimensions}")
// TODO: Notify user of success, maybe show result in gallery or share
}
else
{
PGHLog.e(TAG, "❌ Stitching failed: ${result.errorMessage}")
// TODO: Notify user of failure
}
}
}
private fun cancelLongScreenshot()
{
try
{
PGHLog.i(TAG, "❌ Canceling long screenshot collection")
longScreenshotCapture?.cancelCollection()
PGHLog.i(TAG, "✅ Long screenshot collection canceled")
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error canceling long screenshot", e)
}
}
override fun onDestroy() {
super.onDestroy()
@ -1218,6 +1459,9 @@ class ScreenCaptureService : Service() {
screenCaptureManager.release()
}
// Clean up long screenshot system
longScreenshotCapture?.cleanup()
// Proper executor shutdown with timeout
ocrExecutor.shutdown()
try {

548
app/src/main/java/com/quillstudios/pokegoalshelper/capture/LongScreenshotCapture.kt

@ -0,0 +1,548 @@
package com.quillstudios.pokegoalshelper.capture
import android.content.Context
import android.graphics.Bitmap
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.os.Handler
import android.os.Looper
import com.quillstudios.pokegoalshelper.utils.PGHLog
import com.quillstudios.pokegoalshelper.stitching.ImageStitcher
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
/**
* Long screenshot capture system that works alongside existing MediaProjection setup.
*
* Features:
* - Independent capture system using shared MediaProjection
* - Manual screenshot collection management
* - Thread-safe screenshot queue
* - Proper memory management and cleanup
* - Error handling and recovery
* - Image storage and retrieval
*/
class LongScreenshotCapture(
private val context: Context,
private val handler: Handler = Handler(Looper.getMainLooper())
)
{
companion object
{
private const val TAG = "LongScreenshotCapture"
private const val BUFFER_COUNT = 2
private const val CAPTURE_TIMEOUT_MS = 5000L
private const val MAX_SCREENSHOTS = 50
}
// Core components
private var mediaProjection: MediaProjection? = null
// Screen dimensions
private var screenWidth = 0
private var screenHeight = 0
private var screenDensity = 0
// State management
private val isInitialized = AtomicBoolean(false)
private val isCapturing = AtomicBoolean(false)
private val screenshotCount = AtomicInteger(0)
// Screenshot collection
private val capturedScreenshots = ConcurrentLinkedQueue<CapturedScreenshot>()
private val captureScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Image stitching
private val imageStitcher: ImageStitcher by lazy { ImageStitcher(context) }
// Storage
private val storageDir: File by lazy {
File(context.filesDir, "long_screenshots").apply {
if (!exists()) mkdirs()
}
}
// Callbacks
private var progressCallback: ((count: Int) -> Unit)? = null
private var errorCallback: ((error: String) -> Unit)? = null
private var stitchingProgressCallback: ((progress: Float, message: String) -> Unit)? = null
private var stitchingCompleteCallback: ((result: ImageStitcher.StitchingResult) -> Unit)? = null
// Configuration
private var keepIndividualScreenshots = true // Keep originals when stitching
private var debugMode = false // Debug mode for development/testing
/**
* Initialize the long screenshot system (lightweight - no VirtualDisplay needed)
*/
fun initialize(mediaProjection: MediaProjection, screenWidth: Int, screenHeight: Int, screenDensity: Int): Boolean
{
if (isInitialized.get())
{
PGHLog.w(TAG, "Long screenshot capture already initialized")
return true
}
try
{
PGHLog.i(TAG, "🔧 Initializing long screenshot capture system")
this.mediaProjection = mediaProjection
this.screenWidth = screenWidth
this.screenHeight = screenHeight
this.screenDensity = screenDensity
// Note: We don't create our own VirtualDisplay/ImageReader since Android doesn't allow
// multiple VirtualDisplays from the same MediaProjection. Instead, we'll capture
// screenshots by requesting them from the existing screen capture system.
isInitialized.set(true)
PGHLog.i(TAG, "✅ Long screenshot capture system initialized successfully (lightweight mode)")
return true
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error initializing long screenshot capture", e)
cleanup()
return false
}
}
/**
* Start long screenshot collection session
*/
fun startCollection(): Boolean
{
if (!isInitialized.get())
{
PGHLog.e(TAG, "❌ Cannot start collection - not initialized")
return false
}
if (isCapturing.get())
{
PGHLog.w(TAG, "⚠️ Collection already in progress")
return true
}
try
{
PGHLog.i(TAG, "🚀 Starting long screenshot collection")
// Clear any previous screenshots
clearStoredScreenshots()
capturedScreenshots.clear()
screenshotCount.set(0)
isCapturing.set(true)
PGHLog.i(TAG, "✅ Long screenshot collection started")
return true
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error starting collection", e)
errorCallback?.invoke("Failed to start collection: ${e.message}")
return false
}
}
/**
* Capture a single frame manually (on-demand)
* This will be called by the service which will provide the actual image data
*/
fun captureFrame(imageData: Bitmap?): Boolean
{
if (!isCapturing.get())
{
PGHLog.w(TAG, "⚠️ Cannot capture - collection not active")
return false
}
if (screenshotCount.get() >= MAX_SCREENSHOTS)
{
PGHLog.w(TAG, "⚠️ Maximum screenshots reached ($MAX_SCREENSHOTS)")
errorCallback?.invoke("Maximum screenshots reached")
return false
}
return try
{
PGHLog.d(TAG, "📸 Processing manual frame capture")
if (imageData != null)
{
PGHLog.d(TAG, "📸 Image data received, processing...")
// Process the bitmap in a coroutine
captureScope.launch {
processBitmapAsync(imageData)
}
return true
}
else
{
PGHLog.w(TAG, "⚠️ No image data provided")
return false
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error capturing frame", e)
errorCallback?.invoke("Capture failed: ${e.message}")
false
}
}
/**
* Stop collection and return captured screenshots
*/
fun finishCollection(): List<CapturedScreenshot>
{
if (!isCapturing.get())
{
PGHLog.w(TAG, "⚠️ No collection in progress")
return emptyList()
}
try
{
PGHLog.i(TAG, "🏁 Finishing long screenshot collection")
isCapturing.set(false)
val screenshots = capturedScreenshots.toList()
PGHLog.i(TAG, "✅ Collection finished with ${screenshots.size} screenshots")
return screenshots
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error finishing collection", e)
errorCallback?.invoke("Failed to finish collection: ${e.message}")
return emptyList()
}
}
/**
* Stop collection and automatically stitch screenshots into a single image
*/
fun finishCollectionAndStitch()
{
if (!isCapturing.get())
{
PGHLog.w(TAG, "⚠️ No collection in progress")
return
}
try
{
PGHLog.i(TAG, "🏁 Finishing collection and starting stitching process")
val screenshots = finishCollection()
if (screenshots.isEmpty())
{
PGHLog.w(TAG, "⚠️ No screenshots to stitch")
stitchingCompleteCallback?.invoke(
ImageStitcher.StitchingResult(false, errorMessage = "No screenshots to stitch")
)
return
}
// Start stitching process
captureScope.launch {
stitchScreenshots(screenshots)
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error starting stitching process", e)
stitchingCompleteCallback?.invoke(
ImageStitcher.StitchingResult(false, errorMessage = "Failed to start stitching: ${e.message}")
)
}
}
private suspend fun stitchScreenshots(screenshots: List<CapturedScreenshot>)
{
try
{
PGHLog.i(TAG, "🧩 Starting stitching process with ${screenshots.size} screenshots")
// Notify progress
stitchingProgressCallback?.invoke(0.1f, "Starting image stitching...")
// Perform stitching
val result = imageStitcher.stitchScreenshots(screenshots)
// If stitching succeeded and we don't want to keep individual screenshots, clean them up
// Debug mode always keeps screenshots for analysis
if (result.success && !keepIndividualScreenshots && !debugMode)
{
PGHLog.i(TAG, "🧹 Cleaning up individual screenshots as requested")
screenshots.forEach { screenshot ->
try
{
File(screenshot.filePath).delete()
PGHLog.d(TAG, "🗑️ Deleted: ${screenshot.filename}")
}
catch (e: Exception)
{
PGHLog.w(TAG, "⚠️ Failed to delete: ${screenshot.filename}", e)
}
}
}
else if (debugMode)
{
PGHLog.i(TAG, "🐛 Debug mode: Keeping all individual screenshots for analysis")
}
// Notify completion
PGHLog.i(TAG, "🧩 Stitching process completed: success=${result.success}")
stitchingCompleteCallback?.invoke(result)
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error during stitching process", e)
stitchingCompleteCallback?.invoke(
ImageStitcher.StitchingResult(false, errorMessage = "Stitching failed: ${e.message}")
)
}
}
/**
* Cancel current collection
*/
fun cancelCollection()
{
if (!isCapturing.get())
{
PGHLog.w(TAG, "⚠️ No collection to cancel")
return
}
try
{
PGHLog.i(TAG, "❌ Canceling long screenshot collection")
isCapturing.set(false)
// Clear captured screenshots
capturedScreenshots.clear()
screenshotCount.set(0)
clearStoredScreenshots()
PGHLog.i(TAG, "✅ Collection canceled")
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error canceling collection", e)
}
}
/**
* Get current screenshot count
*/
fun getScreenshotCount(): Int = screenshotCount.get()
/**
* Check if collection is active
*/
fun isCollectionActive(): Boolean = isCapturing.get()
/**
* Set progress callback
*/
fun setProgressCallback(callback: (count: Int) -> Unit)
{
this.progressCallback = callback
}
/**
* Set error callback
*/
fun setErrorCallback(callback: (error: String) -> Unit)
{
this.errorCallback = callback
}
/**
* Set stitching progress callback
*/
fun setStitchingProgressCallback(callback: (progress: Float, message: String) -> Unit)
{
this.stitchingProgressCallback = callback
}
/**
* Set stitching completion callback
*/
fun setStitchingCompleteCallback(callback: (result: ImageStitcher.StitchingResult) -> Unit)
{
this.stitchingCompleteCallback = callback
}
/**
* Configure whether to keep individual screenshots when stitching
* @param keep true to keep individual screenshots, false to delete them after stitching
*/
fun setKeepIndividualScreenshots(keep: Boolean)
{
this.keepIndividualScreenshots = keep
}
/**
* Enable debug mode for stitching development
* In debug mode, individual screenshots are always kept and additional logging is enabled
* @param enabled true to enable debug mode
*/
fun setDebugMode(enabled: Boolean)
{
this.debugMode = enabled
if (enabled)
{
PGHLog.i(TAG, "🐛 Debug mode enabled for long screenshot stitching")
this.keepIndividualScreenshots = true // Always keep screenshots in debug mode
}
}
private suspend fun processBitmapAsync(bitmap: Bitmap) = withContext(Dispatchers.IO)
{
try
{
val timestamp = System.currentTimeMillis()
val filename = "screenshot_${timestamp}.png"
val file = File(storageDir, filename)
// Save bitmap to file
saveBitmapToFile(bitmap, file)
val screenshot = CapturedScreenshot(
id = timestamp,
filename = filename,
filePath = file.absolutePath,
timestamp = timestamp,
width = bitmap.width,
height = bitmap.height
)
capturedScreenshots.offer(screenshot)
val count = screenshotCount.incrementAndGet()
PGHLog.i(TAG, "📸 Screenshot #$count captured: $filename")
// Notify progress on main thread
handler.post {
progressCallback?.invoke(count)
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error processing bitmap", e)
errorCallback?.invoke("Failed to save screenshot: ${e.message}")
}
finally
{
// Always recycle the bitmap after processing
if (!bitmap.isRecycled) {
bitmap.recycle()
}
}
}
private fun saveBitmapToFile(bitmap: Bitmap, file: File)
{
FileOutputStream(file).use { stream ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
}
}
private fun clearStoredScreenshots()
{
try
{
storageDir.listFiles()?.forEach { file ->
if (file.name.startsWith("screenshot_") && file.name.endsWith(".png"))
{
file.delete()
}
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error clearing stored screenshots", e)
}
}
/**
* Clean up resources
*/
fun cleanup()
{
try
{
PGHLog.i(TAG, "🧹 Cleaning up long screenshot capture")
isCapturing.set(false)
isInitialized.set(false)
// Cancel any ongoing coroutines
captureScope.cancel()
// Clear collections
capturedScreenshots.clear()
screenshotCount.set(0)
// Clear stored files
clearStoredScreenshots()
// Clean up stitcher
imageStitcher.cleanup()
// Clear references
mediaProjection = null
progressCallback = null
errorCallback = null
stitchingProgressCallback = null
stitchingCompleteCallback = null
PGHLog.i(TAG, "✅ Long screenshot capture cleaned up")
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error during cleanup", e)
}
}
}
/**
* Data class representing a captured screenshot
*/
data class CapturedScreenshot(
val id: Long,
val filename: String,
val filePath: String,
val timestamp: Long,
val width: Int,
val height: Int
)

12
app/src/main/java/com/quillstudios/pokegoalshelper/capture/ScreenCaptureManager.kt

@ -39,6 +39,18 @@ interface ScreenCaptureManager
*/
fun getScreenDimensions(): Pair<Int, Int>?
/**
* Get the current MediaProjection instance for sharing with other capture systems.
* @return MediaProjection instance or null if not available
*/
fun getMediaProjection(): android.media.projection.MediaProjection?
/**
* Get the current screen density.
* @return Screen density or 0 if not initialized
*/
fun getScreenDensity(): Int
/**
* Clean up all resources. Should be called when manager is no longer needed.
*/

10
app/src/main/java/com/quillstudios/pokegoalshelper/capture/ScreenCaptureManagerImpl.kt

@ -207,6 +207,16 @@ class ScreenCaptureManagerImpl(
}
}
override fun getMediaProjection(): MediaProjection?
{
return mediaProjection
}
override fun getScreenDensity(): Int
{
return screenDensity
}
override fun release()
{
Log.d(TAG, "Releasing ScreenCaptureManager")

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

@ -46,7 +46,37 @@ class DetectionController(
detectionRequestCallback?.invoke()
}
override fun onLongScreenshotStart() {
PGHLog.d(TAG, "📸 Long screenshot start requested via controller")
longScreenshotStartCallback?.invoke()
}
override fun onLongScreenshotCapture() {
PGHLog.d(TAG, "📸 Long screenshot capture requested via controller")
longScreenshotCaptureCallback?.invoke()
}
override fun onLongScreenshotFinish() {
PGHLog.d(TAG, "📸 Long screenshot finish requested via controller")
longScreenshotFinishCallback?.invoke()
}
override fun onLongScreenshotFinishAndStitch() {
PGHLog.d(TAG, "🧩 Long screenshot finish and stitch requested via controller")
longScreenshotFinishAndStitchCallback?.invoke()
}
override fun onLongScreenshotCancel() {
PGHLog.d(TAG, "📸 Long screenshot cancel requested via controller")
longScreenshotCancelCallback?.invoke()
}
private var detectionRequestCallback: (() -> Unit)? = null
private var longScreenshotStartCallback: (() -> Unit)? = null
private var longScreenshotCaptureCallback: (() -> Unit)? = null
private var longScreenshotFinishCallback: (() -> Unit)? = null
private var longScreenshotFinishAndStitchCallback: (() -> Unit)? = null
private var longScreenshotCancelCallback: (() -> Unit)? = null
/**
* Set callback for when UI requests detection
@ -56,6 +86,23 @@ class DetectionController(
detectionRequestCallback = callback
}
/**
* Set callbacks for long screenshot events
*/
fun setLongScreenshotCallbacks(
startCallback: () -> Unit,
captureCallback: () -> Unit,
finishCallback: () -> Unit,
finishAndStitchCallback: () -> Unit,
cancelCallback: () -> Unit
) {
longScreenshotStartCallback = startCallback
longScreenshotCaptureCallback = captureCallback
longScreenshotFinishCallback = finishCallback
longScreenshotFinishAndStitchCallback = finishAndStitchCallback
longScreenshotCancelCallback = cancelCallback
}
override fun onClassFilterChanged(className: String?) {
PGHLog.i(TAG, "🔍 Class filter changed to: ${className ?: "ALL CLASSES"}")

574
app/src/main/java/com/quillstudios/pokegoalshelper/stitching/ImageStitcher.kt

@ -0,0 +1,574 @@
package com.quillstudios.pokegoalshelper.stitching
import android.graphics.Bitmap
import android.util.Log
import com.quillstudios.pokegoalshelper.capture.CapturedScreenshot
import com.quillstudios.pokegoalshelper.utils.PGHLog
import com.quillstudios.pokegoalshelper.utils.MatUtils.use
import com.quillstudios.pokegoalshelper.utils.MatUtils.useMats
import com.quillstudios.pokegoalshelper.utils.MatUtils.releaseSafely
import kotlinx.coroutines.*
import org.opencv.android.Utils
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import org.opencv.imgcodecs.Imgcodecs
import java.io.File
import java.io.FileOutputStream
import kotlin.math.*
/**
* OpenCV-based image stitching engine for combining long screenshots.
*
* Features:
* - Template matching for overlap detection
* - Precise alignment using correlation coefficients
* - Vertical image concatenation with blending
* - Quality assessment for stitching results
* - Memory optimization for large image processing
* - Error recovery for failed stitching operations
*/
class ImageStitcher(
private val context: android.content.Context
)
{
companion object
{
private const val TAG = "ImageStitcher"
// Template matching parameters
private const val MIN_OVERLAP_RATIO = 0.1f // 10% minimum overlap
private const val MAX_OVERLAP_RATIO = 0.8f // 80% maximum overlap
private const val CORRELATION_THRESHOLD = 0.3f // Template matching threshold (real screenshots often have lower correlation)
// Memory management
private const val MAX_IMAGE_DIMENSION = 4096 // Max width/height for processing
private const val TILE_PROCESSING_SIZE = 1024 // Process in tiles to save memory
// Quality assessment
private const val MIN_QUALITY_SCORE = 0.6f // Minimum acceptable quality
}
// Storage
private val outputDir: File by lazy {
File(context.filesDir, "stitched_screenshots").apply {
if (!exists()) mkdirs()
}
}
// Processing scope
private val stitchingScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
/**
* Result of stitching operation
*/
data class StitchingResult(
val success: Boolean,
val outputPath: String? = null,
val qualityScore: Float = 0f,
val errorMessage: String? = null,
val processedCount: Int = 0,
val finalDimensions: Pair<Int, Int>? = null
)
/**
* Overlap detection result
*/
data class OverlapInfo(
val detected: Boolean,
val offsetY: Int = 0,
val confidence: Float = 0f,
val overlapHeight: Int = 0
)
/**
* Stitch a list of screenshots into a single long image
*/
suspend fun stitchScreenshots(screenshots: List<CapturedScreenshot>): StitchingResult = withContext(Dispatchers.IO)
{
if (screenshots.isEmpty())
{
PGHLog.w(TAG, "No screenshots to stitch")
return@withContext StitchingResult(false, errorMessage = "No screenshots provided")
}
if (screenshots.size == 1)
{
PGHLog.i(TAG, "Single screenshot - copying to output")
return@withContext copySingleScreenshot(screenshots[0])
}
try
{
PGHLog.i(TAG, "🧩 Starting stitching process with ${screenshots.size} screenshots")
// Load and validate all images
val imageMats = loadScreenshotMats(screenshots)
if (imageMats.isEmpty())
{
return@withContext StitchingResult(false, errorMessage = "Failed to load screenshot images")
}
try
{
// Perform stitching
val stitchingResult = performStitching(imageMats, screenshots)
return@withContext stitchingResult
}
finally
{
// Clean up loaded images
releaseSafely(*imageMats.toTypedArray())
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error during stitching process", e)
return@withContext StitchingResult(false, errorMessage = "Stitching failed: ${e.message}")
}
}
private fun loadScreenshotMats(screenshots: List<CapturedScreenshot>): List<Mat>
{
val imageMats = mutableListOf<Mat>()
for ((index, screenshot) in screenshots.withIndex())
{
try
{
PGHLog.d(TAG, "📖 Loading screenshot ${index + 1}: ${screenshot.filename}")
val mat = Imgcodecs.imread(screenshot.filePath, Imgcodecs.IMREAD_COLOR)
if (mat.empty())
{
PGHLog.e(TAG, "❌ Failed to load image: ${screenshot.filePath}")
continue
}
// Validate image dimensions
if (mat.cols() == 0 || mat.rows() == 0)
{
PGHLog.e(TAG, "❌ Invalid image dimensions: ${mat.cols()}x${mat.rows()}")
mat.release()
continue
}
// Scale down if image is too large for processing
val scaledMat = scaleImageIfNeeded(mat)
if (scaledMat != mat)
{
mat.release()
}
imageMats.add(scaledMat)
PGHLog.d(TAG, "✅ Loaded image ${index + 1}: ${scaledMat.cols()}x${scaledMat.rows()}")
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error loading screenshot ${index + 1}", e)
}
}
PGHLog.i(TAG, "📖 Loaded ${imageMats.size}/${screenshots.size} images successfully")
return imageMats
}
private fun scaleImageIfNeeded(mat: Mat): Mat
{
val maxDim = max(mat.cols(), mat.rows())
if (maxDim <= MAX_IMAGE_DIMENSION)
{
return mat
}
val scale = MAX_IMAGE_DIMENSION.toFloat() / maxDim
val newWidth = (mat.cols() * scale).toInt()
val newHeight = (mat.rows() * scale).toInt()
PGHLog.d(TAG, "🔄 Scaling image from ${mat.cols()}x${mat.rows()} to ${newWidth}x${newHeight}")
val scaledMat = Mat()
Imgproc.resize(mat, scaledMat, Size(newWidth.toDouble(), newHeight.toDouble()))
return scaledMat
}
private suspend fun performStitching(imageMats: List<Mat>, screenshots: List<CapturedScreenshot>): StitchingResult
{
if (imageMats.size != screenshots.size)
{
return StitchingResult(false, errorMessage = "Image count mismatch")
}
// Start with first image
var result = imageMats[0].clone()
var currentHeight = result.rows()
var processedCount = 1
try
{
PGHLog.i(TAG, "🧩 Starting with base image: ${result.cols()}x${result.rows()}")
// Process each subsequent image
for (i in 1 until imageMats.size)
{
val currentImage = imageMats[i]
PGHLog.d(TAG, "🔍 Processing image ${i + 1}/${imageMats.size}")
// Detect overlap with previous section
val overlapInfo = detectOverlap(result, currentImage, currentHeight)
val newResult = if (!overlapInfo.detected)
{
PGHLog.w(TAG, "⚠️ No overlap detected for image ${i + 1}, appending directly")
// Append without overlap
appendImageDirectly(result, currentImage)
}
else
{
PGHLog.i(TAG, "✅ Overlap detected: offset=${overlapInfo.offsetY}, confidence=${overlapInfo.confidence}")
// Blend with overlap
blendImageWithOverlap(result, currentImage, overlapInfo)
}
// Release old result and update to new one
result.release()
result = newResult
currentHeight = result.rows()
processedCount++
// Yield to prevent blocking
kotlinx.coroutines.yield()
}
// Assess final quality
val qualityScore = assessStitchingQuality(result, imageMats.size)
PGHLog.i(TAG, "📊 Stitching quality score: $qualityScore")
if (qualityScore < MIN_QUALITY_SCORE)
{
PGHLog.w(TAG, "⚠️ Low quality stitching result: $qualityScore < $MIN_QUALITY_SCORE")
}
// Save result
val outputPath = saveStitchedImage(result)
return if (outputPath != null)
{
PGHLog.i(TAG, "✅ Stitching completed: ${result.cols()}x${result.rows()}, saved to: $outputPath")
StitchingResult(
success = true,
outputPath = outputPath,
qualityScore = qualityScore,
processedCount = processedCount,
finalDimensions = Pair(result.cols(), result.rows())
)
}
else
{
StitchingResult(false, errorMessage = "Failed to save stitched image")
}
}
finally
{
// Always clean up the result Mat
result.release()
}
}
private fun detectOverlap(baseImage: Mat, newImage: Mat, searchStartY: Int): OverlapInfo
{
PGHLog.d(TAG, "🔍 Overlap detection: base=${baseImage.cols()}x${baseImage.rows()}, new=${newImage.cols()}x${newImage.rows()}")
// For screenshots, let's assume a typical overlap of about 10-30% from the bottom of the base image
// This is much more reliable than complex template matching for UI screenshots
val assumedOverlapRatio = 0.2f // Assume 20% overlap
val overlapHeight = (baseImage.rows() * assumedOverlapRatio).toInt()
val overlapStartY = baseImage.rows() - overlapHeight
// Validate this makes sense
if (overlapHeight < 50 || overlapHeight > baseImage.rows() / 2)
{
PGHLog.d(TAG, "⚠️ Calculated overlap doesn't make sense: $overlapHeight px")
return OverlapInfo(false)
}
// Simple validation - check if the bottom of base image and top of new image are similar
val similarity = checkImageSimilarity(baseImage, newImage, overlapStartY, overlapHeight)
PGHLog.d(TAG, "🎯 Simple overlap check: startY=$overlapStartY, height=$overlapHeight, similarity=$similarity")
if (similarity > 0.3f) // Lower threshold for screenshots
{
PGHLog.i(TAG, "✅ Simple overlap detected: startY=$overlapStartY, height=$overlapHeight, similarity=$similarity")
return OverlapInfo(
detected = true,
offsetY = overlapStartY,
confidence = similarity,
overlapHeight = overlapHeight
)
}
else
{
PGHLog.d(TAG, "🔍 No reliable overlap found - similarity $similarity too low")
return OverlapInfo(false)
}
}
private fun checkImageSimilarity(baseImage: Mat, newImage: Mat, overlapStartY: Int, overlapHeight: Int): Float
{
val baseOverlap = Mat()
val newOverlap = Mat()
val result = Mat()
try
{
// Extract overlap regions
val baseRect = Rect(0, overlapStartY, baseImage.cols(), overlapHeight)
val newRect = Rect(0, 0, newImage.cols(), overlapHeight)
baseImage.submat(baseRect).copyTo(baseOverlap)
newImage.submat(newRect).copyTo(newOverlap)
// Simple correlation check
Imgproc.matchTemplate(baseOverlap, newOverlap, result, Imgproc.TM_CCOEFF_NORMED)
val minMaxResult = Core.minMaxLoc(result)
return minMaxResult.maxVal.toFloat()
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error checking similarity", e)
return 0f
}
finally
{
baseOverlap.release()
newOverlap.release()
result.release()
}
}
private fun appendImageDirectly(baseImage: Mat, newImage: Mat): Mat
{
val combinedHeight = baseImage.rows() + newImage.rows()
val combinedWidth = max(baseImage.cols(), newImage.cols())
return Mat(combinedHeight, combinedWidth, baseImage.type()).apply {
try
{
// Copy base image to top
val baseRect = Rect(0, 0, baseImage.cols(), baseImage.rows())
baseImage.copyTo(this.submat(baseRect))
// Copy new image to bottom
val newRect = Rect(0, baseImage.rows(), newImage.cols(), newImage.rows())
newImage.copyTo(this.submat(newRect))
PGHLog.d(TAG, "📎 Appended image directly: ${this.cols()}x${this.rows()}")
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error appending image", e)
this.release()
throw e
}
}
}
private fun blendImageWithOverlap(baseImage: Mat, newImage: Mat, overlapInfo: OverlapInfo): Mat
{
// Calculate dimensions
val overlapStartY = overlapInfo.offsetY
val overlapHeight = min(overlapInfo.overlapHeight, baseImage.rows() - overlapStartY)
val nonOverlapHeight = newImage.rows() - overlapHeight
val combinedHeight = baseImage.rows() + newImage.rows() - overlapHeight // Subtract overlap to avoid duplication
val combinedWidth = max(baseImage.cols(), newImage.cols())
PGHLog.d(TAG, "🎨 Blend calculation: baseH=${baseImage.rows()}, newH=${newImage.rows()}")
PGHLog.d(TAG, "🎨 Overlap: startY=$overlapStartY, height=$overlapHeight")
PGHLog.d(TAG, "🎨 Result: combinedH=$combinedHeight, nonOverlapH=$nonOverlapHeight")
return Mat(combinedHeight, combinedWidth, baseImage.type()).apply {
try
{
// Copy entire base image first
val baseRect = Rect(0, 0, baseImage.cols(), baseImage.rows())
baseImage.submat(baseRect).copyTo(this.submat(baseRect))
// Calculate where the new image should start (after base image minus overlap)
val newImageStartY = baseImage.rows() - overlapHeight
// Blend overlap region (in the base image area)
if (overlapHeight > 0)
{
blendOverlapRegion(
baseImage, newImage, this,
newImageStartY, overlapHeight
)
}
// Copy non-overlapping part of new image
if (nonOverlapHeight > 0)
{
val newBottomRect = Rect(0, baseImage.rows(), newImage.cols(), nonOverlapHeight)
val newSourceRect = Rect(0, overlapHeight, newImage.cols(), nonOverlapHeight)
newImage.submat(newSourceRect).copyTo(this.submat(newBottomRect))
}
PGHLog.d(TAG, "🎨 Blended image with overlap: ${this.cols()}x${this.rows()}")
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error blending image", e)
this.release()
throw e
}
}
}
private fun blendOverlapRegion(baseImage: Mat, newImage: Mat, result: Mat, startY: Int, height: Int)
{
try
{
PGHLog.d(TAG, "🎨 Blending overlap: startY=$startY, height=$height")
// Define rectangles for blending
val resultBlendRect = Rect(0, startY, min(baseImage.cols(), newImage.cols()), height)
val baseOverlapRect = Rect(0, startY, baseImage.cols(), height)
val newOverlapRect = Rect(0, 0, newImage.cols(), height)
val baseOverlap = Mat()
val newOverlap = Mat()
val blended = Mat()
try
{
baseImage.submat(baseOverlapRect).copyTo(baseOverlap)
newImage.submat(newOverlapRect).copyTo(newOverlap)
// Linear blend: 50% base + 50% new
Core.addWeighted(baseOverlap, 0.5, newOverlap, 0.5, 0.0, blended)
blended.copyTo(result.submat(resultBlendRect))
PGHLog.d(TAG, "🎨 Blend completed successfully")
}
finally
{
baseOverlap.release()
newOverlap.release()
blended.release()
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error in overlap blending", e)
}
}
private fun assessStitchingQuality(stitchedImage: Mat, originalCount: Int): Float
{
// Simple quality assessment based on:
// 1. Image dimensions (reasonable aspect ratio)
// 2. Non-zero pixels ratio
// 3. Smoothness metric
try
{
val aspectRatio = stitchedImage.rows().toFloat() / stitchedImage.cols()
val aspectScore = if (aspectRatio > 1.0f) min(1.0f, aspectRatio / (originalCount * 2)) else 0.2f
// Check for reasonable content (non-zero pixels)
val nonZeroPixels = Core.countNonZero(Mat().apply {
Imgproc.cvtColor(stitchedImage, this, Imgproc.COLOR_BGR2GRAY)
})
val totalPixels = stitchedImage.total().toInt()
val contentScore = nonZeroPixels.toFloat() / totalPixels
val finalScore = (aspectScore + contentScore) / 2.0f
return min(1.0f, max(0.0f, finalScore))
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error assessing quality", e)
return 0.5f // Default moderate quality
}
}
private fun saveStitchedImage(image: Mat): String?
{
return try
{
val timestamp = System.currentTimeMillis()
val filename = "stitched_screenshot_${timestamp}.png"
val outputFile = File(outputDir, filename)
val success = Imgcodecs.imwrite(outputFile.absolutePath, image)
if (success)
{
PGHLog.i(TAG, "💾 Saved stitched image: ${outputFile.absolutePath}")
outputFile.absolutePath
}
else
{
PGHLog.e(TAG, "❌ Failed to save stitched image")
null
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error saving stitched image", e)
null
}
}
private fun copySingleScreenshot(screenshot: CapturedScreenshot): StitchingResult
{
return try
{
val timestamp = System.currentTimeMillis()
val filename = "single_screenshot_${timestamp}.png"
val outputFile = File(outputDir, filename)
// Copy file
File(screenshot.filePath).copyTo(outputFile, overwrite = true)
StitchingResult(
success = true,
outputPath = outputFile.absolutePath,
qualityScore = 1.0f,
processedCount = 1,
finalDimensions = Pair(screenshot.width, screenshot.height)
)
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error copying single screenshot", e)
StitchingResult(false, errorMessage = "Failed to copy single screenshot: ${e.message}")
}
}
/**
* Clean up resources and cancel any ongoing operations
*/
fun cleanup()
{
try
{
PGHLog.i(TAG, "🧹 Cleaning up ImageStitcher")
stitchingScope.cancel()
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error during cleanup", e)
}
}
}

132
app/src/main/java/com/quillstudios/pokegoalshelper/ui/EnhancedFloatingFAB.kt

@ -32,7 +32,12 @@ class EnhancedFloatingFAB(
private val context: Context,
private val onDetectionRequested: () -> Unit,
private val onToggleOverlay: () -> Unit,
private val onReturnToApp: () -> Unit
private val onReturnToApp: () -> Unit,
private val onLongScreenshotStart: () -> Unit,
private val onLongScreenshotCapture: () -> Unit,
private val onLongScreenshotFinish: () -> Unit,
private val onLongScreenshotFinishAndStitch: () -> Unit,
private val onLongScreenshotCancel: () -> Unit
) {
companion object {
private const val FAB_SIZE_DP = 56
@ -53,6 +58,10 @@ class EnhancedFloatingFAB(
private var isMenuExpanded = false
private var isDragging = false
// Long screenshot state
private var isLongScreenshotMode = false
private var screenshotCount = 0
// Animation and positioning
private var currentX = 0
private var currentY = 0
@ -212,11 +221,32 @@ class EnhancedFloatingFAB(
Gravity.TOP or Gravity.END // Right align when menu is on left
}
// Add simplified menu items
val menuItems = listOf(
// Add menu items based on current mode
val menuItems = if (isLongScreenshotMode) {
// Long screenshot mode menu
listOf(
MenuItemData("📸 CAPTURE", android.R.drawable.ic_menu_camera, android.R.color.holo_blue_dark) {
onLongScreenshotCapture()
},
MenuItemData("✅ FINISH", android.R.drawable.ic_menu_save, android.R.color.holo_green_dark) {
onLongScreenshotFinishAndStitch()
exitLongScreenshotMode()
},
MenuItemData("❌ CANCEL", android.R.drawable.ic_menu_close_clear_cancel, android.R.color.holo_red_dark) {
onLongScreenshotCancel()
exitLongScreenshotMode()
}
)
} else {
// Normal mode menu
listOf(
MenuItemData("DETECT", android.R.drawable.ic_menu_search, android.R.color.holo_blue_dark) {
onDetectionRequested()
},
MenuItemData("📸 LONG SHOT", android.R.drawable.ic_menu_camera, android.R.color.holo_purple) {
onLongScreenshotStart()
enterLongScreenshotMode()
},
MenuItemData("OVERLAY", android.R.drawable.ic_menu_view, android.R.color.holo_green_dark) {
onToggleOverlay()
},
@ -224,6 +254,7 @@ class EnhancedFloatingFAB(
onReturnToApp()
}
)
}
menuItems.forEach { item ->
val menuRow = createMenuRow(item, isOnLeftSide)
@ -508,8 +539,13 @@ class EnhancedFloatingFAB(
}
}
// Update main FAB appearance
mainFAB?.background = createFABBackground(android.R.color.holo_red_light)
// Update main FAB appearance based on mode
val fabColor = if (isLongScreenshotMode) {
android.R.color.holo_blue_light // Blue in long screenshot mode
} else {
android.R.color.holo_red_light // Red when menu expanded
}
mainFAB?.background = createFABBackground(fabColor)
}
private fun hideMenu() {
@ -525,10 +561,92 @@ class EnhancedFloatingFAB(
}
}
// Reset main FAB appearance
mainFAB?.background = createFABBackground(android.R.color.holo_blue_bright)
// Reset main FAB appearance based on mode
val fabColor = if (isLongScreenshotMode) {
android.R.color.holo_blue_dark // Dark blue in long screenshot mode when menu closed
} else {
android.R.color.holo_blue_bright // Normal blue
}
mainFAB?.background = createFABBackground(fabColor)
}
// === Long Screenshot Mode Management ===
private fun enterLongScreenshotMode()
{
isLongScreenshotMode = true
screenshotCount = 0
// Update main FAB appearance and icon to show screenshot count
updateMainFABForLongScreenshot()
// Close menu since mode has changed
hideMenu()
}
private fun exitLongScreenshotMode()
{
isLongScreenshotMode = false
screenshotCount = 0
// Reset main FAB appearance and icon
updateMainFABForNormalMode()
// Close menu since mode has changed
hideMenu()
}
private fun updateScreenshotCount(count: Int)
{
screenshotCount = count
if (isLongScreenshotMode)
{
updateMainFABForLongScreenshot()
}
}
private fun updateMainFABForLongScreenshot()
{
mainFAB?.let { fab ->
// Show count on FAB if we have screenshots
if (screenshotCount > 0)
{
// We could show the count as text overlay, but for simplicity,
// we'll just use the blue color to indicate long screenshot mode
fab.background = createFABBackground(android.R.color.holo_blue_dark)
fab.setImageResource(android.R.drawable.ic_menu_camera)
}
else
{
fab.background = createFABBackground(android.R.color.holo_blue_light)
fab.setImageResource(android.R.drawable.ic_menu_camera)
}
}
}
private fun updateMainFABForNormalMode()
{
mainFAB?.let { fab ->
fab.background = createFABBackground(android.R.color.holo_blue_bright)
fab.setImageResource(android.R.drawable.ic_menu_preferences)
}
}
/**
* Public method to update screenshot progress from external callers
*/
fun updateLongScreenshotProgress(count: Int)
{
updateScreenshotCount(count)
}
/**
* Public method to exit long screenshot mode from external callers
*/
fun exitLongScreenshotModeExternal()
{
exitLongScreenshotMode()
}
private fun performHapticFeedback(feedbackType: Int) {
// Check if we have vibrate permission

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

@ -26,6 +26,33 @@ interface DetectionUIEvents {
* @param mode Transformation mode (DIRECT, LETTERBOX, HYBRID)
*/
fun onCoordinateModeChanged(mode: String)
// === Long Screenshot Events ===
/**
* Triggered when user starts long screenshot collection
*/
fun onLongScreenshotStart()
/**
* Triggered when user captures a frame during long screenshot collection
*/
fun onLongScreenshotCapture()
/**
* Triggered when user finishes long screenshot collection
*/
fun onLongScreenshotFinish()
/**
* Triggered when user finishes long screenshot collection and wants to stitch
*/
fun onLongScreenshotFinishAndStitch()
/**
* Triggered when user cancels long screenshot collection
*/
fun onLongScreenshotCancel()
}
/**
@ -57,4 +84,56 @@ interface DetectionUICallbacks {
* @param coordinateMode Current coordinate transformation mode
*/
fun onSettingsChanged(filterClass: String?, debugMode: Boolean, coordinateMode: String)
// === Long Screenshot Callbacks ===
/**
* Called when long screenshot mode starts
*/
fun onLongScreenshotModeStarted()
/**
* Called when long screenshot mode ends
*/
fun onLongScreenshotModeEnded()
/**
* Called when a screenshot is captured during long screenshot mode
* @param count Current number of screenshots captured
*/
fun onLongScreenshotProgress(count: Int)
/**
* Called when long screenshot collection fails
* @param error Error message
*/
fun onLongScreenshotError(error: String)
// === Image Stitching Callbacks ===
/**
* Called when image stitching process starts
*/
fun onStitchingStarted()
/**
* Called during stitching to report progress
* @param progress Progress percentage (0.0 to 1.0)
* @param message Status message
*/
fun onStitchingProgress(progress: Float, message: String)
/**
* Called when stitching completes successfully
* @param outputPath Path to the stitched image
* @param qualityScore Quality score of the stitching (0.0 to 1.0)
* @param dimensions Final image dimensions (width, height)
*/
fun onStitchingCompleted(outputPath: String, qualityScore: Float, dimensions: Pair<Int, Int>)
/**
* Called when stitching fails
* @param error Error message
*/
fun onStitchingFailed(error: String)
}
Loading…
Cancel
Save