ARTICLE AD BOX
When I run my Flutter app locally using flutter run, Bluetooth scanning works perfectly — all nearby devices are detected. However, when I publish the app to Google Play Store (closed testing, release AAB), no Bluetooth devices are detected at all, even though I have manually accepted every permission in the device settings.
I initially thought the issue was caused by a UUID filter I had in place (only scanning for devices with a specific UUID). I removed that filter and pushed a new release. The updated version works fine locally via flutter run, but the Play Store release still detects nothing.
Here are my codes. If you need anything else to debug this issue, let me know!
thanks
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- ── Bluetooth permissions (Android 12+ / SDK 31+) ── --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <!-- Legacy Bluetooth for older devices --> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <!-- Location required for BT scanning on all Android versions --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- Storage permissions for file import/export --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" /> <!-- Bluetooth feature declaration --> <uses-feature android:name="android.hardware.bluetooth_le" android:required="false" /> <application android:label="Brimbéloi" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:allowBackup="false" android:fullBackupContent="false"> <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <!-- Specifies an Android theme to apply to this Activity as soon as the Android process has started. This theme is visible to the user while the Flutter UI initializes. After that, this theme continues to determine the Window background behind the Flutter UI. --> <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </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> <!-- Required to query activities that can process text, see: https://developer.android.com/training/package-visibility and https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> <queries> <intent> <action android:name="android.intent.action.PROCESS_TEXT"/> <data android:mimeType="text/plain"/> </intent> </queries> </manifest>pubspec.yaml
name: brimbeloiv3 description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. version: 1.0.1+1 environment: sdk: ^3.11.1 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 dev_dependencies: flutter_test: sdk: flutter flutter_launcher_icons: ^0.13.1 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^6.0.0 provider: ^6.1.1 flutter_blue_plus: 1.32.4 flutter_reorderable_list: ^1.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true assets: - assets/images/ flutter_icons: android: "launcher_icon" ios: false image_path: "assets/images/icon.png" # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images # For details regarding adding assets from package dependencies, see # https://flutter.dev/to/asset-from-package # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-packagemain.dart
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'bluetooth_provider.dart'; import 'pattern_provider.dart'; import 'esp_file_provider.dart'; import 'settings_provider.dart'; import 'main_screen.dart'; import 'app_theme.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, ]); runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => SettingsProvider()), ChangeNotifierProvider(create: (_) => BluetoothProvider()), ChangeNotifierProvider(create: (_) => PatternProvider()), ChangeNotifierProvider(create: (_) => EspFileProvider()), ], child: const JiggerApp(), ), ); } class JiggerApp extends StatelessWidget { const JiggerApp({super.key}); @override Widget build(BuildContext context) { final settings = context.watch<SettingsProvider>(); return MaterialApp( title: 'Jigger Control', debugShowCheckedModeBanner: false, theme: AppTheme.theme, darkTheme: AppTheme.darkTheme, themeMode: settings.darkMode ? ThemeMode.dark : ThemeMode.light, home: const MainScreen(), ); } }bluetooth_provider.dart
import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'bt_device.dart'; class BluetoothProvider extends ChangeNotifier { // ── Jigger UUIDs ───────────────────────────────────────────── static const String serviceUuid = '4fafc201-1fb5-459e-8fcc-c5c9c331914b'; static const String characteristicUuid = 'beb5483e-36e1-4688-b7f5-ea07361b26a8'; // ── State ───────────────────────────────────────────────────── List<BtDevice> _devices = []; BtDevice? _selectedDevice; bool _isScanning = false; String? _errorMessage; final Map<String, String> _customNames = {}; StreamSubscription? _scanSubscription; StreamSubscription? _scanningSubscription; List<BtDevice> get devices => List.unmodifiable(_devices); BtDevice? get selectedDevice => _selectedDevice; bool get isScanning => _isScanning; String? get errorMessage => _errorMessage; // ── Scanning ────────────────────────────────────────────────── Future<void> startScan() async { _errorMessage = null; _devices = []; notifyListeners(); // Check + request Bluetooth (triggers permission dialog on first launch) final adapterState = await FlutterBluePlus.adapterState.first; if (adapterState != BluetoothAdapterState.on) { // On Android this will show the "turn on Bluetooth" system dialog try { await FlutterBluePlus.turnOn(); // Wait up to 3 seconds for BT to turn on await FlutterBluePlus.adapterState .where((s) => s == BluetoothAdapterState.on) .first .timeout(const Duration(seconds: 3)); } catch (_) { _errorMessage = 'Bluetooth désactivé. Veuillez l\'activer.'; notifyListeners(); return; } } // Always stop any existing scan first — prevents SCAN_FAILED_ALREADY_STARTED try { await FlutterBluePlus.stopScan(); } catch (_) {} // Cancel old subscriptions before creating new ones await _scanSubscription?.cancel(); await _scanningSubscription?.cancel(); _scanSubscription = null; _scanningSubscription = null; // Small delay to let the OS fully stop the previous scan await Future.delayed(const Duration(milliseconds: 300)); _isScanning = true; notifyListeners(); // Listen to scan results and filter by our service UUID _scanSubscription = FlutterBluePlus.onScanResults.listen( (results) { for (final r in results) { // UUID filter disabled — shows all BT devices // Uncomment below to filter by jigger service UUID only: // final advertisedUuids = r.advertisementData.serviceUuids // .map((u) => u.toString().toLowerCase()) // .toList(); // if (!advertisedUuids.contains(serviceUuid.toLowerCase())) continue; final id = r.device.remoteId.str; if (_devices.any((d) => d.id == id)) continue; final name = r.device.platformName.isNotEmpty ? r.device.platformName : (r.advertisementData.advName.isNotEmpty ? r.advertisementData.advName : 'Jigger inconnu'); final device = BtDevice(id: id, originalName: name); if (_customNames.containsKey(id)) { device.customName = _customNames[id]!; } _devices.add(device); notifyListeners(); } }, onError: (e) { _errorMessage = 'Erreur de scan: $e'; _isScanning = false; notifyListeners(); }, ); // Track when the scan finishes naturally _scanningSubscription = FlutterBluePlus.isScanning.listen((scanning) { if (!scanning && _isScanning) { _isScanning = false; notifyListeners(); } }); try { await FlutterBluePlus.startScan( // withServices: [Guid(serviceUuid)], // Uncomment to filter by UUID timeout: const Duration(seconds: 10), ); } catch (e) { _errorMessage = 'Impossible de démarrer le scan: $e'; _isScanning = false; notifyListeners(); } } Future<void> stopScan() async { try { await FlutterBluePlus.stopScan(); } catch (_) {} _isScanning = false; notifyListeners(); } // ── Selection ───────────────────────────────────────────────── void selectDevice(BtDevice? device) { _selectedDevice = device; notifyListeners(); } // ── Rename (in-memory) ──────────────────────────────────────── void renameDevice(String deviceId, String newName) { final trimmed = newName.trim(); _customNames[deviceId] = trimmed; final idx = _devices.indexWhere((d) => d.id == deviceId); if (idx >= 0) { _devices[idx] = _devices[idx].copyWith(customName: trimmed); if (_selectedDevice?.id == deviceId) { _selectedDevice = _devices[idx]; } } notifyListeners(); } // ── Command Sending (stub) ──────────────────────────────────── Future<void> sendCommand(String command) async { if (_selectedDevice == null) { debugPrint('[BT] Aucun appareil sélectionné.'); return; } debugPrint('[BT] → ${_selectedDevice!.displayName}: $command'); // TODO: connect + write to characteristicUuid } @override void dispose() { _scanSubscription?.cancel(); _scanningSubscription?.cancel(); FlutterBluePlus.stopScan(); super.dispose(); } }