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 5bedde4..b508913 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
@@ -11,6 +11,7 @@
*/
package com.linkedin.android.shaky.app;
+import androidx.fragment.app.FragmentActivity;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
@@ -24,7 +25,6 @@
import androidx.fragment.app.FragmentActivity;
import com.linkedin.android.shaky.ActionConstants;
-import com.linkedin.android.shaky.Shaky;
import java.util.Random;
@@ -36,9 +36,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,23 +94,5 @@ public void onClick(View v) {
.startFeedbackFlow(ActionConstants.ACTION_START_BUG_REPORT);
}
});
-
- findViewById(R.id.demo_bottom_sheet_button).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- ((ShakyApplication) getApplication()).getShaky().startShakeBottomSheetFlowManually();
- }
- });
-
- ((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 eb678e2..b4f9dfb 100644
--- a/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java
+++ b/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java
@@ -28,12 +28,12 @@
import android.os.Bundle;
import android.view.LayoutInflater;
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 androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -62,14 +62,7 @@ public class Shaky implements ShakeDetector.Listener {
private final ShakeDetector shakeDetector;
@Nullable
private final ShakyFlowCallback shakyFlowCallback;
-
- // Screen capture related fields
- private ScreenCaptureManager screenCaptureManager;
- private boolean useMediaProjection = false;
- private boolean isScreenCaptureInProgress = false;
- private boolean isBottomSheetFlowActive = false;
- private CollectDataTask.Callback pendingDataCollectionCallback = null;
-
+
@Nullable
private Activity activity;
private Context appContext;
@@ -85,9 +78,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);
@@ -163,85 +153,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());
}
/**
@@ -410,31 +334,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();
- }
}
/**
@@ -485,8 +393,6 @@ private CollectDataTask.Callback createCallback() {
public void onDataReady(@Nullable Result result) {
boolean shouldStartFeedbackActivity = activity != null && collectDataTask != null;
collectDataTask = null;
- dismissCollectFeedbackDialogIfNecessary();
- Result safeResult = result != null ? result : new Result();
if (shouldStartFeedbackActivity && !isBottomSheetFlowActive) {
startFeedbackActivity(safeResult);