Skip to content

Commit

Permalink
feat: protect from multiple MethodChannel.Result invocation
Browse files Browse the repository at this point in the history
  • Loading branch information
ttypic committed Oct 25, 2023
1 parent 801eb34 commit eb5e533
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -133,46 +133,13 @@ public AblyMethodCallHandler(final MethodChannel methodChannel,
_map.put(PlatformConstants.PlatformMethod.cryptoGenerateRandomKey, this::cryptoGenerateRandomKey);
}

// MethodChannel.Result wrapper that responds on the platform thread.
//
// Plugins crash with "Methods marked with @UiThread must be executed on the main thread."
// This happens while making network calls in thread other than main thread
//
// https://github.com/flutter/flutter/issues/34993#issue-459900986
// https://github.com/aloisdeniel/flutter_geocoder/commit/bc34cfe473bfd1934fe098bb7053248b75200241
private static class MethodResultWrapper implements MethodChannel.Result {
private final MethodChannel.Result methodResult;
private final Handler handler;

MethodResultWrapper(MethodChannel.Result result) {
methodResult = result;
handler = new Handler(Looper.getMainLooper());
}

@Override
public void success(final Object result) {
handler.post(() -> methodResult.success(result));
}

@Override
public void error(
final String errorCode, final String errorMessage, final Object errorDetails) {
handler.post(() -> methodResult.error(errorCode, errorMessage, errorDetails));
}

@Override
public void notImplemented() {
handler.post(methodResult::notImplemented);
}
}

private void handleAblyException(@NonNull MethodChannel.Result result, @NonNull AblyException e) {
result.error(Integer.toString(e.errorInfo.code), e.getMessage(), e.errorInfo);
}

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result rawResult) {
final MethodChannel.Result result = new MethodResultWrapper(rawResult);
final MethodChannel.Result result = new SingleTimeMethodResult(new MainThreadMethodResult(rawResult));
Log.v(TAG, String.format("onMethodCall: Ably Flutter platform method \"%s\" invoked.", call.method));
final BiConsumer<MethodCall, MethodChannel.Result> handler = _map.get(call.method);
if (null == handler) {
Expand All @@ -183,7 +150,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
// We have a handler for a method with this name so delegate to it.
handler.accept(call, result);
} catch (Exception e) {
Log.e(TAG, String.format("\"%s\" platform method received error during invocation", call.method), e);
Log.e(TAG, String.format("\"%s\" platform method received error during invocation, caused by: %s", call.method, e.getMessage()), e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.ably.flutter.plugin;

import android.os.Handler;
import android.os.Looper;

import io.flutter.plugin.common.MethodChannel;

// MethodChannel.Result wrapper that responds on the platform thread.
//
// Plugins crash with "Methods marked with @UiThread must be executed on the main thread."
// This happens while making network calls in thread other than main thread
//
// https://github.com/flutter/flutter/issues/34993#issue-459900986
// https://github.com/aloisdeniel/flutter_geocoder/commit/bc34cfe473bfd1934fe098bb7053248b75200241
public class MainThreadMethodResult implements MethodChannel.Result {
private final MethodChannel.Result methodResult;
private final Handler handler;

MainThreadMethodResult(MethodChannel.Result result) {
methodResult = result;
handler = new Handler(Looper.getMainLooper());
}

@Override
public void success(final Object result) {
handler.post(() -> methodResult.success(result));
}

@Override
public void error(
final String errorCode, final String errorMessage, final Object errorDetails) {
handler.post(() -> methodResult.error(errorCode, errorMessage, errorDetails));
}

@Override
public void notImplemented() {
handler.post(methodResult::notImplemented);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.ably.flutter.plugin;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.concurrent.atomic.AtomicInteger;

import io.flutter.plugin.common.MethodChannel;

/**
* Prevents multiple MethodChannel.Result invocation, because it leads to app crash
* @link https://github.com/flutter/flutter/issues/29092
*/
public class SingleTimeMethodResult implements MethodChannel.Result {
private final MethodChannel.Result methodChannelResultInstance;
private final AtomicInteger invocationsCount = new AtomicInteger(0);

public SingleTimeMethodResult(MethodChannel.Result methodChannelResultInstance) {
this.methodChannelResultInstance = methodChannelResultInstance;
}

@Override
public void success(@Nullable Object result) {
if (invocationsCount.getAndIncrement() == 0) {
methodChannelResultInstance.success(result);
} else {
throw new IllegalStateException("Result shouldn't be called more than once");
}
}

@Override
public void error(@NonNull String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
if (invocationsCount.getAndIncrement() == 0) {
methodChannelResultInstance.error(errorCode, errorMessage, errorDetails);
} else {
throw new IllegalStateException("Result shouldn't be called more than once");
}
}

@Override
public void notImplemented() {
if (invocationsCount.getAndIncrement() == 0) {
methodChannelResultInstance.notImplemented();
} else {
throw new IllegalStateException("Result shouldn't be called more than once");
}
}
}

0 comments on commit eb5e533

Please sign in to comment.