Skip to content

Commit f3d7449

Browse files
Fix: Correct ScreenCaptureService lifecycle and error handling
This commit addresses issues in the ScreenCaptureService related to foreground service timing and error handling. Key changes in ScreenCaptureService.kt: - `startForeground()` is now called immediately at the beginning of `onStartCommand()` to prevent crashes on Android 12+ due to StrictMode violations (ForegroundServiceStartNotAllowedException). - Added comprehensive try-catch blocks in `startCapture()`, `takeScreenshot()`, `saveScreenshot()`, and `cleanup()` methods to handle potential exceptions gracefully and ensure the service stops cleanly. - Ensured `cleanup()` is called in error scenarios within `takeScreenshot` and `startCapture`. - Used `applicationContext` for `Toast` messages to prevent issues if the service context is not ideal for UI operations. - Ensured Toast messages are posted on the main looper. - Added compatibility for fetching display metrics on Android R (API 30) and higher within `takeScreenshot()`. - Set notification channel `setShowBadge(false)`. - Made the foreground notification ongoing. - Ensured `stopForeground(STOP_FOREGROUND_REMOVE)` is called in `cleanup()`. MainActivity.kt and AndroidManifest.xml remain as per the previous refactoring to use a foreground service.
1 parent 89294b5 commit f3d7449

1 file changed

Lines changed: 99 additions & 59 deletions

File tree

app/src/main/kotlin/com/google/ai/sample/ScreenCaptureService.kt

Lines changed: 99 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -50,26 +50,33 @@ class ScreenCaptureService : Service() {
5050
}
5151

5252
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
53+
// IMPORTANT: Call startForeground immediately
54+
startForegroundImmediately()
55+
5356
if (intent?.action == ACTION_START_CAPTURE) {
5457
val resultCode = intent.getIntExtra(EXTRA_RESULT_CODE, -1)
5558
val resultData = intent.getParcelableExtra<Intent>(EXTRA_RESULT_DATA)
5659

5760
if (resultCode != -1 && resultData != null) {
58-
startForeground()
5961
startCapture(resultCode, resultData)
6062
} else {
63+
Log.e(TAG, "Invalid result code or data")
6164
stopSelf()
6265
}
66+
} else {
67+
Log.e(TAG, "Invalid action")
68+
stopSelf()
6369
}
6470
return START_NOT_STICKY
6571
}
6672

67-
private fun startForeground() {
73+
private fun startForegroundImmediately() {
6874
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
6975
.setContentTitle("Taking Screenshot")
7076
.setContentText("Processing...")
7177
.setSmallIcon(android.R.drawable.ic_menu_camera)
7278
.setPriority(NotificationCompat.PRIORITY_LOW)
79+
.setOngoing(true)
7380
.build()
7481

7582
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -87,61 +94,84 @@ class ScreenCaptureService : Service() {
8794
NotificationManager.IMPORTANCE_LOW
8895
).apply {
8996
description = "Used for taking screenshots"
97+
setShowBadge(false)
9098
}
9199
val notificationManager = getSystemService(NotificationManager::class.java)
92100
notificationManager.createNotificationChannel(channel)
93101
}
94102
}
95103

96104
private fun startCapture(resultCode: Int, data: Intent) {
97-
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
98-
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data)
105+
try {
106+
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
107+
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data)
99108

100-
Handler(Looper.getMainLooper()).postDelayed({
101-
takeScreenshot()
102-
}, 100)
109+
Handler(Looper.getMainLooper()).postDelayed({
110+
takeScreenshot()
111+
}, 100)
112+
} catch (e: Exception) {
113+
Log.e(TAG, "Error starting capture", e)
114+
cleanup()
115+
}
103116
}
104117

105118
private fun takeScreenshot() {
106-
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
107-
val displayMetrics = DisplayMetrics()
108-
windowManager.defaultDisplay.getMetrics(displayMetrics)
109-
110-
val width = displayMetrics.widthPixels
111-
val height = displayMetrics.heightPixels
112-
val density = displayMetrics.densityDpi
113-
114-
imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
115-
116-
virtualDisplay = mediaProjection?.createVirtualDisplay(
117-
"ScreenCapture",
118-
width, height, density,
119-
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
120-
imageReader!!.surface, null, null
121-
)
122-
123-
imageReader!!.setOnImageAvailableListener({ reader ->
124-
val image = reader.acquireLatestImage()
125-
if (image != null) {
126-
val planes = image.planes
127-
val buffer = planes[0].buffer
128-
val pixelStride = planes[0].pixelStride
129-
val rowStride = planes[0].rowStride
130-
val rowPadding = rowStride - pixelStride * width
131-
132-
val bitmap = Bitmap.createBitmap(
133-
width + rowPadding / pixelStride,
134-
height,
135-
Bitmap.Config.ARGB_8888
136-
)
137-
bitmap.copyPixelsFromBuffer(buffer)
138-
139-
saveScreenshot(bitmap)
140-
141-
image.close()
142-
cleanup()
119+
try {
120+
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
121+
val displayMetrics = DisplayMetrics()
122+
123+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
124+
val display = display
125+
display?.getRealMetrics(displayMetrics)
126+
} else {
127+
@Suppress("DEPRECATION")
128+
windowManager.defaultDisplay.getMetrics(displayMetrics)
143129
}
144-
}, Handler(Looper.getMainLooper()))
130+
131+
val width = displayMetrics.widthPixels
132+
val height = displayMetrics.heightPixels
133+
val density = displayMetrics.densityDpi
134+
135+
imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
136+
137+
virtualDisplay = mediaProjection?.createVirtualDisplay(
138+
"ScreenCapture",
139+
width, height, density,
140+
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
141+
imageReader!!.surface, null, null
142+
)
143+
144+
imageReader!!.setOnImageAvailableListener({ reader ->
145+
var image: android.media.Image? = null
146+
try {
147+
image = reader.acquireLatestImage()
148+
if (image != null) {
149+
val planes = image.planes
150+
val buffer = planes[0].buffer
151+
val pixelStride = planes[0].pixelStride
152+
val rowStride = planes[0].rowStride
153+
val rowPadding = rowStride - pixelStride * width
154+
155+
val bitmap = Bitmap.createBitmap(
156+
width + rowPadding / pixelStride,
157+
height,
158+
Bitmap.Config.ARGB_8888
159+
)
160+
bitmap.copyPixelsFromBuffer(buffer)
161+
162+
saveScreenshot(bitmap)
163+
}
164+
} catch (e: Exception) {
165+
Log.e(TAG, "Error processing image", e)
166+
} finally {
167+
image?.close()
168+
cleanup()
169+
}
170+
}, Handler(Looper.getMainLooper()))
171+
} catch (e: Exception) {
172+
Log.e(TAG, "Error taking screenshot", e)
173+
cleanup()
174+
}
145175
}
146176

147177
private fun saveScreenshot(bitmap: Bitmap) {
@@ -160,26 +190,36 @@ class ScreenCaptureService : Service() {
160190
outputStream.close()
161191

162192
Log.i(TAG, "Screenshot saved to: ${file.absolutePath}")
163-
Toast.makeText(
164-
this,
165-
"Screenshot saved to: Android/data/com.google.ai.sample/files/Pictures/Screenshots/",
166-
Toast.LENGTH_LONG
167-
).show()
193+
194+
Handler(Looper.getMainLooper()).post {
195+
Toast.makeText(
196+
applicationContext,
197+
"Screenshot saved to: Android/data/com.google.ai.sample/files/Pictures/Screenshots/",
198+
Toast.LENGTH_LONG
199+
).show()
200+
}
168201
} catch (e: Exception) {
169202
Log.e(TAG, "Failed to save screenshot", e)
170-
Toast.makeText(this, "Failed to save screenshot: ${e.message}", Toast.LENGTH_LONG).show()
203+
Handler(Looper.getMainLooper()).post {
204+
Toast.makeText(applicationContext, "Failed to save screenshot: ${e.message}", Toast.LENGTH_LONG).show()
205+
}
171206
}
172207
}
173208

174209
private fun cleanup() {
175-
virtualDisplay?.release()
176-
virtualDisplay = null
177-
imageReader?.close()
178-
imageReader = null
179-
mediaProjection?.stop()
180-
mediaProjection = null
181-
stopForeground(true)
182-
stopSelf()
210+
try {
211+
virtualDisplay?.release()
212+
virtualDisplay = null
213+
imageReader?.close()
214+
imageReader = null
215+
mediaProjection?.stop()
216+
mediaProjection = null
217+
} catch (e: Exception) {
218+
Log.e(TAG, "Error during cleanup", e)
219+
} finally {
220+
stopForeground(STOP_FOREGROUND_REMOVE)
221+
stopSelf()
222+
}
183223
}
184224

185225
override fun onDestroy() {

0 commit comments

Comments
 (0)