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

Hypertrack is preventing FCM notification #8

Closed
bikcrum opened this issue Oct 14, 2020 · 4 comments · Fixed by hypertrack/sdk-flutter#3
Closed

Hypertrack is preventing FCM notification #8

bikcrum opened this issue Oct 14, 2020 · 4 comments · Fixed by hypertrack/sdk-flutter#3
Assignees

Comments

@bikcrum
Copy link

bikcrum commented Oct 14, 2020

Bug report

Describe the bug
I am using hypertrack_plugin (https://pub.dev/packages/hypertrack_plugin) which presumably uses FCM under the hood for its functioning. However using it alongside the firebase_messaging plugin, I am unable to get any message in callback onMessage() whenever the app is in the foreground. When I remove hypetrack_plugin from my project, it works fine.

Steps to reproduce

Steps to reproduce the behavior:

  1. Add hypertrack_plugin and firebase_messaging in the project dependency.
  2. Follow necessary steps to setup HyperTrack from the link given above.
  3. Also, do necessary steps to setup firebase_messaging.
  4. Implement a callback in following way:-
_firebaseMessaging.configure(
      onMessage: (Map<String, dynamic> message) async {
        print('on message $message');
        // not called when app is in foreground when hypertrack_plugin is added to the project
      },
      onResume: (Map<String, dynamic> message) async {
        print('on resume $message');
        // called when app is launched from background state from the notification tap. This works as expected.
      },
      onLaunch: (Map<String, dynamic> message) async {
        print('on launch $message');
        // called when app is launched from terminated state from the notification tap. This works as expected.
      },
    );

REST API used to invoke callbacks

curl --location --request POST 'https://fcm.googleapis.com/fcm/send' \
--header 'Content-Type: application/json' \
--header 'Authorization: key=<SERVER_KEY>' \
--data-raw '{
  "to": "<DEVICE_FCM_TOKEN>",
  "notification": {
    "title": "This is the title",
    "body": "Here is the body"
  },
  "data": {
    "click_action": "FLUTTER_NOTIFICATION_CLICK",
    "title": "This is the title",
    "body": "Here is the body",
    "some key": "some value",
    "another key": "another value"
  }
}'

Expected behavior

onMessage() should be called whenever the app is in the foreground.


Additional context

Add any other context about the problem here.


Flutter doctor

Run flutter doctor and paste the output below:

Click To Expand
[✓] Flutter (Channel stable, v1.17.5, on Mac OS X 10.15.4 19E287, locale en-NP)
 
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.1)
[✓] Xcode - develop for iOS and macOS (Xcode 11.3.1)
[✓] Android Studio (version 4.0)
[!] IntelliJ IDEA Ultimate Edition (version 2020.1.2)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[✓] Connected device (2 available)

! Doctor found issues in 1 category.

Flutter dependencies

Run flutter pub deps -- --style=compact and paste the output below:

Click To Expand
Dart SDK 2.8.4
Flutter SDK 1.17.5
chaincargo_courier 3.1.2+19

dependencies:
- connectivity 0.4.9+3 [flutter meta connectivity_platform_interface connectivity_macos connectivity_for_web]
- cupertino_icons 0.1.3
- device_info 0.4.2+8 [flutter device_info_platform_interface]
- dio 3.0.10 [http_parser path]
- firebase_core 0.5.0+1 [firebase_core_platform_interface flutter quiver meta firebase_core_web]
- firebase_messaging 7.0.3 [meta flutter firebase_core]
- firebase_remote_config 0.4.0+1 [flutter firebase_core]
- flushbar 1.10.4 [flutter]
- flutter 0.0.0 [collection meta typed_data vector_math sky_engine]
- flutter_localizations 0.0.0 [flutter intl collection meta path typed_data vector_math]
- flutter_svg 0.17.4 [path_drawing xml vector_math meta flutter]
- font_awesome_flutter 8.8.1 [flutter]
- hexcolor 1.0.4 [flutter]
- http 0.12.2 [http_parser path pedantic]
- image 2.1.12 [archive xml]
- intl 0.16.1 [path]
- intl_translation 0.17.9 [analyzer args dart_style intl path petitparser]
- package_info 0.4.3 [flutter]
- permission_handler 5.0.1+1 [flutter meta permission_handler_platform_interface]
- provider 3.2.0 [flutter]
- rxdart 0.20.0
- shared_preferences 0.5.12 [meta flutter shared_preferences_platform_interface shared_preferences_linux shared_preferences_macos shared_preferences_web shared_preferences_windows]
- url_launcher 5.7.2 [flutter url_launcher_platform_interface url_launcher_web url_launcher_linux url_launcher_macos url_launcher_windows]

