From 4d8992b6163397ea321ba0a273fb2384da99c22a Mon Sep 17 00:00:00 2001 From: Mansi Singh Date: Fri, 26 Dec 2025 19:56:47 +0530 Subject: [PATCH 1/2] Add support for capturing screenshots of multiple windows with hardware bitmap support Issue: - Falcon library fails when processing hardware-accelerated bitmaps with error: Software rendering doesn't support hardware bitmaps. - The fallback method Utils.capture() is not capable of capturing screenshots of multiple window surfaces (dialogs, bottom sheets, Compose overlays). Cause: - Falcon library uses Canvas-based rendering which requires software bitmaps, but modern Android apps use hardware-accelerated bitmaps for performance improvement, causing screenshot capture to fail. - The fallback approach used Utils.capture() which internally uses PixelCopy API that supports hardware bitmaps but could only capture the screenshot of the main activity window and not any additional windows layered on top. - Each window (activity, dialog, bottom sheet) has its own Surface in Android's rendering system, requiring separate capture operations. Fix: - Created new MultiWindowScreenshotCapture.java utility class to handle hardware-accelerated bitmap capture of multiple windows using PixelCopy API. - Implemented 3-tier fallback strategy in Shaky.java: 1. Attempt Falcon library for fast Canvas-based capture (works for traditional Views without hardware bitmaps). 2. Use PixelCopy multi-window capture when Falcon fails (handles hardware bitmaps). 3. Fallback to Utils.capture() for main activity window only (last resort). - Implemented getRootViews() to detect all active windows using reflection into WindowManager internals. - Added captureMultipleAsync() method with dual capture strategy: - Primary: Capture from Surface (via reflection on ViewRoot's mSurface) for per-window isolation. - Fallback: Capture from Window object when Surface unavailable. - Created ViewRootData class to encapsulate window hierarchy information including view reference, window frame bounds, layout parameters, ViewRoot object, window type detection, and window association via tokens. - Updated CollectDataTask.java to handle multiple bitmaps where first bitmap becomes the main screenshot for UI preview and subsequent bitmaps are added as attachments. --- .../android/shaky/CollectDataTask.java | 32 +- .../shaky/MultiWindowScreenshotCapture.java | 477 ++++++++++++++++++ .../com/linkedin/android/shaky/Shaky.java | 62 ++- 3 files changed, 552 insertions(+), 19 deletions(-) create mode 100644 shaky/src/main/java/com/linkedin/android/shaky/MultiWindowScreenshotCapture.java diff --git a/shaky/src/main/java/com/linkedin/android/shaky/CollectDataTask.java b/shaky/src/main/java/com/linkedin/android/shaky/CollectDataTask.java index 00caa76..9253e76 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/CollectDataTask.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/CollectDataTask.java @@ -67,15 +67,33 @@ protected Result doInBackground(Bitmap... params) { } } - Uri screenshotUri = null; - Bitmap bitmap = params != null && params.length != 0 ? params[0] : null; - if (bitmap != null) { - File screenshotFile = Utils.writeBitmapToDirectory(bitmap, screenshotDirectory); - screenshotUri = Uri.fromFile(screenshotFile); + Result result = new Result(); + + // Process all bitmaps + if (params != null && params.length > 0) { + for (int i = 0; i < params.length; i++) { + Bitmap bitmap = params[i]; + if (bitmap != null) { + File screenshotFile = Utils.writeBitmapToDirectory(bitmap, screenshotDirectory); + + if (screenshotFile != null) { + Uri screenshotUri = Uri.fromFile(screenshotFile); + + // First bitmap becomes the main screenshot (for UI preview) + if (i == 0) { + result.setScreenshotUri(screenshotUri); + } else { + // Subsequent screenshots (dialogs/bottom sheets) are only attachments + result.getAttachments().add(screenshotUri); + } + } else { + Log.e(TAG, "Failed to write bitmap " + i + " to file"); + } + } + } + Log.d(TAG, "Saved " + result.getAttachments().size() + " screenshot(s) total"); } - Result result = new Result(); - result.setScreenshotUri(screenshotUri); delegate.collectData(activity, result); return result; } diff --git a/shaky/src/main/java/com/linkedin/android/shaky/MultiWindowScreenshotCapture.java b/shaky/src/main/java/com/linkedin/android/shaky/MultiWindowScreenshotCapture.java new file mode 100644 index 0000000..6c84475 --- /dev/null +++ b/shaky/src/main/java/com/linkedin/android/shaky/MultiWindowScreenshotCapture.java @@ -0,0 +1,477 @@ +/** + * Copyright (C) 2016 LinkedIn Corp. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.linkedin.android.shaky; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.util.Log; +import android.view.PixelCopy; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Multi-window screenshot capture for Android applications. + * + * Uses PixelCopy API (Android O+) to capture hardware-accelerated bitmaps that cannot be + * accessed via traditional Canvas rendering. This is required for Jetpack Compose screens + * and Coil-loaded images which create GPU-stored bitmaps. + * + * Captures each window separately (activity, dialogs, bottom sheets) by reflecting into + * WindowManager's internal data structures to enumerate all root views. + * + * Requirements: Android API 26+ (PixelCopy API) + */ +final class MultiWindowScreenshotCapture { + private static final String TAG = "MultiWindowCapture"; + + private MultiWindowScreenshotCapture() { + } + + /** + * Captures screenshots of all visible windows asynchronously. + *

+ * Uses reflection to enumerate all windows, then captures each via PixelCopy. + * Callback is invoked on main thread when all captures complete. + * + * @param activity the activity to capture screenshots from + * @param callback receives list of bitmaps (one per window), or null if all captures failed + */ + static void captureMultipleAsync(@NonNull Activity activity, + @NonNull MultiBitmapCallback callback + ) { + final List rootViews = getRootViews(activity); + + if (rootViews.isEmpty()) { + callback.onCaptureComplete(null); + return; + } + + Log.d(TAG, "Found " + rootViews.size() + " window(s) to capture"); + + final Bitmap[] bitmaps = new Bitmap[rootViews.size()]; + final AtomicInteger completedCount = new AtomicInteger(0); + + for (int i = 0; i < rootViews.size(); i++) { + final int index = i; + final ViewRootData rootView = rootViews.get(i); + + Window windowForView = rootView.getWindow(); + if (windowForView == null) { + windowForView = activity.getWindow(); + } + + captureAsync(rootView, windowForView, new CaptureCallback() { + @Override + public void onCaptureComplete(Bitmap bitmap) { + bitmaps[index] = bitmap; + int completed = completedCount.incrementAndGet(); + + if (bitmap == null) { + Log.e(TAG, "Failed to capture window " + index); + } + + if (completed == rootViews.size()) { + List bitmapList = new ArrayList<>(); + for (Bitmap bmp : bitmaps) { + if (bmp != null) { + bitmapList.add(bmp); + } + } + + callback.onCaptureComplete(bitmapList.isEmpty() ? null : bitmapList); + } + } + }); + } + } + + /** + * Gets all root views using reflection to access WindowManager internals. + * Includes activity, dialogs, and bottom sheets currently visible. + * + * @param activity the activity context + * @return List of ViewRootData, or empty list if reflection fails + */ + @SuppressWarnings("unchecked") + static List getRootViews(@NonNull Activity activity) { + Object globalWindowManager = getFieldValueSafe("mGlobal", activity.getWindowManager()); + if (globalWindowManager == null) { + return Collections.emptyList(); + } + + Object rootObjects = getFieldValueSafe("mRoots", globalWindowManager); + Object paramsObject = getFieldValueSafe("mParams", globalWindowManager); + + if (rootObjects == null || paramsObject == null) { + return Collections.emptyList(); + } + + Object[] roots = ((List) rootObjects).toArray(); + List paramsList = + (List) paramsObject; + WindowManager.LayoutParams[] params = paramsList.toArray(new WindowManager.LayoutParams[0]); + + List rootViews = extractViewRootData(roots, params); + if (rootViews.isEmpty()) { + return Collections.emptyList(); + } + + offsetRootsTopLeft(rootViews); + ensureDialogsAreAfterActivities(rootViews); + + return rootViews; + } + + /** + * Captures a screenshot of a view using the associated window. + * Uses PixelCopy API (Android O+) for hardware bitmap support. + * + * @param viewRootData information about the view root to capture + * @param window the window containing the view + * @param callback callback to receive the captured bitmap + */ + private static void captureAsync(@NonNull ViewRootData viewRootData, + @Nullable Window window, + @NonNull CaptureCallback callback) { + final View view = viewRootData._view.getRootView(); + + if (view.getWidth() == 0 || view.getHeight() == 0) { + callback.onCaptureComplete(null); + return; + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || window == null) { + Log.e(TAG, "PixelCopy not available (API < 26) or window is null"); + callback.onCaptureComplete(null); + return; + } + + final Bitmap bitmap = + Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); + + // Try to get the Surface from ViewRoot for per-window capture + Surface surface = getSurfaceFromViewRoot(viewRootData._viewRoot); + + if (surface != null && surface.isValid()) { + // Use Surface directly - captures specific window (activity/dialog/bottom sheet) + PixelCopy.request(surface, bitmap, copyResult -> { + if (copyResult == PixelCopy.SUCCESS) { + callback.onCaptureComplete(bitmap); + } else { + Log.e(TAG, "PixelCopy from Surface failed with result: " + copyResult); + callback.onCaptureComplete(null); + } + }, new Handler(Looper.getMainLooper())); + } else { + // Fallback to Window + PixelCopy.request(window, bitmap, copyResult -> { + if (copyResult == PixelCopy.SUCCESS) { + callback.onCaptureComplete(bitmap); + } else { + Log.e(TAG, "PixelCopy from Window failed with result: " + copyResult); + callback.onCaptureComplete(null); + } + }, new Handler(Looper.getMainLooper())); + } + } + + /** + * Safely extracts Surface from ViewRootImpl using reflection. + * + * @param viewRoot the ViewRootImpl object + * @return Surface if available and valid, null otherwise + */ + @Nullable + private static Surface getSurfaceFromViewRoot(@Nullable Object viewRoot) { + if (viewRoot == null) { + return null; + } + + Object surfaceObj = getFieldValueSafe("mSurface", viewRoot); + return surfaceObj instanceof Surface ? (Surface) surfaceObj : null; + } + + /** + * Extracts view root data from WindowManager's internal arrays. Filters out hidden views and + * calculates content bounds for dialogs/bottom sheets. + * + * @param roots ViewRootImpl objects from WindowManager + * @param params corresponding LayoutParams for each root + * @return List of ViewRootData with screen positions + */ + private static List extractViewRootData(Object[] roots, + WindowManager.LayoutParams[] params) { + List rootViews = new ArrayList<>(); + + for (int i = 0; i < roots.length; i++) { + Object root = roots[i]; + View rootView = (View) getFieldValueSafe("mView", root); + + if (rootView == null || !rootView.isShown()) { + continue; + } + + WindowManager.LayoutParams layoutParams = params[i]; + int[] location = new int[2]; + rootView.getLocationOnScreen(location); + + int left = location[0]; + int top = location[1]; + int width = rootView.getWidth(); + int height = rootView.getHeight(); + + // For dialogs/bottom sheets, find actual content bounds + if (layoutParams.type == WindowManager.LayoutParams.TYPE_APPLICATION) { + View contentView = findBottomSheetContent(rootView, 0); + if (contentView != null && contentView != rootView) { + int[] childLocation = new int[2]; + contentView.getLocationOnScreen(childLocation); + left = childLocation[0]; + top = childLocation[1]; + width = contentView.getWidth(); + height = contentView.getHeight(); + } + } + + Rect area = new Rect(left, top, left + width, top + height); + rootViews.add(new ViewRootData(rootView, area, layoutParams, root)); + } + + return rootViews; + } + + /** + * Recursively searches for bottom sheet content view. + * Bottom sheets typically have a non-zero top position from screen. + */ + private static View findBottomSheetContent(View view, int depth) { + if (depth > 10) return null; // Prevent deep recursion + + int[] location = new int[2]; + view.getLocationOnScreen(location); + + if (location[1] > 100) { // Bottom sheets typically start below y=100 + return view; + } + + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View child = viewGroup.getChildAt(i); + if (child != null && child.getVisibility() == View.VISIBLE) { + View result = findBottomSheetContent(child, depth + 1); + if (result != null) { + return result; + } + } + } + } + + return null; + } + + /** + * Normalizes all root view coordinates so top-left starts at (0,0). + * Required for consistent positioning across different screen configurations. + */ + private static void offsetRootsTopLeft(List rootViews) { + int minTop = Integer.MAX_VALUE; + int minLeft = Integer.MAX_VALUE; + + for (ViewRootData rootView : rootViews) { + minTop = Math.min(minTop, rootView._winFrame.top); + minLeft = Math.min(minLeft, rootView._winFrame.left); + } + + for (ViewRootData rootView : rootViews) { + rootView._winFrame.offset(-minLeft, -minTop); + } + } + + /** + * Sorts view roots so activities come before their dialogs. + * Ensures correct capture order when multiple windows belong to same activity. + */ + private static void ensureDialogsAreAfterActivities(List viewRoots) { + if (viewRoots.size() <= 1) { + return; + } + + for (int dialogIndex = 0; dialogIndex < viewRoots.size() - 1; dialogIndex++) { + ViewRootData viewRoot = viewRoots.get(dialogIndex); + if (!viewRoot.isDialogType() || viewRoot.getWindowToken() == null) { + continue; + } + + for (int parentIndex = dialogIndex + 1; parentIndex < viewRoots.size(); parentIndex++) { + ViewRootData possibleParent = viewRoots.get(parentIndex); + if (possibleParent.isActivityType() + && possibleParent.getWindowToken() == viewRoot.getWindowToken()) { + viewRoots.remove(possibleParent); + viewRoots.add(dialogIndex, possibleParent); + break; + } + } + } + } + + /** + * Safely gets a field value using reflection, returns null on failure. + */ + private static Object getFieldValueSafe(String fieldName, Object target) { + try { + Field field = findField(fieldName, target.getClass()); + field.setAccessible(true); + return field.get(target); + } catch (Exception e) { + Log.w(TAG, "Failed to get field " + fieldName, e); + return null; + } + } + + /** + * Finds a field in a class hierarchy. + */ + private static Field findField(String name, Class clazz) throws NoSuchFieldException { + Class currentClass = clazz; + while (currentClass != null && currentClass != Object.class) { + for (Field field : currentClass.getDeclaredFields()) { + if (name.equals(field.getName())) { + return field; + } + } + currentClass = currentClass.getSuperclass(); + } + throw new NoSuchFieldException("Field " + name + " not found for class " + clazz); + } + + //region Data Classes & Interfaces + + /** + * Contains information about a view root in the window hierarchy. + * Each window (activity, dialog, bottom sheet) has its own ViewRoot. + */ + static class ViewRootData { + final View _view; + final Rect _winFrame; + final Rect _originalWinFrame; + final WindowManager.LayoutParams _layoutParams; + final Object _viewRoot; + + ViewRootData(View view, Rect winFrame, WindowManager.LayoutParams layoutParams, + Object viewRoot) { + _view = view; + _winFrame = winFrame; + _originalWinFrame = new Rect(winFrame); + _layoutParams = layoutParams; + _viewRoot = viewRoot; + } + + /** + * Returns true if this is a dialog or bottom sheet window. + */ + boolean isDialogType() { + return _layoutParams.type == WindowManager.LayoutParams.TYPE_APPLICATION; + } + + /** + * Returns true if this is the main activity window. + */ + boolean isActivityType() { + return _layoutParams.type == WindowManager.LayoutParams.TYPE_BASE_APPLICATION; + } + + /** + * Returns the window token for matching dialogs to their parent activities. + */ + IBinder getWindowToken() { + return _layoutParams.token; + } + + + /** + * Gets the Window associated with this view root. + * Tries multiple approaches to find the window. + */ + @Nullable + Window getWindow() { + // Try to get from context + Context context = _view.getContext(); + + if (context instanceof Activity) { + return ((Activity) context).getWindow(); + } + + // Unwrap ContextWrapper to find Activity + Context ctx = context; + while (ctx instanceof ContextWrapper && !(ctx instanceof Activity)) { + ctx = ((ContextWrapper) ctx).getBaseContext(); + if (ctx == null) break; + } + + if (ctx instanceof Activity) { + return ((Activity) ctx).getWindow(); + } + + return null; + } + } + + /** + * Callback interface for asynchronous screenshot capture. + */ + interface CaptureCallback { + /** + * Called when screenshot capture completes. + * + * @param bitmap the captured bitmap, or null if capture failed + */ + void onCaptureComplete(@Nullable Bitmap bitmap); + } + + /** + * Callback interface for receiving multiple captured bitmaps. + */ + interface MultiBitmapCallback { + /** + * Called when all screenshots have been captured. + * + * @param bitmaps list of captured bitmaps, one per window, or null if capture failed + */ + void onCaptureComplete(@Nullable List bitmaps); + } + + //endregion +} diff --git a/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java b/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java index 33db569..dc5bed2 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java @@ -26,12 +26,12 @@ import android.hardware.SensorManager; import android.net.Uri; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.widget.LinearLayoutManager; @@ -42,6 +42,7 @@ import com.squareup.seismic.ShakeDetector; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -53,6 +54,7 @@ */ public class Shaky implements ShakeDetector.Listener { + private static final String TAG = "Shaky"; private static final String SEND_FEEDBACK_TAG = "SendFeedback"; private static final String COLLECT_DATA_TAG = "CollectFeedbackData"; private static final String CUSTOM_DIALOG_TAG = "CustomDialog"; @@ -191,7 +193,7 @@ private void doStartFeedbackFlow() { shakyFlowCallback.onCollectingData(); } collectDataTask = new CollectDataTask(activity, delegate, createCallback()); - collectDataTask.execute(getScreenshotBitmap()); + getScreenshotBitmap(); } /** @@ -373,18 +375,54 @@ boolean canStartFeedbackFlow() { return canStart; } - @Nullable - @UiThread - private Bitmap getScreenshotBitmap() { + /** + * Captures screenshot(s) as a Bitmap. This method implements a 3 step approach to capture the + * screenshot(s): + * 1. `Falcon` - Fast single-bitmap capture of the entire screen (Canvas-based). However, fails + * if UI contains hardware bitmaps. + * 2. `PixelCopy` multi-window - Captures each window separately to handle hardware bitmaps. + * 3. If both Falcon and PixelCopy fails, {@link Utils#capture} fallback - Main activity only + * (PixelCopy on API 26+, Canvas on older APIs). + */ + private void getScreenshotBitmap() { try { - // Attempt to use Falcon to take the screenshot - return Falcon.takeScreenshotBitmap(activity); - } catch (Falcon.UnableToTakeScreenshotException exception) { - // Fallback to using the default screenshot capture mechanism if Falcon does not work (e.g. if it has not - // been updated to work on newer versions of Android yet) - View view = activity.getWindow().getDecorView().getRootView(); - return Utils.capture(view, activity.getWindow()); + Bitmap bitmap = Falcon.takeScreenshotBitmap(activity); + if (bitmap != null) { + collectDataTask.execute(bitmap); + return; + } + } catch (Exception exception) { + Log.e(TAG, "Falcon failed: " + exception.getMessage() + ", falling back to PixelCopy"); + } + + // Falcon failed - use PixelCopy to capture all windows separately + MultiWindowScreenshotCapture.captureMultipleAsync(activity, (List bitmaps) -> { + if (bitmaps != null && !bitmaps.isEmpty()) { + Log.i(TAG, "PixelCopy captured " + bitmaps.size() + " screenshot(s)"); + // Convert List to array for CollectDataTask + Bitmap[] bitmapArray = bitmaps.toArray(new Bitmap[0]); + // Pass the captured bitmaps as params to CollectDataTask + collectDataTask.execute(bitmapArray); + } else { + // Both Falcon and PixelCopy failed - try final fallback + Log.e(TAG, "PixelCopy failed, falling back to Canvas"); + captureWithCanvas(); + } + }); + } + + /** + * Final fallback when all other screenshot methods fail. + * Attempts to capture at least the main window via {@link Utils#capture}. + */ + private void captureWithCanvas() { + View view = activity.getWindow().getDecorView().getRootView(); + Bitmap bitmap = Utils.capture(view, activity.getWindow()); + if (bitmap == null) { + Log.e(TAG, "Screenshot capture failed"); } + // Execute task regardless - user can still submit feedback without screenshot + collectDataTask.execute(bitmap); } private void dismissCollectFeedbackDialogIfNecessary() { From 7fa87e60d78f6762878a454105de8e21e4ecb77a Mon Sep 17 00:00:00 2001 From: Mansi Singh Date: Thu, 8 Jan 2026 19:54:43 +0530 Subject: [PATCH 2/2] Added a flag to control the multiwindow capture feature --- .../linkedin/android/shaky/ShakeDelegate.java | 13 +++++ .../com/linkedin/android/shaky/Shaky.java | 48 +++++++++++-------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java b/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java index 791ff2e..99fe129 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java @@ -183,6 +183,19 @@ public boolean shouldUseBottomSheet() { return true; } + /** + * Controls whether multi-window screenshot capture is enabled. + * When enabled, uses PixelCopy API to capture multiple windows (activity, dialogs, bottom sheets) + * separately. This handles hardware bitmaps correctly but may result in multiple screenshot attachments. + * When disabled (default), uses the original single-screenshot fallback behavior for backward + * compatibility. + * + * @return true to enable multi-window capture, false to use single-screenshot fallback (default) + */ + public boolean enableMultiWindowCapture() { + return false; + } + /** * Called when the user submits the Feedback form. Creates and starts an email Intent. * This method can be overridden to send data to a custom URL endpoint, etc. diff --git a/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java b/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java index dc5bed2..0b114bf 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java @@ -376,13 +376,15 @@ boolean canStartFeedbackFlow() { } /** - * Captures screenshot(s) as a Bitmap. This method implements a 3 step approach to capture the - * screenshot(s): + * Captures screenshot(s) as a Bitmap. This method implements a multi-step approach to capture + * screenshots: * 1. `Falcon` - Fast single-bitmap capture of the entire screen (Canvas-based). However, fails * if UI contains hardware bitmaps. - * 2. `PixelCopy` multi-window - Captures each window separately to handle hardware bitmaps. - * 3. If both Falcon and PixelCopy fails, {@link Utils#capture} fallback - Main activity only - * (PixelCopy on API 26+, Canvas on older APIs). + * 2. If {@link ShakeDelegate#enableMultiWindowCapture()} is enabled: + * - `PixelCopy` multi-window - Captures each window separately to handle hardware bitmaps. + * - Falls back to {@link Utils#capture} if PixelCopy fails. + * 3. If multi-window capture is disabled (default): + * - {@link Utils#capture} fallback - Main activity only. */ private void getScreenshotBitmap() { try { @@ -392,23 +394,29 @@ private void getScreenshotBitmap() { return; } } catch (Exception exception) { - Log.e(TAG, "Falcon failed: " + exception.getMessage() + ", falling back to PixelCopy"); + Log.e(TAG, "Falcon failed: " + exception.getMessage() + ", falling back"); } - // Falcon failed - use PixelCopy to capture all windows separately - MultiWindowScreenshotCapture.captureMultipleAsync(activity, (List bitmaps) -> { - if (bitmaps != null && !bitmaps.isEmpty()) { - Log.i(TAG, "PixelCopy captured " + bitmaps.size() + " screenshot(s)"); - // Convert List to array for CollectDataTask - Bitmap[] bitmapArray = bitmaps.toArray(new Bitmap[0]); - // Pass the captured bitmaps as params to CollectDataTask - collectDataTask.execute(bitmapArray); - } else { - // Both Falcon and PixelCopy failed - try final fallback - Log.e(TAG, "PixelCopy failed, falling back to Canvas"); - captureWithCanvas(); - } - }); + // Falcon failed - check if multi-window capture is enabled + if (delegate.enableMultiWindowCapture()) { + // Use PixelCopy to capture all windows separately + MultiWindowScreenshotCapture.captureMultipleAsync(activity, (List bitmaps) -> { + if (bitmaps != null && !bitmaps.isEmpty()) { + Log.i(TAG, "PixelCopy captured " + bitmaps.size() + " screenshot(s)"); + // Convert List to array for CollectDataTask + Bitmap[] bitmapArray = bitmaps.toArray(new Bitmap[0]); + // Pass the captured bitmaps as params to CollectDataTask + collectDataTask.execute(bitmapArray); + } else { + // PixelCopy failed - try final fallback + Log.e(TAG, "PixelCopy failed, falling back to Canvas"); + captureWithCanvas(); + } + }); + } else { + // Multi-window capture disabled - use single-screenshot fallback + captureWithCanvas(); + } } /**