Skip to content
Open
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,25 @@ void overlayMain() {
/// closes overlay if open
await FlutterOverlayWindow.closeOverlay();

/// broadcast data to and from overlay app
/// Broadcast data to and from overlay app.
/// This method may return `false` when invoked from the overlay while the application is closed.
/// Returns `true` if the data was sent successfully, otherwise `false`.
await FlutterOverlayWindow.shareData("Hello from the other side");

/// streams message shared between overlay and main app
FlutterOverlayWindow.overlayListener.listen((event) {
log("Current Event: $event");
});

/// Overlay status stream.
/// Emit `true` when overlay is showing, and `false` when overlay is closed.
/// Emit value only once for every state change.
/// Doesn't emit a change when the overlay is already showing and [showOverlay] is called,
/// as in this case the overlay will almost immediately reopen.
FlutterOverlayWindow.overlayStatusListener.listen((event) {
print("Overlay status: $event");
});

/// use [OverlayFlag.focusPointer] when you want to use fields that show keyboards
await FlutterOverlayWindow.showOverlay(flag: OverlayFlag.focusPointer);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package flutter.overlay.window.flutter_overlay_window;

import androidx.annotation.Nullable;
import io.flutter.plugin.common.MethodChannel;

public abstract class CachedMessageChannels {
@Nullable
public static MethodChannel mainAppMessageChannel;
@Nullable
public static MethodChannel overlayMessageChannel;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package flutter.overlay.window.flutter_overlay_window;

import android.app.Activity;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;

import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.view.WindowManager;
Expand All @@ -16,47 +16,50 @@
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationManagerCompat;

import java.util.Objects;
import java.util.Map;

import io.flutter.FlutterInjector;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterEngineCache;
import io.flutter.embedding.engine.FlutterEngineGroup;
import io.flutter.embedding.engine.FlutterEngineGroupCache;
import io.flutter.embedding.engine.dart.DartExecutor;
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.plugin.common.BasicMessageChannel;
import io.flutter.plugin.common.JSONMessageCodec;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry;

public class FlutterOverlayWindowPlugin implements
FlutterPlugin, ActivityAware, BasicMessageChannel.MessageHandler, MethodCallHandler,
FlutterPlugin, ActivityAware, MethodCallHandler,
PluginRegistry.ActivityResultListener {

private MethodChannel channel;
private Context context;
private Activity mActivity;
private BasicMessageChannel<Object> messenger;
private Result pendingResult;
final int REQUEST_CODE_FOR_OVERLAY_PERMISSION = 1248;
@Nullable
FlutterPluginBinding flutterBinding;
@Nullable
ActivityPluginBinding activityPluginBinding;
boolean isMainAppEngine;

@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
flutterBinding = flutterPluginBinding;
this.context = flutterPluginBinding.getApplicationContext();
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), OverlayConstants.CHANNEL_TAG);
channel.setMethodCallHandler(this);

messenger = new BasicMessageChannel(flutterPluginBinding.getBinaryMessenger(), OverlayConstants.MESSENGER_TAG,
JSONMessageCodec.INSTANCE);
messenger.setMessageHandler(this);

WindowSetup.messenger = messenger;
WindowSetup.messenger.setMessageHandler(this);
FlutterEngineGroup overlayEngineGroup = ensureEngineGroupCreated(context);
isMainAppEngine = flutterBinding.getEngineGroup() != overlayEngineGroup;
registerMessageChannel(isMainAppEngine);
}

@RequiresApi(api = Build.VERSION_CODES.N)
Expand All @@ -78,6 +81,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
result.error("PERMISSION", "overlay permission is not enabled", null);
return;
}

ensureEngineCreated(context);

Integer height = call.argument("height");
Integer width = call.argument("width");
String alignment = call.argument("alignment");
Expand Down Expand Up @@ -131,50 +137,121 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
} else {
result.notImplemented();
}

}

@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
WindowSetup.messenger.setMessageHandler(null);
flutterBinding = null;
unregisterMessageChannel(isMainAppEngine);
FlutterEngineGroupCache.getInstance().remove(OverlayConstants.CACHED_TAG);
}

@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
activityPluginBinding = binding;
mActivity = binding.getActivity();
if (FlutterEngineCache.getInstance().get(OverlayConstants.CACHED_TAG) == null) {
FlutterEngineGroup enn = new FlutterEngineGroup(context);
DartExecutor.DartEntrypoint dEntry = new DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"overlayMain");
FlutterEngine engine = enn.createAndRunEngine(context, dEntry);
FlutterEngineCache.getInstance().put(OverlayConstants.CACHED_TAG, engine);
}

