Can't get broadcasts working with a native android module (AAR file) in Unity

The idea was that I needed the GPS in the background for a unity project. It is for finding out if a user is near an ‘information point’ while hiking certain routes. For that we figured (since we needed to get decently frequent GPS updates) to use a geofence. I created a native module in Android studio and placed the AAR file in unity. however the broadcastreceiver doesn’t ever trigger.
I started with the background broadcast tutorial of Android as a basic template (Membuat dan memantau pembatasan wilayah  |  Sensors and location  |  Android Developers).
The code ended up looking like this:

Bridge.java

package com.BrabantWater.mynativemodulegps;

//import needed packages / classes.
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofencingClient;
import com.google.android.gms.location.GeofencingRequest;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;

import java.util.ArrayList;
import java.util.List;

import androidx.annotation.NonNull;

public class Bridge extends Application
{
    public static Context context;
    public static String notificationContentTitle = "";
    public static String notificationContentText = "";
    public static String notificationDescription = "";

    private GeofencingClient geofencingClient;
    private ArrayList<Geofence> geofenceList;
    GeofencingRequest.Builder builder;
    PendingIntent geofencePendingIntent;
    GeofencingRequest geofencingRequest;

    public Bridge(Context ctx)
    {
        context = ctx;
        geofencingClient = new GeofencingClient(context);
        geofenceList = new ArrayList<>();
        builder = new GeofencingRequest.Builder();
    }

    public Bridge(Context ctx, String contentTitle, String contentText, String description)
    {
        context = ctx;
        notificationContentTitle = contentTitle;
        notificationContentText = contentText;
        notificationDescription = description;

        geofencingClient = new GeofencingClient(context);
        geofenceList = new ArrayList<>();
        builder = new GeofencingRequest.Builder();
    }

    public void addGeoFence(String id, double latitude, double longitude)
    {
        geofenceList.add(new Geofence.Builder()
                // Set the request ID of the geofence. This is a string to identify this
                // geofence.
                .setRequestId(id)
                .setCircularRegion(
                        latitude,
                        longitude,
                        300
                )
                .setNotificationResponsiveness(5000)
                .setExpirationDuration(Geofence.NEVER_EXPIRE)
                .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER |
                        Geofence.GEOFENCE_TRANSITION_EXIT |
                        Geofence.GEOFENCE_TRANSITION_DWELL)
                .setLoiteringDelay(1)
                .build());
        Log.i("TESTTAG", "GEOFENCE toegevoegd aan lijst");
        Log.i("TESTTAG", geofenceList.toString());
    }

    @SuppressLint("MissingPermission")
    public void GeoFenceCompleted()
    {
        Log.i("TESTTAG", "Completing Geofence");
        builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER | GeofencingRequest.INITIAL_TRIGGER_DWELL);
        builder.addGeofences(geofenceList);
        geofencingRequest =  builder.build();

        geofencingClient.addGeofences(geofencingRequest, getGeofencePendingIntent())
                .addOnSuccessListener(new OnSuccessListener<Void>() {
                    @Override
                    public void onSuccess(Void aVoid) {
                        // Geofences added
                        // ...
                        Log.i("TESTTAG", "Geofences added");
                    }
                })
                .addOnFailureListener(new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        // Failed to add geofences
                        // ...
                        Log.e("TESTTAG", "Couldn't add geofences");
                    }
                });
    }

    public void removeAllGeoFences()
    {
        geofencingClient.removeGeofences(getGeofencePendingIntent())
                .addOnSuccessListener(new OnSuccessListener<Void>() {
                    @Override
                    public void onSuccess(Void aVoid) {
                        // Geofences removed
                        // ...
                        Log.i("TESTTAG", "removed all geofences");
                    }
                })
                .addOnFailureListener(new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        // Failed to remove geofences
                        // ...
                        Log.e("TESTTAG", "Couldn't remove all geofences");
                    }
                });
    }

    public void removeGeoFences(List<String> ids) {
        geofencingClient.removeGeofences(ids);
    }

    private PendingIntent getGeofencePendingIntent() {
        // Reuse the PendingIntent if we already have it.
        if (geofencePendingIntent != null) {
            Log.i("TESTTAG", "used old intent for GeofenceBroadcastReceiver");
            return geofencePendingIntent;
        }
        Log.i("TESTTAG", "Created new intent for GeofenceBroadcastReceiver");
        Intent intent = new Intent(context, GeofenceBroadcastReceiver.class);
        // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when
        // calling addGeofences() and removeGeofences().
        geofencePendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.
                FLAG_UPDATE_CURRENT);
        return geofencePendingIntent;
    }
}

GeofenceBroadcastReceiver.java

package com.BrabantWater.mynativemodulegps;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;

import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofenceStatusCodes;
import com.google.android.gms.location.GeofencingEvent;

