Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support masking private views #516

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## [13.4.0](https://github.com/Instabug/Instabug-Flutter/compare/v13.3.0...v13.4.0) (September 29, 2024)
## [Unreleased](https://github.com/Instabug/Instabug-Flutter/compare/v13.4.0...dev)

### Added

Expand Down
8 changes: 4 additions & 4 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ rootProject.allprojects {
apply plugin: 'com.android.library'

android {
compileSdkVersion 28
compileSdkVersion 33

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
Expand All @@ -41,10 +41,10 @@ android {
}

dependencies {
api 'com.instabug.library:instabug:13.4.1'

api 'com.instabug.library:instabug:13.4.1.6295791-SNAPSHOT'
testImplementation 'org.robolectric:robolectric:4.12.2'
testImplementation 'junit:junit:4.13.2'
testImplementation "org.mockito:mockito-inline:3.12.1"
testImplementation "org.mockito:mockito-inline:5.0.0"
}

// add upload_symbols task
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.instabug.flutter.generated.InstabugPrivateViewPigeon;
import com.instabug.flutter.modules.ApmApi;
import com.instabug.flutter.modules.BugReportingApi;
import com.instabug.flutter.modules.CrashReportingApi;
Expand All @@ -19,31 +20,36 @@
import com.instabug.flutter.modules.RepliesApi;
import com.instabug.flutter.modules.SessionReplayApi;
import com.instabug.flutter.modules.SurveysApi;

import java.util.concurrent.Callable;
import com.instabug.flutter.util.privateViews.BoundryCaptureManager;
import com.instabug.flutter.util.privateViews.PixelCopyCaptureManager;
import com.instabug.flutter.util.privateViews.PrivateViewManager;
import com.instabug.library.internal.crossplatform.InternalCore;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.plugin.common.PluginRegistry;

public class InstabugFlutterPlugin implements FlutterPlugin, ActivityAware {
private static final String TAG = InstabugFlutterPlugin.class.getName();

@SuppressLint("StaticFieldLeak")
private static Activity activity;

PrivateViewManager privateViewManager;

/**
* Embedding v1
*/
@SuppressWarnings("deprecation")
public static void registerWith(Registrar registrar) {
public static void registerWith(PluginRegistry.Registrar registrar) {
activity = registrar.activity();
register(registrar.context().getApplicationContext(), registrar.messenger(), (FlutterRenderer) registrar.textures());
}


@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
register(binding.getApplicationContext(), binding.getBinaryMessenger(), (FlutterRenderer) binding.getTextureRegistry());
Expand All @@ -57,55 +63,44 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
activity = binding.getActivity();
if (privateViewManager != null) {
privateViewManager.setActivity(activity);
}

}

@Override
public void onDetachedFromActivityForConfigChanges() {
activity = null;
privateViewManager.setActivity(null);

}

@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
activity = binding.getActivity();
privateViewManager.setActivity(activity);

}

@Override
public void onDetachedFromActivity() {
activity = null;
ahmedAlaaInstabug marked this conversation as resolved.
Show resolved Hide resolved
privateViewManager.setActivity(null);

}

private static void register(Context context, BinaryMessenger messenger, FlutterRenderer renderer) {
final Callable<Bitmap> screenshotProvider = new Callable<Bitmap>() {
@Override
public Bitmap call() {
return takeScreenshot(renderer);
}
};

ApmApi.init(messenger);
BugReportingApi.init(messenger);
CrashReportingApi.init(messenger);
FeatureRequestsApi.init(messenger);
InstabugApi.init(messenger, context, screenshotProvider);
privateViewManager = new PrivateViewManager(new InstabugPrivateViewPigeon.InstabugPrivateViewApi(messenger), new PixelCopyCaptureManager(), new BoundryCaptureManager(renderer));
InstabugApi.init(messenger, context, privateViewManager, InternalCore.INSTANCE);
InstabugLogApi.init(messenger);
RepliesApi.init(messenger);
SessionReplayApi.init(messenger);
SurveysApi.init(messenger);
}

@Nullable
private static Bitmap takeScreenshot(FlutterRenderer renderer) {
try {
final View view = activity.getWindow().getDecorView().getRootView();

view.setDrawingCacheEnabled(true);
final Bitmap bitmap = renderer.getBitmap();
view.setDrawingCacheEnabled(false);

return bitmap;
} catch (Exception e) {
Log.e(TAG, "Failed to take screenshot using " + renderer.toString() + ". Cause: " + e);
return null;
}
}
}
33 changes: 23 additions & 10 deletions android/src/main/java/com/instabug/flutter/modules/InstabugApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.instabug.flutter.util.ArgsRegistry;
import com.instabug.flutter.util.Reflection;
import com.instabug.flutter.util.ThreadManager;
import com.instabug.flutter.util.privateViews.PrivateViewManager;
import com.instabug.library.Feature;
import com.instabug.library.Instabug;
import com.instabug.library.InstabugColorTheme;
Expand All @@ -21,13 +22,14 @@
import com.instabug.library.Platform;
import com.instabug.library.ReproConfigurations;
import com.instabug.library.featuresflags.model.IBGFeatureFlag;
import com.instabug.library.internal.crossplatform.InternalCore;
import com.instabug.library.internal.module.InstabugLocale;
import com.instabug.library.invocation.InstabugInvocationEvent;
import com.instabug.library.model.NetworkLog;
import com.instabug.library.screenshot.ScreenshotCaptor;
import com.instabug.library.screenshot.instacapture.ScreenshotRequest;
import com.instabug.library.ui.onboarding.WelcomeMessage;
import io.flutter.FlutterInjector;
import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.plugin.common.BinaryMessenger;

import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;

Expand All @@ -40,22 +42,27 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;

import io.flutter.FlutterInjector;
import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.plugin.common.BinaryMessenger;

public class InstabugApi implements InstabugPigeon.InstabugHostApi {
private final String TAG = InstabugApi.class.getName();
private final Context context;
private final Callable<Bitmap> screenshotProvider;
private final InstabugCustomTextPlaceHolder placeHolder = new InstabugCustomTextPlaceHolder();
private final PrivateViewManager privateViewManager;
private final InternalCore internalCore;

public static void init(BinaryMessenger messenger, Context context, Callable<Bitmap> screenshotProvider) {
final InstabugApi api = new InstabugApi(context, screenshotProvider);
public static void init(BinaryMessenger messenger, Context context, PrivateViewManager privateViewManager, InternalCore internalCore) {
final InstabugApi api = new InstabugApi(context, privateViewManager, internalCore);
InstabugPigeon.InstabugHostApi.setup(messenger, api);
}

public InstabugApi(Context context, Callable<Bitmap> screenshotProvider) {
public InstabugApi(Context context, PrivateViewManager privateViewManager, InternalCore internalCore) {
this.context = context;
this.screenshotProvider = screenshotProvider;
this.privateViewManager = privateViewManager;
this.internalCore = internalCore;
}

@VisibleForTesting
Expand Down Expand Up @@ -112,7 +119,13 @@ public void init(@NonNull String token, @NonNull List<String> invocationEvents,
.setSdkDebugLogsLevel(parsedLogLevel)
.build();

Instabug.setScreenshotProvider(screenshotProvider);
internalCore._setScreenshotCaptor(new ScreenshotCaptor() {
@Override
public void capture(@NonNull ScreenshotRequest screenshotRequest) {
privateViewManager.mask(screenshotRequest.getListener());
}
});
Comment on lines +122 to +127
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the privateViewManager frequently switching to the main thread through ThreadManager.runOnMainThread, are there possibilities of the screenshot captor being called on a background thread?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noo,, access flutter view need to be on the main thread


}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.instabug.flutter.util.privateViews;

import android.app.Activity;
import android.graphics.Bitmap;
import android.util.DisplayMetrics;
import android.view.View;

import com.instabug.flutter.util.ThreadManager;

import io.flutter.embedding.engine.renderer.FlutterRenderer;

public class BoundryCaptureManager implements CaptureManager {
FlutterRenderer renderer;

public BoundryCaptureManager(FlutterRenderer renderer) {
this.renderer = renderer;
}

@Override
public void capture(Activity activity, ScreenshotResultCallback screenshotResultCallback) {
ThreadManager.runOnMainThread(new Runnable() {
@Override
public void run() {
try {
if (activity == null) {
screenshotResultCallback.onError();
return;
}
View rootView = activity.getWindow().getDecorView().getRootView();
rootView.setDrawingCacheEnabled(true);
Bitmap bitmap = renderer.getBitmap();
rootView.setDrawingCacheEnabled(false);
DisplayMetrics displayMetrics = activity.getResources().getDisplayMetrics();
screenshotResultCallback.onScreenshotResult(new ScreenshotResult(displayMetrics.density, bitmap));

} catch (Exception e) {
screenshotResultCallback.onError();
}
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.instabug.flutter.util.privateViews;

import android.app.Activity;

public interface CaptureManager {
void capture(Activity activity, ScreenshotResultCallback screenshotResultCallback);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.instabug.flutter.util.privateViews;

import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.view.PixelCopy;
import android.view.SurfaceView;

import androidx.annotation.RequiresApi;

import com.instabug.library.util.memory.MemoryUtils;

import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.android.FlutterFragment;
import io.flutter.embedding.android.FlutterView;

public class PixelCopyCaptureManager implements CaptureManager {

@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void capture(Activity activity, ScreenshotResultCallback screenshotResultCallback) {
FlutterView flutterView = getFlutterView(activity);
if (flutterView == null || !isValidFlutterView(flutterView)) {
screenshotResultCallback.onError();
return;
}

SurfaceView surfaceView = (SurfaceView) flutterView.getChildAt(0);
Bitmap bitmap = createBitmapFromSurface(surfaceView);

if (bitmap == null) {
screenshotResultCallback.onError();
return;
}

PixelCopy.request(surfaceView, bitmap, copyResult -> {
if (copyResult == PixelCopy.SUCCESS) {
DisplayMetrics displayMetrics = activity.getResources().getDisplayMetrics();
screenshotResultCallback.onScreenshotResult(new ScreenshotResult(displayMetrics.density, bitmap));
} else {
screenshotResultCallback.onError();
}
}, new Handler(Looper.getMainLooper()));
}

private FlutterView getFlutterView(Activity activity) {
FlutterView flutterViewInActivity = activity.findViewById(FlutterActivity.FLUTTER_VIEW_ID);
FlutterView flutterViewInFragment = activity.findViewById(FlutterFragment.FLUTTER_VIEW_ID);
return flutterViewInActivity != null ? flutterViewInActivity : flutterViewInFragment;
}

private boolean isValidFlutterView(FlutterView flutterView) {
boolean hasChildren = flutterView.getChildCount() > 0;
boolean isSurfaceView = flutterView.getChildAt(0) instanceof SurfaceView;
return hasChildren && isSurfaceView;
}

private Bitmap createBitmapFromSurface(SurfaceView surfaceView) {
int width = surfaceView.getWidth();
int height = surfaceView.getHeight();

if (width <= 0 || height <= 0) {
return null;
}
Bitmap bitmap;
try {
if (((long) width * height * 4) < MemoryUtils.getFreeMemory(surfaceView.getContext())) {
// ARGB_8888 store each pixel in 4 bytes
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
} else {
// RGB_565 store each pixel in 2 bytes
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
}

} catch (IllegalArgumentException | OutOfMemoryError e) {
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
}


return bitmap;
}
}
Loading