Skip to content

Cross-platform compatibility issue between iOS and Android #45

@manudicri

Description

@manudicri

Bridgefy messaging functionality is not working between iOS and Android devices, preventing cross-platform communication.

Expected Behavior

  • Messages sent from iOS devices should be received by Android devices
  • Messages sent from Android devices should be received by iOS devices
  • Mesh networking should work seamlessly across both platforms

Actual Behavior

  • Messages are not being transmitted/received between iOS and Android devices
  • Cross-platform discovery and connection may be failing
  • Communication seems to work within the same platform (iOS to iOS)

This is my service code, execute on both iOS, iPadOS and Android.
Messages from Android are not received on iOS and iPadOS, and messages from iOS are not received on Android.
I'm just using broadcast transmissionMode, no p2p connections.

I get no errors and no logs while sending or receiving. Bridgefy is successfully started on both devices

// ignore_for_file: constant_identifier_names

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:bridgefy/bridgefy.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:praisetune/config/env.dart';
import 'package:praisetune/extra/utility.dart';

class NearDevice {
  String username;
  String? deviceModel;
  bool get following => NearDevice.followingList.contains(uuid);
  bool blocked;
  String uuid;
  int? lastSongId;

  String get iconLetter {
    if (username.trim().isNotEmpty) {
      return username.split(".").map((piece) => piece.substring(0, 1)).take(2).join().toUpperCase();
    } else {
      return "?";
    }
  }

  static List<String> followingList = [];

  //String get iconLetter => username.trim().isNotEmpty ? username.substring(0, 1).toUpperCase() : "?";

  NearDevice({
    required this.username,
    required this.uuid,
    this.blocked = false,
    this.deviceModel,
  });
}

extension MyNearDeviceListExtensions on List<NearDevice> {
  NearDevice? findByUuid(String uuid) {
    return firstWhereOrNull((device) => device.uuid == uuid);
  }
}

final bridgefyServiceProvider = ChangeNotifierProvider<BridgefyService>((ref) {
  return BridgefyService();
});

class BridgefyService extends ChangeNotifier implements BridgefyDelegate {
  final _bridgefy = Bridgefy();
  factory BridgefyService() => _instance;
  BridgefyService._internal();
  static final BridgefyService _instance = BridgefyService._internal();

  static const int MSG_USER_NAME = 1;
  static const int MSG_SONG_ID = 2;
  static const int MSG_PING = 3;
  static const int MSG_STOP = 4;
  static const int MSG_TRANSPOSE = 5;

  bool isInitialized = false;
  bool isStarted = false;
  bool permissionsGranted = false;
  String userId = "";
  String? username = "";

  Timer? _debounceTransposeTimer;

  final nearDevices = <NearDevice>[];

  Future<bool> _checkPermissions() async {
    final status = await [
      Permission.location,
      Permission.bluetoothAdvertise,
      Permission.bluetoothConnect,
      Permission.bluetoothScan,
    ].request();
    if (Platform.isIOS) return true;

    bool granted = true;
    status.forEach((key, value) {
      if (value == PermissionStatus.permanentlyDenied || value == PermissionStatus.denied) {
        granted = false;
      }
    });
    if (!granted) {
      openAppSettings();
    }
    return granted;
  }

  Future<void> initialized() async {
    myPrint("Initializing Bridgefy");
    try {
      permissionsGranted = await _checkPermissions();
      if (!permissionsGranted || isInitialized) return;
      await _bridgefy.initialize(
        apiKey: Environment().config.bridgefyApiKey,
        delegate: this,
        verboseLogging: true,
      );
      myPrint("Bridgefy initialized with API Key: ${Environment().config.bridgefyApiKey}");
      isInitialized = await _bridgefy.isInitialized;
      notifyListeners();

      try {
        // aggiorno la licenza sempre, però se non è connesso ad internet lascia stare
        await _bridgefy.updateLicense();
      } catch (err) {
        myPrint("Non connesso ad internet, non aggiorno la licenza");
      }
    } catch (e) {
      if (e is BridgefyError && e.type == BridgefyErrorType.alreadyInstantiated) {
        isInitialized = await _bridgefy.isInitialized;
        notifyListeners();
      }
      myPrint(e);
    }
  }

  Future<void> start({
    String? userId,
    BridgefyPropagationProfile propagationProfile = BridgefyPropagationProfile.standard,
  }) async {
    if (!isInitialized || !permissionsGranted) {
      await initialized();
    }
    if (!permissionsGranted) {
      return;
    }
    if (!(await _bridgefy.isInitialized)) {
      isInitialized = false;
      await initialized();
    }
    assert(isInitialized, 'Bridgefy is not initialized');
    await _bridgefy.start();
    myPrint("Bridgefy partito! (in teoria)");
    notifyListeners();
  }

  Future<void> stop() async {
    assert(isInitialized, 'Bridgefy is not initialized');
    assert(isStarted, 'Bridgefy is not started');

    await _bridgefy.send(
        data: Uint8List.fromList([MSG_STOP]),
        transmissionMode: BridgefyTransmissionMode(type: BridgefyTransmissionModeType.broadcast, uuid: userId));

    await _bridgefy.stop();
    nearDevices.clear();
    notifyListeners();
  }

  @override
  void bridgefyDidConnect({required String userID}) {
    // TODO: implement bridgefyDidConnect
  }

  @override
  void bridgefyDidDestroySession() {
    // TODO: implement bridgefyDidDestroySession
  }

  @override
  void bridgefyDidDisconnect({required String userID}) {
    // TODO: implement bridgefyDidDisconnect
  }

  @override
  void bridgefyDidEstablishSecureConnection({required String userID}) {
    // TODO: implement bridgefyDidEstablishSecureConnection
  }

  @override
  void bridgefyDidFailSendingMessage({required String messageID, BridgefyError? error}) {
    myPrint("Ah non ho proprio inviato..");
    myPrint(error);
    // TODO: implement bridgefyDidFailSendingMessage
  }

  Function(int, NearDevice)? onSongIdReceived;
  Function(int, NearDevice)? onTransposeReceived;
  void Function()? onBridgefyStart;

  @override
  void bridgefyDidFailToDestroySession() {
    // TODO: implement bridgefyDidFailToDestroySession
  }

  @override
  void bridgefyDidFailToEstablishSecureConnection({required String userID, required BridgefyError error}) {
    // TODO: implement bridgefyDidFailToEstablishSecureConnection
  }

  @override
  void bridgefyDidFailToStart({required BridgefyError error}) {
    // TODO: implement bridgefyDidFailToStart
  }

  @override
  void bridgefyDidFailToStop({required BridgefyError error}) {
    // TODO: implement bridgefyDidFailToStop
  }

  @override
  void bridgefyDidReceiveData(
      {required Uint8List data, required String messageId, required BridgefyTransmissionMode transmissionMode}) {
    myPrint("Ho ricevuto qualcosa!");
    final type = data[0];
    myPrint(type);
    if (type == BridgefyService.MSG_SONG_ID) {
      // TODO: ora che ci penso, con la sessione, uno potrebbe avere il permesso di aprire canzoni non sue, senza poterle salvare ovviamente.
      final byteData = ByteData.sublistView(data);
      final songId = byteData.getUint32(1);

      final device = nearDevices.findByUuid(transmissionMode.uuid);
      if (device != null && device.following) {
        device.lastSongId = songId;
        onSongIdReceived?.call(songId, device);
      }
    }
    if (type == BridgefyService.MSG_TRANSPOSE) {
      final transpose = data[1];
      final device = nearDevices.findByUuid(transmissionMode.uuid);
      if (device != null && device.following) {
        onTransposeReceived?.call(transpose - 12, device);
      }
    }
    if (type == BridgefyService.MSG_USER_NAME) {
      final name = utf8.decode(data.sublist(1)); // 1 after the MSG_USER_NAME

      final existing = nearDevices.findByUuid(transmissionMode.uuid);
      if (existing != null) {
        if (existing.username != name) {
          existing.username = name;
          notifyListeners();
        }
      } else {
        nearDevices.add(NearDevice(username: name, uuid: transmissionMode.uuid));
        notifyListeners();
      }
    }
    if (type == BridgefyService.MSG_PING) {
      sendUsername();
    }
    if (type == BridgefyService.MSG_STOP) {
      nearDevices.removeWhere((device) => device.uuid == transmissionMode.uuid);
      notifyListeners();
    }
  }

  @override
  void bridgefyDidSendDataProgress({required String messageID, required int position, required int of}) {
    // TODO: implement bridgefyDidSendDataProgress
    myPrint("O almeno ci provo");
  }

  @override
  void bridgefyDidSendMessage({required String messageID}) {
    // TODO: implement bridgefyDidSendMessage
    myPrint("Ho inviato un messaggio!");
  }

  @override
  void bridgefyDidStart({required String currentUserID}) {
    isStarted = true;
    userId = currentUserID;
    myPrint("Bridgefy partito! (in PRATICA)");
    onBridgefyStart?.call();
    notifyListeners();
  }

  @override
  void bridgefyDidStop() {
    isStarted = false;
    userId = '';

    notifyListeners();
  }

  Future<void> sendUsername() {
    if (username == null || username!.isEmpty) throw "Utente non registrato";
    if (!isStarted) throw BridgefyError(type: BridgefyErrorType.notStarted);
    final nameBytes = Uint8List.fromList(username!.codeUnits); // UTF-16 bytes
    final result = Uint8List.fromList([
      MSG_USER_NAME,
      ...nameBytes,
    ]);
    return _bridgefy.send(
        data: result,
        transmissionMode: BridgefyTransmissionMode(type: BridgefyTransmissionModeType.broadcast, uuid: userId));
  }

  Future<void> pingNearDevices() {
    if (!isStarted) throw BridgefyError(type: BridgefyErrorType.notStarted);
    myPrint("Pinging near devices!");
    return _bridgefy.send(
        data: Uint8List.fromList([MSG_PING]),
        transmissionMode: BridgefyTransmissionMode(type: BridgefyTransmissionModeType.broadcast, uuid: userId));
  }

  Future<void> sendTranspose(int transpose, {bool noDebounce = false}) {
    if (!isStarted) throw BridgefyError(type: BridgefyErrorType.notStarted);

    if (noDebounce) {
      return _bridgefy.send(
          data: Uint8List.fromList([BridgefyService.MSG_TRANSPOSE, transpose + 12]),
          transmissionMode: BridgefyTransmissionMode(type: BridgefyTransmissionModeType.broadcast, uuid: userId));
    } else {
      _debounceTransposeTimer?.cancel();
      _debounceTransposeTimer = Timer(const Duration(seconds: 1), () {
        sendTranspose(transpose, noDebounce: true);
      });
      return Future.value();
    }
  }

  Future<void> sendSongId(int songId) {
    if (!isStarted) throw BridgefyError(type: BridgefyErrorType.notStarted);
    final bytes = ByteData(5);
    bytes.setUint8(0, BridgefyService.MSG_SONG_ID);
    bytes.setUint32(1, songId);
    return _bridgefy.send(
        data: bytes.buffer.asUint8List(),
        transmissionMode: BridgefyTransmissionMode(type: BridgefyTransmissionModeType.broadcast, uuid: userId));
  }

  void followDevice(NearDevice device) {
    //if (device.blocked) return;
    if (!NearDevice.followingList.contains(device.uuid)) {
      NearDevice.followingList.add(device.uuid);
    }
    notifyListeners();
  }

  void unfollowDevice(NearDevice device) {
    NearDevice.followingList.remove(device.uuid);
    notifyListeners();
  }
}

What am I doing wrong?
Thanks...

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.manudicri.praisetune" xmlns:tools="http://schemas.android.com/tools">
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.VIBRATE"/>
  <uses-permission android:name="com.android.vending.BILLING" />
  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
  <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  <uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"  
    android:maxSdkVersion="30"  
    tools:node="replace" />  
  <uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"  
    android:maxSdkVersion="32"  
    tools:node="replace" /> 
  <application android:label="PraiseTune" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true"
    android:enableOnBackInvokedCallback="true"
  
  >

    <meta-data android:name="com.bridgefy.sdk.API_KEY" android:value="<my_api_key>" />
      

    <activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" android:exported="true">
      <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme"/>
      <meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
        
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>

      <!-- App Link sample -->
      <intent-filter android:autoVerify="true">
          <action android:name="android.intent.action.VIEW" />
          <category android:name="android.intent.category.DEFAULT" />
          <category android:name="android.intent.category.BROWSABLE" />
          <data android:scheme="http" android:host="praisetune.com" android:pathPattern="/app/.*"/>
          <data android:scheme="https" android:host="praisetune.com" android:pathPattern="/app/.*" />
          <data android:scheme="http" android:host="praisetune.com" android:pathPattern="/songs/.*"/>
          <data android:scheme="https" android:host="praisetune.com" android:pathPattern="/songs/.*" />
      </intent-filter>
    </activity>
    <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
    <meta-data android:name="flutterEmbedding" android:value="2"/>
  </application>
</manifest>
flutter doctor -v
[✓] Flutter (Channel stable, 3.32.2, on macOS 15.4 24E248 darwin-arm64, locale it-IT) [599ms]
    • Flutter version 3.32.2 on channel stable at /Users/manudicri/Development/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 8defaa71a7 (5 days ago), 2025-06-04 11:02:51 -0700
    • Engine revision 1091508939
    • Dart version 3.8.1
    • DevTools version 2.45.1

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [2,2s]
    • Android SDK at /Users/manudicri/Library/Android/sdk
    • Platform android-35, build-tools 34.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 16.1) [1.456ms]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 16B40
    • CocoaPods version 1.16.2

[✓] Chrome - develop for the web [12ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.2) [12ms]
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694)

[✓] VS Code (version 1.100.3) [11ms]
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.112.0

[✓] Connected device (5 available) [7,7s]
    • AGS2 W09 (mobile)                    • JALBB20C04101156          • android-arm64  • Android 8.0.0 (API 26)
    • iPhone di Manuel (wireless) (mobile) • 00008110-00041D0034D9801E • ios            • iOS 18.5 22F76
    • iPad di Manuel (mobile)              • 00008101-0002406A14C1A01E • ios            • iOS 18.4.1 22E252
    • macOS (desktop)                      • macos                     • darwin-arm64   • macOS 15.4 24E248 darwin-arm64
    • Chrome (web)                         • chrome                    • web-javascript • Google Chrome 137.0.7151.69
    ! Error: Browsing on the local area network for Apple Watch di Manuel. Ensure the device is unlocked and discoverable via Bluetooth. (code -27)
    ! Error: Browsing on the local area network for iPad di Gabriel. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)

[✓] Network resources [783ms]
    • All expected network resources are available.

• No issues found!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions