How to Capture Android Screenshots with Java
Capture Android screens in Java with UiAutomation, UiDevice, window APIs, or MediaProjection. Includes complete code, failures, lifecycle, and testing guidance.
Use the API that matches where your code runs. For an instrumentation or UI test that needs the whole device, use UiAutomation.takeScreenshot() or AndroidX UiDevice.takeScreenshot(...). For a test that needs one app window, API 34 adds UiAutomation.takeScreenshot(Window). For a feature inside a normal production app, use MediaProjectionManager; Android shows the user a consent dialog before capture.
These APIs have different permissions, lifecycle rules, output types, and failure modes. The examples below use Java and check every nullable or boolean result.
1. Choose the screenshot API
| Goal | Recommended API | Runs in | Key constraint |
|---|---|---|---|
| Whole device in a UI test | UiAutomation.takeScreenshot() |
Instrumentation | Returns a Bitmap or null; API 18+ |
| Whole device with convenient file output | UiDevice.takeScreenshot(File) |
AndroidX UI Automator test | Returns true or false; PNG output |
| One app window in a test | UiAutomation.takeScreenshot(Window) |
Instrumentation | API 34+; window must be laid out and have a valid surface |
| User-facing screen capture | MediaProjectionManager |
Production app | Explicit user consent and resource cleanup |
| Visual assertion for one view or Compose node | Targeted view or Compose capture | UI test | Prefer a stable target over a whole-device image |
Android’s instrumentation reference says: “A typical test case should be using either the UiAutomation or Instrumentation APIs.” Instrumentation API reference
2. Capture the whole device with UiAutomation
UiAutomation.takeScreenshot() has been available since API level 18. Obtain it from Instrumentation.getUiAutomation(). UiAutomation APIs can work across application boundaries, which makes them suitable for end-to-end tests.
Gradle setup
dependencies {
androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("androidx.test:rules:1.6.1")
}
Use versions that match your project’s current AndroidX catalog. The API shape below is independent of the exact library patch version.
Save a Bitmap from Java
package com.example.screenshot;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import android.app.Instrumentation;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@RunWith(AndroidJUnit4.class)
public class DeviceScreenshotTest {
@Test
public void captureDevice() throws IOException {
Instrumentation instrumentation = getInstrumentation();
Bitmap bitmap = instrumentation.getUiAutomation().takeScreenshot();
if (bitmap == null) {
throw new AssertionError("Android did not return a device screenshot");
}
File output = new File(
instrumentation.getTargetContext().getCacheDir(),
"device-screenshot.png");
try (FileOutputStream stream = new FileOutputStream(output)) {
boolean written = bitmap.compress(CompressFormat.PNG, 100, stream);
if (!written) {
throw new IOException("Bitmap.compress returned false");
}
} finally {
bitmap.recycle();
}
}
}
The cache directory is convenient for a test artifact. Copy the file to your test-report location using your runner or CI artifact mechanism. Do not assume that a production app has permission to write to an arbitrary shared path.
When this capture can fail
nullresult: the system could not produce a frame. Wait until the UI is idle and visible, then retry once.- Wrong screen: your test captured before navigation or animation finished. Wait for a deterministic view condition instead of sleeping for an arbitrary duration.
- Rotation mismatch: the returned bitmap follows the display orientation. Make the test orientation explicit if pixel comparisons depend on dimensions.
3. Save a PNG with AndroidX UiDevice
AndroidX UI Automator wraps device operations and can write a PNG directly. UiDevice.takeScreenshot(File) uses the original scale and 90% quality by default, adjusts for display rotation, and returns true only when the file is created successfully. The overload accepts a scale and quality from 0 to 100.
package com.example.screenshot;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
@RunWith(AndroidJUnit4.class)
public class UiDeviceScreenshotTest {
@Test
public void savePng() {
UiDevice device = UiDevice.getInstance(getInstrumentation());
File output = new File(
getInstrumentation().getTargetContext().getCacheDir(),
"ui-device.png");
boolean ok = device.takeScreenshot(output, 1.0f, 90);
if (!ok) {
throw new AssertionError("UiDevice could not create " + output);
}
}
}
For a smaller artifact, lower the scale. For lossless visual baselines, use PNG and keep the scale fixed across devices. A Bitmap overload is also available; check it for null before using it. See the UiDevice API reference.
4. Capture one window on Android 14 (API 34+)
API level 34 adds UiAutomation.takeScreenshot(Window). This is useful when a test needs a particular window rather than status bars, another app, or the entire display.
import android.app.Instrumentation;
import android.graphics.Bitmap;
import android.os.Build;
import android.view.Window;
import androidx.annotation.RequiresApi;
import androidx.test.platform.app.InstrumentationRegistry;
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public final class WindowCapture {
private WindowCapture() {}
public static Bitmap capture(Window window) {
if (window == null) {
throw new IllegalArgumentException("window == null");
}
Instrumentation instrumentation =
InstrumentationRegistry.getInstrumentation();
Bitmap bitmap = instrumentation.getUiAutomation()
.takeScreenshot(window);
if (bitmap == null) {
throw new IllegalStateException(
"Window screenshot unavailable; verify layout and SurfaceControl");
}
return bitmap;
}
}
A null result can mean that layout has not completed, the window has no valid SurfaceControl, or SurfaceFlinger reported an error. Call it after the window is shown and laid out; do not treat a null result as an empty image.
5. Capture the screen from a normal Android app with MediaProjection
A production app cannot silently read the display. Start the system consent flow with MediaProjectionManager.createScreenCaptureIntent(). After the user approves, pass the result to getMediaProjection(...), create a VirtualDisplay, and direct frames into a Surface backed by an ImageReader or video encoder.
MediaProjection was introduced in API 21. Apps targeting Android Q (API 29) or later need a media-projection foreground service. Android U (API 34) adds further ordering and permission requirements. Check the current MediaProjection guide and your target SDK before shipping.
Manifest and service outline
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<application ...>
<service
android:name=".CaptureService"
android:foregroundServiceType="mediaProjection"
android:exported="false" />
</application>
Foreground-service declarations are target-SDK sensitive. Use the exact permissions required by the Android version and target SDK in your build.
Request consent from an Activity
public class MainActivity extends Activity {
private static final int REQUEST_CAPTURE = 42;
private MediaProjectionManager projectionManager;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
projectionManager = (MediaProjectionManager)
getSystemService(Context.MEDIA_PROJECTION_SERVICE);
}
public void requestCapture() {
Intent intent = projectionManager.createScreenCaptureIntent();
startActivityForResult(intent, REQUEST_CAPTURE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != REQUEST_CAPTURE || resultCode != RESULT_OK || data == null) {
return; // User declined or the result was invalid.
}
Intent service = new Intent(this, CaptureService.class)
.setAction(CaptureService.ACTION_START)
.putExtra(CaptureService.EXTRA_RESULT_CODE, resultCode)
.putExtra(CaptureService.EXTRA_RESULT_DATA, data);
ContextCompat.startForegroundService(this, service);
}
}
Create the VirtualDisplay and release it safely
public class CaptureService extends Service {
public static final String ACTION_START = "capture.START";
public static final String EXTRA_RESULT_CODE = "result_code";
public static final String EXTRA_RESULT_DATA = "result_data";
private MediaProjection projection;
private VirtualDisplay virtualDisplay;
private ImageReader imageReader;
private Surface surface;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null || !ACTION_START.equals(intent.getAction())) {
stopSelf();
return START_NOT_STICKY;
}
startProjectionForegroundNotification();
int resultCode = intent.getIntExtra(EXTRA_RESULT_CODE, Activity.RESULT_CANCELED);
Intent resultData = intent.getParcelableExtra(EXTRA_RESULT_DATA);
if (resultCode != Activity.RESULT_OK || resultData == null) {
stopSelf();
return START_NOT_STICKY;
}
MediaProjectionManager manager = (MediaProjectionManager)
getSystemService(Context.MEDIA_PROJECTION_SERVICE);
projection = manager.getMediaProjection(resultCode, resultData);
if (projection == null) {
stopSelf();
return START_NOT_STICKY;
}
projection.registerCallback(new MediaProjection.Callback() {
@Override
public void onStop() {
releaseCapture();
stopSelf();
}
}, new Handler(Looper.getMainLooper()));
DisplayMetrics metrics = getResources().getDisplayMetrics();
int width = metrics.widthPixels;
int height = metrics.heightPixels;
int density = metrics.densityDpi;
imageReader = ImageReader.newInstance(
width, height, PixelFormat.RGBA_8888, 2);
surface = imageReader.getSurface();
imageReader.setOnImageAvailableListener(reader -> {
try (Image image = reader.acquireLatestImage()) {
if (image != null) {
// Copy pixels or encode them before this Image is closed.
}
}
}, new Handler(Looper.getMainLooper()));
virtualDisplay = projection.createVirtualDisplay(
"JavaScreenCapture",
width,
height,
density,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface,
null,
null);
if (virtualDisplay == null) {
releaseCapture();
stopSelf();
}
return START_NOT_STICKY;
}
private void releaseCapture() {
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
if (imageReader != null) {
imageReader.close();
imageReader = null;
}
if (surface != null) {
surface.release();
surface = null;
}
if (projection != null) {
projection.stop();
projection = null;
}
}
@Override
public void onDestroy() {
releaseCapture();
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
Register the callback before creating the virtual display. The system can stop projection when the user stops it from system UI, the screen locks, or another projection session starts. Always release the virtual display, surface, reader, and projection in the callback and service teardown. The MediaProjection reference documents this lifecycle.
6. Capture a view or Compose node for visual tests
A whole-device screenshot includes system bars, animations, and unrelated windows. For a visual regression test, capture the smallest stable target: a view, a screenshot-test rule, or a Compose node supported by your test stack. Keep device-wide screenshots for debugging and end-to-end evidence. AndroidX describes DeviceCapture as an experimental, debugging-oriented whole-screen helper; it is not a universal visual-assertion API.
Make the target deterministic: disable transitions, wait for content to settle, use fixed font scale and locale, and keep device dimensions consistent. Compare pixels only after deciding how to handle anti-aliasing, text rendering, and dynamic data.
7. Timing, output, and configuration choices
- Wait for readiness: wait for a visible element or an idle condition. A fixed sleep is less reliable than a state-based wait.
- Rotation: lock orientation in tests or normalize the resulting bitmap before comparison.
- Scale and quality: use UiDevice’s scale and quality overload to control artifact size. Use PNG for exact baselines; JPEG is smaller but introduces compression differences.
- Storage: write test files to the instrumentation target’s cache or your runner’s artifact directory. Check that the parent directory exists and that the stream closes.
- Memory: a full RGBA bitmap uses roughly width × height × 4 bytes before overhead. Recycle or release images promptly and use
acquireLatestImage()when dropped intermediate frames are acceptable. - Privacy: MediaProjection can capture other apps and sensitive content. Explain capture to users, store images securely, and stop projection when the feature ends.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
takeScreenshot() returns null |
No valid frame or transient system failure | Wait for the UI to be visible and idle, then retry once; fail with a diagnostic message. |
| Window overload returns null | Window not laid out, missing SurfaceControl, or SurfaceFlinger error |
Capture after layout and visibility; verify API 34+ and the window token. |
UiDevice.takeScreenshot returns false |
Destination cannot be created or screenshot failed | Use a writable test cache file, create parent directories, and check free space. |
| Screenshot shows a previous screen | Capture ran during navigation or animation | Wait on a specific destination view; disable transitions in the test configuration. |
| MediaProjection result is canceled | User declined consent or result data was lost | Handle non-OK results, request consent again from an Activity, and do not start the service. |
| Foreground-service exception | Missing or mismatched media-projection service declaration | Review current target-SDK permissions and foregroundServiceType requirements. |
| No frames arrive from ImageReader | Virtual display or surface was not created correctly | Check dimensions, density, surface validity, listener registration, and log whether createVirtualDisplay returned null. |
| App crashes after projection stops | Resources used after callback cleanup | Make cleanup idempotent, guard nullable fields, and stop consuming images after onStop(). |
9. Performance, reliability, and cost
For tests, the main cost is execution time and artifact size. Capture once per assertion or failure, avoid full-device images when a view capture answers the question, and upload only failed artifacts in CI. Reusing a test device configuration improves comparability.
For MediaProjection, frame production is continuous. Do not retain every Image; close it promptly, choose an appropriate ImageReader buffer count, and encode or downsample off the main thread. Treat projection as a session with explicit start and stop states. Android may end it outside your process, so the callback is part of the normal path.
Android screenshot APIs do not charge per capture. Infrastructure costs come from CI minutes, storage, bandwidth, and any image-processing service you add. Keep retention and resolution proportional to the debugging or product requirement.
10. Or skip the browser setup
If the screenshot you need is a website image rather than an Android device frame, ScreenshotNeo provides a single HTTP request. It accepts cookie or consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.
See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', bytes);
ScreenshotNeo also supports full-page and element captures, dark mode, device presets and custom viewports, retina scale, PDF output, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.
11. FAQ
Can a regular app call UiAutomation?
Use UiAutomation in instrumentation or UI automation tests. A user-facing app should use MediaProjection and obtain consent.
What API level supports whole-device UiAutomation screenshots?
UiAutomation.takeScreenshot() is available from API 18. The window overload was added in API 34.
Does MediaProjection capture only my app?
It captures the projection scope granted by the system and user. Treat captured pixels as potentially sensitive and stop the session when finished.
Should I compare full screenshots in visual regression tests?
Usually capture the smallest view or Compose node that proves the behavior. Whole-device captures are better for debugging and end-to-end records.
Why must I register a MediaProjection callback?
The system can stop projection independently of your code. The callback lets you release the virtual display, surface, reader, and related resources immediately.