import java.util.List;

import androidx.core.app.NotificationCompat;

import static android.content.ContentValues.TAG;
import static androidx.core.content.ContextCompat.getSystemService;

public class GeofenceBroadcastReceiver extends BroadcastReceiver {
    // ...
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i("TESTTAG", "GEOFENCE received");
        GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
        if (geofencingEvent.hasError()) {
            String errorMessage = GeofenceStatusCodes.getStatusCodeString(geofencingEvent.getErrorCode());
            Log.e(TAG, errorMessage);
            return;
        }

        // Get the transition type.
        int geofenceTransition = geofencingEvent.getGeofenceTransition();

        // Test that the reported transition was of interest.
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
                geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT || geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL) {

            // Get the geofences that were triggered. A single event can trigger
            // multiple geofences.
            List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();

            // Get the transition details as a String.
            String geofenceTransitionDetails = triggeringGeofences.toString();

            // Send notification and log the transition details.
            sendNotification(geofenceTransitionDetails, context);
            Log.i(TAG, geofenceTransitionDetails);
        } else {
            // Log the error.
            Log.e(TAG, "Transition Error");
        }
    }

    private void sendNotification(String geofenceTransitionDetails, Context context)
    {
        Log.i("TESTTAG", "GEOFENCE triggered");
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "Brabant_Water_GO")
                .setContentTitle(Bridge.notificationContentTitle)
                .setContentText(Bridge.notificationContentText)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT);

        // Create the NotificationChannel, but only on API 26+ because
        // the NotificationChannel class is new and not in the support library
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            CharSequence name = "Brabant_Water_GO";
            String description = Bridge.notificationDescription;
            int importance = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel("Brabant_Water_GO", name, importance);
            channel.setDescription(description);
            // Register the channel with the system; you can't change the importance
            // or other notification behaviors after this
            NotificationManager notificationManager = getSystemService(context, NotificationManager.class);
            notificationManager.createNotificationChannel(channel);

            notificationManager.notify(10000, builder.build());
            Log.i("TESTTAG", "notification Shown");
        }
    }
}

The manifest looks like this:
AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.BrabantWater.mynativemodulegps">

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

    <application>
        <receiver android:name=".GeofenceBroadcastReceiver" />
    </application>
</manifest>

On the Unity side of things i made a class that calls the methods of bridge and modified the manifest to ensure i got access to the location.

AndroidCaller.cs

#if UNITY_ANDROID
//basic imports.
using UnityEngine;

public class AndroidCaller : MonoBehaviour
{
    AndroidJavaClass unityPlayerClass;
    AndroidJavaObject unityActivity;
    AndroidJavaObject bridge;
    object[] parameters;

    void Start()
    {
        //just done for testing purposes
        CallNativePlugin();
    }
    //method that calls our native plugin.
    public void CallNativePlugin()
    {
        // Retrieve the UnityPlayer class.
        unityPlayerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");

        // Retrieve the UnityPlayerActivity object ( a.k.a. the current context )
        unityActivity = unityPlayerClass.GetStatic<AndroidJavaObject>("currentActivity");

        // Setup the parameters we want to send to our native plugin.   
        parameters = new object[4];
        parameters[0] = unityActivity;
        parameters[1] = "Title";
        parameters[2] = "ContentText";
        parameters[3] = "Description";

        // Retrieve the "Bridge" from our native plugin.
        // ! Notice we define the complete package name.             
        bridge = new AndroidJavaObject("com.BrabantWater.mynativemodulegps.Bridge", parameters);

        // Call addGeoFence in bridge, with our parameters.
        bridge.Call("addGeoFence", "id", 51.461059, 4.310962);
        bridge.Call("addGeoFence", "id2", 51.451459, 4.329905);
        bridge.Call("GeoFenceCompleted");
    }
}
#endif

AndroidManifest.xml(in Unity project)

<?xml version="1.0" encoding="utf-8"?>
<!-- GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN-->
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.unity3d.player"
    xmlns:tools="http://schemas.android.com/tools">
    <application>
        <receiver android:name="com.BrabantWater.mynativemodulegps.GeofenceBroadcastReceiver" />
        <activity android:name="com.unity3d.player.UnityPlayerActivity"
                  android:theme="@style/UnityThemeSelector">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
        </activity>
    </application>
   
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    <uses-permission android:name="android.permission.CAMERA" />   
</manifest>

After alot of testing by using logcat i couldn’t figure out why the onReceive was never triggered. So that’s how i got stuck.

I never thought i would end up with an answer myself. Apparantly you have to use the full name in the receiver for both the xml in unity as well as the one in android studio, like so:

<receiver android:name="com.BrabantWater.mynativemodulegps.GeofenceBroadcastReceiver" />
2 Likes