dev dependencies:
- flutter_launcher_icons 0.6.1 [image dart_config]
- flutter_test 0.0.0 [flutter test_api path image quiver stack_trace vector_math archive args async boolean_selector charcode collection convert crypto matcher meta petitparser source_span stream_channel string_scanner term_glyph typed_data xml]

transitive dependencies:
- _fe_analyzer_shared 7.0.0 [meta]
- analyzer 0.39.17 [_fe_analyzer_shared args charcode cli_util collection convert crypto glob html meta package_config path pub_semver source_span watcher yaml]
- archive 2.0.13 [crypto args path]
- args 1.6.0
- async 2.4.1 [collection]
- boolean_selector 2.0.0 [source_span string_scanner]
- charcode 1.1.3
- cli_util 0.2.0 [path]
- collection 1.14.12
- connectivity_for_web 0.3.1+2 [connectivity_platform_interface flutter_web_plugins flutter]
- connectivity_macos 0.1.0+5 [flutter]
- connectivity_platform_interface 1.0.6 [flutter meta plugin_platform_interface]
- convert 2.1.1 [charcode typed_data]
- crypto 2.1.4 [collection convert typed_data]
- csslib 0.16.2 [source_span]
- dart_config 0.5.0 [yaml]
- dart_style 1.3.6 [analyzer args path source_span]
- device_info_platform_interface 1.0.1 [flutter meta plugin_platform_interface]
- ffi 0.1.3
- file 5.2.1 [intl meta path]
- firebase 7.3.0 [http http_parser js]
- firebase_core_platform_interface 2.0.0 [flutter meta plugin_platform_interface quiver]
- firebase_core_web 0.2.0 [firebase firebase_core_platform_interface flutter flutter_web_plugins meta js]
- flutter_web_plugins 0.0.0 [flutter collection meta typed_data vector_math]
- glob 1.2.0 [async collection node_io path pedantic string_scanner]
- html 0.14.0+3 [csslib source_span]
- http_parser 3.1.4 [charcode collection source_span string_scanner typed_data]
- js 0.6.2
- matcher 0.12.6 [stack_trace]
- meta 1.1.8
- node_interop 1.1.1 [js]
- node_io 1.1.1 [node_interop path]
- package_config 1.9.3 [path charcode]
- path 1.6.4
- path_drawing 0.4.1+1 [vector_math meta path_parsing flutter]
- path_parsing 0.1.4 [vector_math meta]
- path_provider_linux 0.0.1+2 [path xdg_directories path_provider_platform_interface flutter]
- path_provider_platform_interface 1.0.3 [flutter meta platform plugin_platform_interface]
- path_provider_windows 0.0.4+1 [path_provider_platform_interface meta path flutter ffi win32]
- pedantic 1.9.0
- permission_handler_platform_interface 2.0.1 [flutter meta plugin_platform_interface]
- petitparser 2.4.0 [meta]
- platform 2.2.1
- plugin_platform_interface 1.0.3 [meta]
- process 3.0.13 [file intl meta path platform]
- pub_semver 1.4.4 [collection]
- quiver 2.1.3 [matcher meta]
- shared_preferences_linux 0.0.2+2 [file flutter meta path path_provider_linux shared_preferences_platform_interface]
- shared_preferences_macos 0.0.1+10 [shared_preferences_platform_interface flutter]
- shared_preferences_platform_interface 1.0.4 [meta flutter]
- shared_preferences_web 0.1.2+7 [shared_preferences_platform_interface flutter flutter_web_plugins meta]
- shared_preferences_windows 0.0.1+1 [shared_preferences_platform_interface flutter ffi file meta path path_provider_platform_interface path_provider_windows]
- sky_engine 0.0.99
- source_span 1.7.0 [charcode collection meta path term_glyph]
- stack_trace 1.9.3 [path]
- stream_channel 2.0.0 [async]
- string_scanner 1.0.5 [charcode meta source_span]
- term_glyph 1.1.0
- test_api 0.2.15 [async boolean_selector collection meta path source_span stack_trace stream_channel string_scanner term_glyph matcher]
- typed_data 1.1.6
- url_launcher_linux 0.0.1+1 [flutter]
- url_launcher_macos 0.0.1+8 [flutter]
- url_launcher_platform_interface 1.0.8 [flutter meta plugin_platform_interface]
- url_launcher_web 0.1.4+1 [url_launcher_platform_interface flutter flutter_web_plugins meta]
- url_launcher_windows 0.0.1+1 [flutter]
- vector_math 2.0.8
- watcher 0.9.7+15 [async path pedantic]
- win32 1.7.3 [ffi]
- xdg_directories 0.1.0 [path process flutter]
- xml 3.6.1 [collection convert meta petitparser]
- yaml 2.2.1 [charcode collection string_scanner source_span]