ensureEngineCreated(context);

binding.addActivityResultListener(this);
}

@Override
public void onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity();
}

@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
this.mActivity = binding.getActivity();
onAttachedToActivity(binding);
}

@Override
public void onDetachedFromActivity() {
Objects.requireNonNull(activityPluginBinding).removeActivityResultListener(this);
activityPluginBinding = null;
mActivity = null;
}

@Override
public void onMessage(@Nullable Object message, @NonNull BasicMessageChannel.Reply reply) {
BasicMessageChannel overlayMessageChannel = new BasicMessageChannel(
FlutterEngineCache.getInstance().get(OverlayConstants.CACHED_TAG)
.getDartExecutor(),
OverlayConstants.MESSENGER_TAG, JSONMessageCodec.INSTANCE);
overlayMessageChannel.send(message, reply);
private void registerMessageChannel(boolean isMainAppEngine) {
io.flutter.plugin.common.BinaryMessenger binaryMessenger = Objects.requireNonNull(flutterBinding).getBinaryMessenger();
if(isMainAppEngine) {
registerMainAppMessageChannel(binaryMessenger);
} else {
registerOverlayMessageChannel(binaryMessenger);
}
}

private void unregisterMessageChannel(boolean isMainAppEngine) {
if(isMainAppEngine) {
if (CachedMessageChannels.mainAppMessageChannel == null) return;
CachedMessageChannels.mainAppMessageChannel.setMethodCallHandler(null);
CachedMessageChannels.mainAppMessageChannel = null;
} else {
if(CachedMessageChannels.overlayMessageChannel == null) return;
CachedMessageChannels.overlayMessageChannel.setMethodCallHandler(null);
CachedMessageChannels.overlayMessageChannel = null;
}
}

private void registerOverlayMessageChannel(io.flutter.plugin.common.BinaryMessenger overlyEngineBinaryMessenger) {
MethodChannel overlayMessageChannel = new MethodChannel(overlyEngineBinaryMessenger, OverlayConstants.MESSENGER_TAG, JSONMethodCodec.INSTANCE);
overlayMessageChannel.setMethodCallHandler((call, result) -> {
if (CachedMessageChannels.mainAppMessageChannel == null) {
result.success(false);
return;
}
CachedMessageChannels.mainAppMessageChannel.invokeMethod("message", call.arguments);
result.success(true);
});
CachedMessageChannels.overlayMessageChannel = overlayMessageChannel;
}

private void registerMainAppMessageChannel(io.flutter.plugin.common.BinaryMessenger mainAppEngineBinaryMessenger) {
MethodChannel mainAppMessageChannel = new MethodChannel(mainAppEngineBinaryMessenger, OverlayConstants.MESSENGER_TAG, JSONMethodCodec.INSTANCE);
mainAppMessageChannel.setMethodCallHandler((call, result) -> {
if (CachedMessageChannels.overlayMessageChannel == null) {
result.success(false);
return;
}
CachedMessageChannels.overlayMessageChannel.invokeMethod("message", call.arguments);
result.success(true);
});
CachedMessageChannels.mainAppMessageChannel = mainAppMessageChannel;
}

private FlutterEngineGroup ensureEngineGroupCreated(android.content.Context context) {
FlutterEngineGroup enn = FlutterEngineGroupCache.getInstance().get(OverlayConstants.CACHED_TAG);

if(enn == null) {
enn = new FlutterEngineGroup(context);
FlutterEngineGroupCache.getInstance().put(OverlayConstants.CACHED_TAG, enn);
}

return enn;
}
private void ensureEngineCreated(android.content.Context context) {
FlutterEngine engine = FlutterEngineCache.getInstance().get(OverlayConstants.CACHED_TAG);
if(engine == null) {
FlutterEngineGroup enn = ensureEngineGroupCreated(context);
DartExecutor.DartEntrypoint dEntry = new DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"overlayMain");
engine = Objects.requireNonNull(enn).createAndRunEngine(context, dEntry);
FlutterEngineCache.getInstance().put(OverlayConstants.CACHED_TAG, engine);
engine.addEngineLifecycleListener(new FlutterEngine.EngineLifecycleListener() {
@Override
public void onPreEngineRestart() {

}
@Override
public void onEngineWillDestroy() {
FlutterEngineCache.getInstance().remove(OverlayConstants.CACHED_TAG);
}
});
}
}
private boolean checkOverlayPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Settings.canDrawOverlays(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterEngineCache;
import io.flutter.plugin.common.BasicMessageChannel;
import io.flutter.plugin.common.JSONMessageCodec;
import io.flutter.plugin.common.MethodChannel;

public class OverlayService extends Service implements View.OnTouchListener {
Expand All @@ -56,7 +54,6 @@ public class OverlayService extends Service implements View.OnTouchListener {
private WindowManager windowManager = null;
private FlutterView flutterView;
private MethodChannel flutterChannel = new MethodChannel(FlutterEngineCache.getInstance().get(OverlayConstants.CACHED_TAG).getDartExecutor(), OverlayConstants.OVERLAY_TAG);
private BasicMessageChannel<Object> overlayMessageChannel = new BasicMessageChannel(FlutterEngineCache.getInstance().get(OverlayConstants.CACHED_TAG).getDartExecutor(), OverlayConstants.MESSENGER_TAG, JSONMessageCodec.INSTANCE);
private int clickableFlag = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;

Expand All @@ -79,15 +76,19 @@ public IBinder onBind(Intent intent) {
@Override
public void onDestroy() {
Log.d("OverLay", "Destroying the overlay window service");
if (windowManager != null) {
isRunning = false;
if(windowManager != null && flutterView != null) {
windowManager.removeView(flutterView);
windowManager = null;
}
if(flutterView != null) {
flutterView.detachFromFlutterEngine();
flutterView = null;
}
isRunning = false;
OverlayStatusEmitter.emitIsShowing(false);
NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(OverlayConstants.NOTIFICATION_ID);

instance = null;
}

Expand All @@ -99,20 +100,16 @@ public int onStartCommand(Intent intent, int flags, int startId) {
int startY = intent.getIntExtra("startY", OverlayConstants.DEFAULT_XY);
boolean isCloseWindow = intent.getBooleanExtra(INTENT_EXTRA_IS_CLOSE_WINDOW, false);
if (isCloseWindow) {
if (windowManager != null) {
windowManager.removeView(flutterView);
windowManager = null;
flutterView.detachFromFlutterEngine();
stopSelf();
}
isRunning = false;
stopSelf();
return START_STICKY;
}
if (windowManager != null) {
if(windowManager != null && flutterView != null) {
windowManager.removeView(flutterView);
windowManager = null;
}
if(flutterView != null) {
flutterView.detachFromFlutterEngine();
stopSelf();
flutterView = null;
}
isRunning = true;
Log.d("onStartCommand", "Service started");
Expand All @@ -139,9 +136,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
resizeOverlay(width, height, enableDrag, result);
}
});
overlayMessageChannel.setMessageHandler((message, reply) -> {
WindowSetup.messenger.send(message);
});

windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
Expand Down Expand Up @@ -174,6 +169,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
flutterView.setOnTouchListener(this);
windowManager.addView(flutterView, params);
moveOverlay(dx, dy, null);
OverlayStatusEmitter.emitIsShowing(true);
return START_STICKY;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package flutter.overlay.window.flutter_overlay_window;

public abstract class OverlayStatusEmitter {
static private final String methodName = "isShowingOverlay";
static private boolean lastEmittedStatus;

static void emitIsShowing(boolean isShowing) {
if(isShowing == lastEmittedStatus) return;
lastEmittedStatus = isShowing;
if(CachedMessageChannels.mainAppMessageChannel != null) {
CachedMessageChannels.mainAppMessageChannel.invokeMethod(methodName, isShowing);
}
if(CachedMessageChannels.overlayMessageChannel != null) {
CachedMessageChannels.overlayMessageChannel.invokeMethod(methodName, isShowing);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@

import android.view.Gravity;
import android.view.WindowManager;

import androidx.core.app.NotificationCompat;

import io.flutter.plugin.common.BasicMessageChannel;

public abstract class WindowSetup {

static int height = WindowManager.LayoutParams.MATCH_PARENT;
static int width = WindowManager.LayoutParams.MATCH_PARENT;
static int flag = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
static int gravity = Gravity.CENTER;
static BasicMessageChannel<Object> messenger = null;
static String overlayTitle = "Overlay is activated";
static String overlayContent = "Tap to edit settings or disable";
static String positionGravity = "none";
Expand Down
Loading