From d2db9428253276c81b6507b2bbef7fc729ed4f76 Mon Sep 17 00:00:00 2001 From: Advait Mahashabde Date: Fri, 5 Sep 2025 16:35:24 -0700 Subject: [PATCH] Remove code to use MEDIA_PROJECTION_API --- .../linkedin/android/shaky/app/ShakyDemo.java | 16 -- shaky/src/main/AndroidManifest.xml | 9 - .../android/shaky/ScreenCaptureManager.java | 236 ------------------ .../com/linkedin/android/shaky/Shaky.java | 110 +------- 4 files changed, 9 insertions(+), 362 deletions(-) delete mode 100644 shaky/src/main/java/com/linkedin/android/shaky/ScreenCaptureManager.java diff --git a/shaky-sample/src/main/java/com/linkedin/android/shaky/app/ShakyDemo.java b/shaky-sample/src/main/java/com/linkedin/android/shaky/app/ShakyDemo.java index cec4827..6cce10c 100644 --- a/shaky-sample/src/main/java/com/linkedin/android/shaky/app/ShakyDemo.java +++ b/shaky-sample/src/main/java/com/linkedin/android/shaky/app/ShakyDemo.java @@ -12,7 +12,6 @@ package com.linkedin.android.shaky.app; import androidx.fragment.app.FragmentActivity; -import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.view.View; @@ -24,7 +23,6 @@ import androidx.core.view.WindowInsetsCompat; import com.linkedin.android.shaky.ActionConstants; -import com.linkedin.android.shaky.Shaky; import java.util.Random; @@ -36,9 +34,6 @@ public class ShakyDemo extends FragmentActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_demo); - - // Initialize ScreenCaptureManager for the Shaky library - ((ShakyApplication) getApplication()).getShaky().initializeScreenCapture(this); ViewCompat.setOnApplyWindowInsetsListener( getWindow().findViewById(R.id.demo_background), @@ -97,16 +92,5 @@ public void onClick(View v) { .startFeedbackFlow(ActionConstants.ACTION_START_BUG_REPORT); } }); - - ((ShakyApplication)getApplication()).getShaky().setUseMediaProjection(true); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - Shaky shaky = ((ShakyApplication) getApplication()).getShaky(); - if (!shaky.handleActivityResult(requestCode, resultCode, data)) { - // If Shaky did not handle the result, call the super method - super.onActivityResult(requestCode, resultCode, data); - } } } diff --git a/shaky/src/main/AndroidManifest.xml b/shaky/src/main/AndroidManifest.xml index 4a8a030..e96385e 100644 --- a/shaky/src/main/AndroidManifest.xml +++ b/shaky/src/main/AndroidManifest.xml @@ -18,17 +18,8 @@ - - - - - diff --git a/shaky/src/main/java/com/linkedin/android/shaky/ScreenCaptureManager.java b/shaky/src/main/java/com/linkedin/android/shaky/ScreenCaptureManager.java deleted file mode 100644 index 2625f2e..0000000 --- a/shaky/src/main/java/com/linkedin/android/shaky/ScreenCaptureManager.java +++ /dev/null @@ -1,236 +0,0 @@ -package com.linkedin.android.shaky; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -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.media.projection.MediaProjectionManager; -import android.os.Handler; -import android.view.Display; -import android.view.WindowManager; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.fragment.app.FragmentActivity; - -import java.nio.ByteBuffer; - -/** - * Manages screen capture operations using MediaProjection API. - * This class handles the permission request and screen capturing process. - */ -public class ScreenCaptureManager { - private Context appContext; - private MediaProjectionManager projectionManager; - private MediaProjection mediaProjection; - private VirtualDisplay virtualDisplay; - private ImageReader imageReader; - private int screenWidth; - private int screenHeight; - private int screenDensity; - - private CaptureCallback captureCallback; - private ActivityResultLauncher mediaProjectionLauncher; - - public interface CaptureCallback { - void onCaptureComplete(Bitmap bitmap); - void onCaptureFailed(); - } - - public ScreenCaptureManager(Context context) { - this.appContext = context.getApplicationContext(); - projectionManager = (MediaProjectionManager) - appContext.getSystemService(Context.MEDIA_PROJECTION_SERVICE); - } - - /** - * Initialize the activity result launcher. This must be called in the activity's onCreate method - * before any fragments are created or the activity is started. - * - * @param activity The FragmentActivity to register the launcher with - */ - public void initialize(FragmentActivity activity) { - mediaProjectionLauncher = activity.registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - this::handleActivityResult - ); - } - - /** - * Request screen capture permission and start the capturing process. - * This needs to be called from an Activity after initialize() has been called. - * - * @param activity Activity to request permission from - * @param callback Callback to receive the captured screenshot or failure - */ - public void requestCapturePermission(Activity activity, CaptureCallback callback) { - if (activity == null || callback == null) { - return; - } - if (mediaProjectionLauncher == null) { - throw new IllegalStateException("ScreenCaptureManager must be initialized before requesting permission. Call initialize() in onCreate()."); - } - - this.captureCallback = callback; - - // Get screen metrics - WindowManager windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE); - Display display = windowManager.getDefaultDisplay(); - android.util.DisplayMetrics metrics = new android.util.DisplayMetrics(); - display.getMetrics(metrics); - - screenWidth = metrics.widthPixels; - screenHeight = metrics.heightPixels; - screenDensity = metrics.densityDpi; - - // Request permission using the modern Activity Result API - Intent intent = projectionManager.createScreenCaptureIntent(); - mediaProjectionLauncher.launch(intent); - } - - /** - * Handle activity result from permission request using the modern Activity Result API. - * - * @param result ActivityResult from the launcher - */ - private void handleActivityResult(ActivityResult result) { - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - // Start the foreground service before creating MediaProjection - ScreenCaptureService.start(appContext); - - // Wait briefly to ensure the service is fully started - new Handler().postDelayed(() -> { - startCapture(result.getResultCode(), result.getData()); - }, 100); // Small delay to ensure service starts - } else { - if (captureCallback != null) { - captureCallback.onCaptureFailed(); - } - } - } - - /** - * @deprecated Use the modern Activity Result API instead. This method is kept for backwards compatibility - * but may not work reliably on newer Android versions. - */ - @Deprecated - public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { - // This is kept for backwards compatibility but should not be used - // The modern approach using ActivityResultLauncher is more reliable - return false; - } - - private final MediaProjection.Callback mediaProjectionCallback = new MediaProjection.Callback() { - @Override - public void onStop() { - stopCapture(); - } - }; - - private void startCapture(int resultCode, Intent data) { - mediaProjection = projectionManager.getMediaProjection(resultCode, data); - // Register callback to manage resources - mediaProjection.registerCallback(mediaProjectionCallback, new Handler()); - - imageReader = ImageReader.newInstance( - screenWidth, screenHeight, - PixelFormat.RGBA_8888, 2); - - virtualDisplay = mediaProjection.createVirtualDisplay( - "Shaky Screenshot", - screenWidth, screenHeight, screenDensity, - DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, - imageReader.getSurface(), null, null); - - // Capture the image with a delay to ensure display is ready - new Handler().postDelayed(() -> captureImage(), 100); - } - - private void captureImage() { - if (imageReader == null) { - if (captureCallback != null) { - captureCallback.onCaptureFailed(); - } - stopCapture(); - return; - } - - Image image = null; - try { - image = imageReader.acquireLatestImage(); - if (image == null) { - if (captureCallback != null) { - captureCallback.onCaptureFailed(); - } - stopCapture(); - return; - } - - final Image.Plane[] planes = image.getPlanes(); - final ByteBuffer buffer = planes[0].getBuffer(); - - int pixelStride = planes[0].getPixelStride(); - int rowStride = planes[0].getRowStride(); - int rowPadding = rowStride - pixelStride * screenWidth; - - // Create bitmap - Bitmap bitmap = Bitmap.createBitmap( - screenWidth + rowPadding / pixelStride, - screenHeight, - Bitmap.Config.ARGB_8888); - bitmap.copyPixelsFromBuffer(buffer); - - // Crop if needed due to rowPadding - Bitmap croppedBitmap = Bitmap.createBitmap( - bitmap, 0, 0, screenWidth, screenHeight); - - if (bitmap != croppedBitmap) { - bitmap.recycle(); - } - - if (captureCallback != null) { - captureCallback.onCaptureComplete(croppedBitmap); - } - - } catch (Exception e) { - if (captureCallback != null) { - captureCallback.onCaptureFailed(); - } - } finally { - if (image != null) { - image.close(); - } - stopCapture(); - } - } - - private void stopCapture() { - if (virtualDisplay != null) { - virtualDisplay.release(); - virtualDisplay = null; - } - - if (mediaProjection != null) { - mediaProjection.stop(); - mediaProjection = null; - } - - if (imageReader != null) { - imageReader.close(); - imageReader = null; - } - - // Stop the foreground service - ScreenCaptureService.stop(appContext); - } -} 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 7b603ce..2a8581b 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java @@ -27,12 +27,12 @@ import android.net.Uri; import android.os.Bundle; import android.view.View; +import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; -import androidx.fragment.app.FragmentActivity; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.jraska.falcon.Falcon; @@ -60,12 +60,6 @@ public class Shaky implements ShakeDetector.Listener { @Nullable private final ShakyFlowCallback shakyFlowCallback; - // Screen capture related fields - private ScreenCaptureManager screenCaptureManager; - private boolean useMediaProjection = false; - private boolean isScreenCaptureInProgress = false; - private CollectDataTask.Callback pendingDataCollectionCallback = null; - @Nullable private Activity activity; private Context appContext; @@ -81,9 +75,6 @@ public class Shaky implements ShakeDetector.Listener { shakeDetector.setSensitivity(getDetectorSensitivityLevel()); - // Initialize the screen capture manager - screenCaptureManager = new ScreenCaptureManager(context); - IntentFilter filter = new IntentFilter(); filter.addAction(ActionConstants.ACTION_START_FEEDBACK_FLOW); filter.addAction(ActionConstants.ACTION_START_BUG_REPORT); @@ -158,85 +149,19 @@ void setActivity(@Nullable Activity activity) { this.activity = activity; if (activity != null) { start(); - // we're attaching to a new Activity instance - // make sure the UI is in sync with the AsyncTask state - dismissCollectFeedbackDialogIfNecessary(); } else { stop(); } } - /** - * Enable or disable MediaProjection API for taking screenshots. - * When enabled, Shaky will capture screenshots that include system overlays. - * - * @param useMediaProjection true to use MediaProjection, false to use default screenshot method - */ - public void setUseMediaProjection(boolean useMediaProjection) { - this.useMediaProjection = useMediaProjection; - } - - /** - * Initialize the ScreenCaptureManager for MediaProjection API. - * This must be called from the host activity's onCreate method before any screen capture operations. - * - * @param activity The FragmentActivity that will handle the MediaProjection permission flow - */ - public void initializeScreenCapture(FragmentActivity activity) { - if (screenCaptureManager != null) { - screenCaptureManager.initialize(activity); - } - } - - /** - * Handle activity result for MediaProjection permission request. - * This must be called from the host activity's onActivityResult method. - * - * @param requestCode Request code from onActivityResult - * @param resultCode Result code from onActivityResult - * @param data Intent data from onActivityResult - * @return true if handled by Shaky, false otherwise - */ - public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { - if (isScreenCaptureInProgress) { - boolean handled = screenCaptureManager.handleActivityResult(requestCode, resultCode, data); - if (handled) { - isScreenCaptureInProgress = false; - } - return handled; - } - return false; - } - private void doStartFeedbackFlow() { new CollectDataDialog().show(activity.getFragmentManager(), COLLECT_DATA_TAG); if (shakyFlowCallback != null) { shakyFlowCallback.onCollectingData(); } - if (useMediaProjection) { - // For MediaProjection API, we'll start the permission request first - isScreenCaptureInProgress = true; - screenCaptureManager.requestCapturePermission(activity, new ScreenCaptureManager.CaptureCallback() { - @Override - public void onCaptureComplete(Bitmap bitmap) { - // Once we have the bitmap, start the data collection task - collectDataTask = new CollectDataTask(activity, delegate, createCallback()); - collectDataTask.execute(bitmap); - } - - @Override - public void onCaptureFailed() { - // If capture fails, continue with normal flow (without screenshot) - collectDataTask = new CollectDataTask(activity, delegate, createCallback()); - collectDataTask.execute((Bitmap) null); - } - }); - } else { - // Use the regular screenshot method - collectDataTask = new CollectDataTask(activity, delegate, createCallback()); - collectDataTask.execute(getScreenshotBitmap()); - } + collectDataTask = new CollectDataTask(activity, delegate, createCallback()); + collectDataTask.execute(getScreenshotBitmap()); } /** @@ -345,31 +270,15 @@ boolean canStartFeedbackFlow() { @Nullable @UiThread private Bitmap getScreenshotBitmap() { - if (activity == null) { - return null; - } - - // If MediaProjection is not enabled, use the default method - if (!useMediaProjection) { + 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()); } - - // For MediaProjection, return null and handle it separately - return null; - } - - private void dismissCollectFeedbackDialogIfNecessary() { - if (collectDataTask != null || activity == null) { - return; - } - - CollectDataDialog dialog = (CollectDataDialog) activity.getFragmentManager() - .findFragmentByTag(COLLECT_DATA_TAG); - - if (dialog != null) { - dialog.dismiss(); - } } /** @@ -419,7 +328,6 @@ private CollectDataTask.Callback createCallback() { public void onDataReady(@Nullable Result result) { boolean shouldStartFeedbackActivity = activity != null && collectDataTask != null; collectDataTask = null; - dismissCollectFeedbackDialogIfNecessary(); if (shouldStartFeedbackActivity) { startFeedbackActivity(result == null ? new Result() : result);