@deniszpua deniszpua self-assigned this Nov 4, 2020
@deniszpua
Copy link
Contributor

Planned solution is to forward unprocessed messages and tokens to FirebasePlugin

@bikcrum
Copy link
Author

bikcrum commented Nov 4, 2020

Planned solution is to forward unprocessed messages and tokens to FirebasePlugin

I cannot see incoming message implementation in Hypertrack SDK. What can be done?

@deniszpua
Copy link
Contributor

deniszpua commented Nov 4, 2020

@bikcrum You need to extend HyperTrackMessagingService and use reflection to get access to FirebasePlugin. You can use our Cordova plugin as a reference.
The tricky point here is that you cannot be sure whether FlutterFirebaseMessagingPlugin is present in project or not, otherwise you don't need to use reflection, but just send those broadcasts.
To fix it not for generic case, but for your project only, you can just extend HyperTrackMessagingService and forward its messages to FlutterFirebaseMessagingPlugin as broadcasts.

@bikcrum
Copy link
Author

bikcrum commented Nov 13, 2020

Problem

There is multiple services class that extends from FirebaseMessagingService. Because of this, messages are received in one class with high priority and not the other.

Solution

Method 1

Add the following to your AndroidManifest.xml file

<service android:name="io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService">
      <intent-filter android:priority="100">
            <action android:name="com.google.firebase.MESSAGING_EVENT"/>
      </intent-filter>
</service>

How it works?

The priority set for FlutterFirebaseMessagingService in its manifest file is zero (default) but HyperTrackMessagingService in its manifest file is declared with a priority of 5 (version 4.8.0 now). The above solution simply overrides the priority and lets incoming messages come to FlutterFirebaseMessagingService instead of HyperTrackMessagingService.

Limitation:

Although HyperTrack will work fine, HyperTrack uses FCM for device-server communication for optimization which won't function without FCM. However, you might not notice this.

Method 2

Forward incoming message in HyperTrackMessagingService to FlutterFirebaseMessagingService in the plugin hypertrack_plugin using reflection.

Steps:

  1. You will need to download hypertrack_plugin and make it local instead of hosted dependency.
  2. Create a new class inside the library hypertrack_plugin-0.1.3/android/src/main/kotlin/com/hypertrack/sdk/flutter/MyFirebaseMessagingService.java
package com.hypertrack.sdk.flutter;

import android.annotation.SuppressLint;
import android.util.Log;

import com.google.firebase.messaging.RemoteMessage;
import com.hypertrack.sdk.HyperTrackMessagingService;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

@SuppressLint("LongLogTag")
public class MyFirebaseMessagingService extends HyperTrackMessagingService {

    private static final String TAG = "MyFirebaseMessagingService";

    private Class<?> serviceClass;
    private Object serviceObject;

    public MyFirebaseMessagingService() {
        try {
            serviceClass = Class.forName("io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService");
            serviceObject = serviceClass.newInstance();
            injectContext();

            Log.d(TAG, "io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService is found");
        } catch (Throwable t) {
            Log.w(TAG, "Can't find the class io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService", t);
        }
    }

    @Override
    public void onNewToken(final String s) {
        super.onNewToken(s);
        injectToken(s);
    }

    @Override
    public void onMessageReceived(final RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        injectMessage(remoteMessage);
    }

    public void injectToken(String newToken) {
        if (serviceClass != null) {
            try {
                Method sendTokenRefresh = serviceClass.getMethod("onNewToken", String.class);
                sendTokenRefresh.invoke(serviceObject, newToken);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                Log.w(TAG, "Can't inject token due to error ", e);
            }
        }
    }

    public void injectMessage(RemoteMessage remoteMessage) {
        if (serviceClass != null) {
            try {
                Method sendTokenRefresh = serviceClass.getMethod("onMessageReceived", RemoteMessage.class);
                sendTokenRefresh.invoke(serviceObject, remoteMessage);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                Log.w(TAG, "Can't inject message due to error ", e);
            }
        }
    }

    private void injectContext() {
        if (serviceObject != null) {
            if (setField(serviceObject, "mBase", this)) {
                Log.d(TAG, "context is set to io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService");
            }
        }
    }

    private boolean setField(Object targetObject, String fieldName, Object fieldValue) {
        Field field;
        try {
            field = targetObject.getClass().getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            field = null;
        }
        Class<?> superClass = targetObject.getClass().getSuperclass();
        while (field == null && superClass != null) {
            try {
                field = superClass.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                superClass = superClass.getSuperclass();
            }
        }
        if (field == null) {
            return false;
        }
        field.setAccessible(true);
        try {
            field.set(targetObject, fieldValue);
            return true;
        } catch (IllegalAccessException e) {
            return false;
        }
    }
}
  1. Edit manifest file hypertrack_plugin-0.1.3/android/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.hypertrack.sdk.flutter">

    <application>
        <service
            android:name=".MyFirebaseMessagingService"
            android:exported="false" >
            <intent-filter android:priority="100" >
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
    </application>
