Skip to content

Commit

Permalink
Merge pull request #215 from ably/bug/app-processes-is-null
Browse files Browse the repository at this point in the history
0 Fix app crashing when app is not yet started on some devices
  • Loading branch information
ben-xD authored Nov 15, 2021
2 parents b3a3ced + c06b6a3 commit 31c42f2
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@

import com.google.firebase.messaging.RemoteMessage;

import javax.crypto.Cipher;

import io.ably.flutter.plugin.generated.PlatformConstants;
import io.ably.flutter.plugin.push.RemoteMessageCallback;
import io.ably.flutter.plugin.push.PushActivationEventHandlers;
Expand Down Expand Up @@ -74,7 +72,7 @@ private void setupChannels(BinaryMessenger messenger, Context applicationContext
BackgroundMethodCallHandler backgroundMethodCallHandler = new BackgroundMethodCallHandler(messenger, codec);
methodChannel.setMethodCallHandler(methodCallHandler);
PushActivationEventHandlers.instantiate(applicationContext, methodChannel);
PushMessagingEventHandlers.instantiate(applicationContext, methodChannel);
PushMessagingEventHandlers.reset(applicationContext, methodChannel);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import io.ably.flutter.plugin.generated.PlatformConstants;
import io.ably.flutter.plugin.push.PushActivationEventHandlers;
import io.ably.flutter.plugin.push.PushBackgroundIsolateRunner;
import io.ably.flutter.plugin.types.PlatformClientOptions;
import io.ably.flutter.plugin.util.BiConsumer;
import io.ably.lib.realtime.AblyRealtime;
Expand Down Expand Up @@ -102,7 +101,6 @@ public AblyMethodCallHandler(final MethodChannel channel,
_map.put(PlatformConstants.PlatformMethod.pushListSubscriptions, this::pushListSubscriptions);
_map.put(PlatformConstants.PlatformMethod.pushDevice, this::pushDevice);
_map.put(PlatformConstants.PlatformMethod.pushNotificationTapLaunchedAppFromTerminated, this::pushNotificationTapLaunchedAppFromTerminated);
_map.put(PlatformConstants.PlatformMethod.pushSetOnBackgroundMessage, this::pushSetOnBackgroundMessage);

// paginated results
_map.put(PlatformConstants.PlatformMethod.nextPage, this::getNextPage);
Expand Down Expand Up @@ -740,11 +738,6 @@ private void pushNotificationTapLaunchedAppFromTerminated(@NonNull MethodCall ca
remoteMessageFromUserTapLaunchesApp = null;
}

private void pushSetOnBackgroundMessage(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Long backgroundMessageHandlerHandle = (Long) call.arguments;
PushBackgroundIsolateRunner.setBackgroundMessageHandler(applicationContext, backgroundMessageHandlerHandle);
}

private void getNextPage(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments;
this.<Integer>ablyDo(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package io.ably.flutter.plugin.push;

import static io.ably.flutter.plugin.push.PushMessagingEventHandlers.PUSH_ON_BACKGROUND_MESSAGE_PROCESSING_COMPLETE;
import static io.ably.flutter.plugin.push.PushMessagingEventHandlers.PUSH_ON_BACKGROUND_MESSAGE_RECEIVED;
import static io.ably.flutter.plugin.push.PushMessagingEventHandlers.PUSH_ON_MESSAGE_RECEIVED;

import android.app.ActivityManager;
import android.content.BroadcastReceiver;
Expand All @@ -11,11 +9,10 @@
import android.content.IntentFilter;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.google.firebase.messaging.RemoteMessage;

import java.util.List;

import io.ably.flutter.plugin.AblyFlutterPlugin;
Expand All @@ -42,7 +39,7 @@ public void onReceive(Context context, Intent intent) {
* (PushMessagingEventHandlers.BroadcastReceiver) to listen to messages from this receiver
* (FirebaseMessagingReceiver).
*/
private void setupFlutterApplicationProcessingCompletionReceiver(Context context) {
private void setupFlutterApplicationProcessingCompletionReceiver(@NonNull final Context context) {
// Wait for Flutter application to process message
// At the end of the receiver's execution time (and user's application processing the message)
// , Firebase messaging library will automatically create a notification.
Expand All @@ -54,32 +51,30 @@ private void setupFlutterApplicationProcessingCompletionReceiver(Context context
}
}

private void sendMessageToFlutterApplication(Context context, Intent intent) {
final RemoteMessage message = new RemoteMessage(intent.getExtras());
private void sendMessageToFlutterApplication(@NonNull final Context context,
@NonNull final Intent intent) {
final Boolean isApplicationInForeground = isApplicationInForeground(context);

if (isApplicationInForeground) {
// Send message to Dart side app already running
final Intent onMessageReceivedIntent = new Intent(PUSH_ON_MESSAGE_RECEIVED);
onMessageReceivedIntent.putExtras(intent.getExtras());
LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
PushMessagingEventHandlers.sendMessageToFlutterApp(context, intent);
} else if (AblyFlutterPlugin.isMainActivityRunning) {
// Flutter is already running, just send a background message to it.
final Intent onMessageReceivedIntent = new Intent(PUSH_ON_BACKGROUND_MESSAGE_RECEIVED);
onMessageReceivedIntent.putExtras(intent.getExtras());
LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
PushMessagingEventHandlers.sendBackgroundMessageToFlutterApp(context, intent);
} else {
// No existing Flutter Activity is running, create a FlutterEngine and pass it the RemoteMessage
new PushBackgroundIsolateRunner(context, this, message);
new ManualFlutterApplicationRunner(context, this, intent);
}

}

private Boolean isApplicationInForeground(final Context context) {
private Boolean isApplicationInForeground(@NonNull final Context context) {
final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
// This only shows processes for the current android app.
final List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();

if (appProcesses == null) {
// If no processes are running, appProcesses are null, not an empty list.
// The user's app is definitely not in the foreground if no processes are running.
return false;
}

for (ActivityManager.RunningAppProcessInfo process : appProcesses) {
// Importance is IMPORTANCE_SERVICE (not IMPORTANCE_FOREGROUND)
// - when app was terminated, or
Expand All @@ -98,7 +93,7 @@ private Boolean isApplicationInForeground(final Context context) {
* A dynamic broadcast receiver registered to listen to a `PUSH_ON_BACKGROUND_MESSAGE_PROCESSING_COMPLETE`
*/
class BackgroundMessageProcessingCompleteReceiver extends BroadcastReceiver {
BackgroundMessageProcessingCompleteReceiver(final Context context) {
BackgroundMessageProcessingCompleteReceiver(@NonNull final Context context) {
final IntentFilter filter = new IntentFilter();
filter.addAction(PUSH_ON_BACKGROUND_MESSAGE_PROCESSING_COMPLETE);
LocalBroadcastManager.getInstance(context).registerReceiver(this, filter);
Expand All @@ -123,6 +118,7 @@ public void onReceive(Context context, Intent intent) {
void finish() {
if (asyncProcessingPendingResult != null) {
asyncProcessingPendingResult.finish();
asyncProcessingPendingResult = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package io.ably.flutter.plugin.push;

import static android.content.Context.MODE_PRIVATE;
import static io.ably.flutter.plugin.generated.PlatformConstants.PlatformMethod.pushOnBackgroundMessage;
import static io.ably.flutter.plugin.generated.PlatformConstants.PlatformMethod.pushSetOnBackgroundMessage;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.Intent;
import android.util.Log;

import androidx.annotation.NonNull;
Expand All @@ -21,20 +20,35 @@
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.StandardMethodCodec;

public class PushBackgroundIsolateRunner implements MethodChannel.MethodCallHandler {
private static final String TAG = PushBackgroundIsolateRunner.class.getName();
private static final String SHARED_PREFERENCES_KEY = "io.ably.flutter.plugin.push.PushBackgroundIsolate.SHARED_PREFERENCES_KEY";
private static final String BACKGROUND_MESSAGE_HANDLE_KEY = "BACKGROUND_MESSAGE_HANDLE_KEY";
/**
* This class is used when the application was terminated when the push notification is received.
* It launches the Flutter application and sends it a RemoteMessage. See [PushMessagingEventHandlers.java]
* to see where push notifications being handled whilst the app is in the background or the foreground.
* Use this class when no existing Flutter Activity is running.
*
* This class can be generalized to launch the app manually for any purpose, but currently it is
* narrowly scoped for push notifications.
*/
public class ManualFlutterApplicationRunner implements MethodChannel.MethodCallHandler {
private static final String TAG = ManualFlutterApplicationRunner.class.getName();
private final FirebaseMessagingReceiver broadcastReceiver;
private final RemoteMessage remoteMessage;
private final MethodChannel backgroundMethodChannel;

@NonNull
private final FlutterEngine flutterEngine;

public PushBackgroundIsolateRunner(Context context, FirebaseMessagingReceiver receiver, RemoteMessage message) {
/**
* Creates a Flutter engine, launches the Flutter application inside that Flutter engine, and
* creates a MethodChannel to communicate with the Flutter application.
*
* @param context
* @param receiver The FirebaseMessagingReceiver which received the message
* @param intent An intent containing a RemoteMessage passed straight from FirebaseMessagingReceiver
*/
public ManualFlutterApplicationRunner(@NonNull final Context context,
@NonNull final FirebaseMessagingReceiver receiver,
@NonNull final Intent intent) {
this.broadcastReceiver = receiver;
this.remoteMessage = message;
this.remoteMessage = new RemoteMessage(intent.getExtras());
flutterEngine = new FlutterEngine(context, null);
DartExecutor executor = flutterEngine.getDartExecutor();
backgroundMethodChannel = new MethodChannel(executor.getBinaryMessenger(), "io.ably.flutter.plugin.background", new StandardMethodCodec(new AblyMessageCodec(new CipherParamsStorage())));
Expand All @@ -46,18 +60,9 @@ public PushBackgroundIsolateRunner(Context context, FirebaseMessagingReceiver re
flutterEngine.getBroadcastReceiverControlSurface().attachToBroadcastReceiver(receiver, null);
}

/**
* This method is called when the main app is running and the user sets the background handler.
*
* @param backgroundMessageHandlerHandle
*/
public static void setBackgroundMessageHandler(Context context, Long backgroundMessageHandlerHandle) {
SharedPreferences preferences = context.getApplicationContext().getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE);
preferences.edit().putLong(BACKGROUND_MESSAGE_HANDLE_KEY, backgroundMessageHandlerHandle).apply();
}

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
public void onMethodCall(@NonNull final MethodCall call,
@NonNull final MethodChannel.Result result) {
if (call.method.equals(pushSetOnBackgroundMessage)) {
// This signals that the manually spawned app is ready to receive a message to handle.
// We ask the user to set the background message handler early on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

Expand All @@ -16,21 +16,52 @@
import io.ably.flutter.plugin.generated.PlatformConstants;
import io.flutter.plugin.common.MethodChannel;

/**
* This class is used when the application is in the **foreground** or **background**, but not when
* **terminated**. See [PushTerminatedIsolateRunner.java] to see where push notifications being
* handled whilst the app is terminated.
*/
final public class PushMessagingEventHandlers {
private static final String TAG = PushMessagingEventHandlers.class.getName();
public static final String PUSH_ON_MESSAGE_RECEIVED = "io.ably.ably_flutter.PUSH_ON_MESSAGE_RECEIVED";
public static final String PUSH_ON_BACKGROUND_MESSAGE_RECEIVED = "io.ably.ably_flutter.PUSH_ON_BACKGROUND_MESSAGE_RECEIVED";
public static final String PUSH_ON_BACKGROUND_MESSAGE_PROCESSING_COMPLETE = "io.ably.ably_flutter.PUSH_ON_BACKGROUND_MESSAGE_COMPLETE";
static PushMessagingEventHandlers instance;

public static void instantiate(Context context, MethodChannel methodChannel) {
if (instance == null) {
instance = new PushMessagingEventHandlers(context, methodChannel);
/**
* Replaces the existing instance if required. this is because the latest methodChannel is the
* most important because it is from the latest plugin registration.
* @param context
* @param methodChannel
*/
public static void reset(Context context, MethodChannel methodChannel) {
if (instance != null) {
instance.close(context);
}
instance = new PushMessagingEventHandlers(context, methodChannel);
}

private void close(Context context) {
this.broadcastReceiver.close(context);
}

private final BroadcastReceiver broadcastReceiver;
private PushMessagingEventHandlers(Context context, MethodChannel methodChannel) {
new BroadcastReceiver(context, methodChannel);
this.broadcastReceiver = new BroadcastReceiver(context, methodChannel);
}

// Send message to Dart side app already running
public static void sendMessageToFlutterApp(@NonNull final Context context, @NonNull final Intent intent) {
final Intent onMessageReceivedIntent = new Intent(PUSH_ON_MESSAGE_RECEIVED);
onMessageReceivedIntent.putExtras(intent.getExtras());
LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
}

// Flutter is already running, just send a background message to it.
public static void sendBackgroundMessageToFlutterApp(Context context, Intent intent) {
final Intent onMessageReceivedIntent = new Intent(PUSH_ON_BACKGROUND_MESSAGE_RECEIVED);
onMessageReceivedIntent.putExtras(intent.getExtras());
LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
}

private static class BroadcastReceiver extends android.content.BroadcastReceiver {
Expand All @@ -48,6 +79,11 @@ private void register(Context context) {
LocalBroadcastManager.getInstance(context).registerReceiver(this, filter);
}

void close(Context context) {
LocalBroadcastManager.getInstance(context).unregisterReceiver(this);
this.methodChannel = null;
}

@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Expand Down
2 changes: 2 additions & 0 deletions lib/src/platform/src/platform.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

import 'package:ably_flutter/src/platform/src/background_android_isolate_platform.dart';
import 'package:flutter/services.dart';

import '../../error/error.dart';
Expand Down Expand Up @@ -28,6 +29,7 @@ class Platform {
if (_initializer == null) {
AblyMethodCallHandler(methodChannel);
_initializer = methodChannel.invokeMethod(PlatformMethod.registerAbly);
BackgroundIsolateAndroidPlatform();
}
return _initializer;
}
Expand Down

0 comments on commit 31c42f2

Please sign in to comment.