</manifest>

How it works?

MyFirebaseMessagingService extended from HyperTrackMessagingService, is declared in the AndroidManifest.xml file with higher priority (definitely higher than HyperTrackMessagingService). This will allow messages directly to come to the new class. This will also remove the limitation in method 1. Now we forward the message also to FlutterFirebaseMessagingService using reflection.

Limitation:

No limitation, but you need to manually update the hypertrack_plugin when the host library is updated. It's good to have this update in the plugin itself which is not present now (Nov 13)

Method 3

Now we will not touch any code in the library but write our own code. We will propose a solution in a safe way. You don't have to add hypertrack_plugin as local dependencies.

  1. Create class <project_root>/android/app/src/main/<your_package_name>/MyFirebaseMessagingService.java
package com.example.myapp;

import android.annotation.SuppressLint;
import android.util.Log;

import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

import org.jetbrains.annotations.NotNull;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

@SuppressLint("LongLogTag")
public class MyFirebaseMessagingService extends FirebaseMessagingService {

    private static final String TAG = "MyFirebaseMessagingService";

    // put all the firebase messaging service classes used in your project here
    private String[] fcmClasses = new String[]{
            "io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService",
            "com.hypertrack.sdk.HyperTrackMessagingService"};

    @Override
    public void onNewToken(@NotNull String token) {
        Log.d(TAG, "onNewToken()");
        super.onNewToken(token);
        injectToken(token);
    }

    @Override
    public void onMessageReceived(@NotNull RemoteMessage remoteMessage) {
        Log.d(TAG, "onMessageReceived()");
        super.onMessageReceived(remoteMessage);
        injectMessage(remoteMessage);
    }

    public void injectToken(String newToken) {
        Log.d(TAG, "injectToken()");

        for (String fcmClass : fcmClasses) {
            try {
                Class<?> serviceClass = Class.forName(fcmClass);
                Object serviceObject = serviceClass.newInstance();
                injectContext(serviceClass, serviceObject);

                Method sendTokenRefresh = serviceClass.getMethod("onNewToken", String.class);
                sendTokenRefresh.invoke(serviceObject, newToken);

            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException | InstantiationException e) {
                Log.w(TAG, "Can't inject token due to error ", e);
            }
        }
    }

    public void injectMessage(RemoteMessage remoteMessage) {
        Log.d(TAG, "injectMessage()");

        for (String fcmClass : fcmClasses) {
            try {
                Class<?> serviceClass = Class.forName(fcmClass);
                Object serviceObject = serviceClass.newInstance();
                injectContext(serviceClass, serviceObject);

                Method sendTokenRefresh = serviceClass.getMethod("onMessageReceived", RemoteMessage.class);
                sendTokenRefresh.invoke(serviceObject, remoteMessage);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException | InstantiationException e) {
                Log.w(TAG, "Can't inject token due to error ", e);
            }
        }
    }

    private void injectContext(Class<?> serviceClass, Object serviceObject) {
        Log.d(TAG, "injectContext()");
        if (serviceClass != null) {
            if (setField(serviceObject, "mBase", this)) {
                Log.d(TAG, "context is set to " + serviceClass.getName());
            }
        }
    }

    private boolean setField(Object targetObject, String fieldName, Object fieldValue) {
        Log.d(TAG, "setField()");

        Field field;
        try {
            field = targetObject.getClass().getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            field = null;
        }
        Class<?> superClass = targetObject.getClass().getSuperclass();
        while (field == null && superClass != null) {
            try {
                field = superClass.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                superClass = superClass.getSuperclass();
            }
        }
        if (field == null) {
            return false;
        }
        field.setAccessible(true);
        try {
            field.set(targetObject, fieldValue);
            return true;
        } catch (IllegalAccessException e) {
            return false;
        }
    }
}
  1. Declare it in <project_root>/couriers/android/app/src/main/AndroidManifest.xml

Important: Set priority higher than 5 (one declared in HyperTrack SDK)

     <service
            android:name=".MyFirebaseMessagingService"
            android:exported="false">
            <intent-filter android:priority="100">
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>

How does it work?

We have created MyFirebaseMessagingService explicitly and extended from FirebaseMessagingService. We also have set a priority as high as 100 to make sure we are receiving incoming messages only here and not in any other firebase messaging service such as HyperTrack. Now we have the flexibility to forward this message wherever we need, using reflection.

Limitation:

None

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants