LogoPortfolio
Home
Projects
Articles
Certificates
More
Contact
Back to Articles
KotlinAndroidFlutterPermissionBackgroundFlutter Plugin

Permission Handling: Accessing Activity to Request Permissions for Background Sync (Android) in Your Flutter Plugin

Published on May 24, 2025

Permission Handling: Accessing Activity to Request Permissions for Background Sync (Android) in Your Flutter Plugin

**Why Do Permissions Matter for Background Tasks?**

Mobile apps that perform work in the background (like syncing from a smartwatch) must still obey Android’s runtime permission model. In Android 12+ especially, Bluetooth and Location permissions are runtime permissions that must be explicitly granted by the user before use. For example, to scan or connect to Bluetooth devices you need and in your AndroidManifest.xml. If you don’t check for and request these at runtime, any background sync will fail or crash. Moreover, background-fetch tasks (e.g. using the background_fetch plugin) will still run in a paused app process, so your native plugin code must be prepared to handle missing permissions gracefully and prompt the user when needed.

For example, your plugin’s AndroidManifest might include:

<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_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

These entries (as shown above) declare that your app can use Bluetooth and location features. But on modern Android versions you still have to check at runtime whether the user has granted them, especially since background work cannot automatically bring up permission dialogs.

Flutter Plugin Setup

To handle background syncing, we’ll write a Flutter plugin with native Android code in Kotlin. First, add the necessary dependencies in your Flutter app’s pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  background_fetch: ^1.3.8
  permission_handler: ^10.6.0   # (not used, only to declare in pubspec as reference, logic is in Android)

We include permission_handler only to ensure the Flutter app knows about these permissions (it’s not strictly used in the native code below, but it can help check status from Dart). The real permission logic will be done in the plugin’s Android code.

Next, create a plugin (e.g. with flutter create --template=plugin my_smartwatch_sync) or add a new Android package in an existing plugin. In the plugin’s Android module, make sure your Gradle is set up for Kotlin and coroutines. In android/build.gradle set compileSdkVersion (e.g. 33) and enable Java 1.8 if needed:

android {
    compileSdkVersion 33
    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 33
    }
    kotlinOptions { 
        jvmTarget = "1.8" 
    }
}
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
}

Also be sure the plugin’s AndroidManifest.xml (usually under android/src/main/) contains the needed lines as shown above. This lets Android know which permissions might be needed. Your manifest might look like:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.smartwatchsync">
    <!-- Android 12+ Bluetooth & location permissions -->
    <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_CONNECT" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <!-- ... other config ... -->
</manifest>

These entries (summarized from Android docs) are required for scanning and connecting via Bluetooth, but do not automatically grant the user’s approval — you must ask at runtime.

Native Android Permission Logic in Flutter Plugins

The core of our solution is in the plugin’s Kotlin code. We implement the new Plugin API (v2 embedding) with an ActivityAware plugin to get an Activity context for showing dialogs. This allows us to check and request permissions from native code. Our plugin class will look something like this:

package com.example.smartwatchsync

import android.Manifest
import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import kotlinx.coroutines.*
class SmartwatchSyncPlugin: FlutterPlugin, MethodChannel.MethodCallHandler,
        ActivityAware, PluginRegistry.RequestPermissionsResultListener,
        PluginRegistry.ActivityResultListener {
    private var activity: Activity? = null
    private lateinit var channel: MethodChannel
    private val coroutineScope = CoroutineScope(Dispatchers.Main)
    companion object {
        private const val PERMISSION_REQUEST_CODE = 1001
        private const val REQUEST_ENABLE_BLUETOOTH = 1002
    }
    // Deferred results for async calls
    private var permissionResult: CompletableDeferred<Boolean>? = null
    private var bluetoothEnableResult: CompletableDeferred<Boolean>? = null
    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(binding.binaryMessenger, "smartwatch_sync")
        channel.setMethodCallHandler(this)
    }
    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }

Notice that the class implements ActivityAware. In onAttachedToActivity, we capture the Activity and register listeners for permission and activity results:

    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
        activity = binding.activity
        // Listen for permission and activity results
        binding.addRequestPermissionsResultListener(this)
        binding.addActivityResultListener(this)
    }
    override fun onDetachedFromActivity() {
        activity = null
    }
    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
        activity = binding.activity
        binding.addRequestPermissionsResultListener(this)
        binding.addActivityResultListener(this)
    }
    override fun onDetachedFromActivityForConfigChanges() {
        activity = null
    }

In onMethodCall, we respond to Flutter method calls (via MethodChannel). We might expose two methods, e.g. "ensurePermissions" to just prompt for missing permissions, and "syncData" to perform the background sync. Using Kotlin coroutines, we can launch work on the main thread and await permission requests without blocking:

      override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "ensurePermissions" -> {
                coroutineScope.launch {
                    val granted = ensurePermissions()
                    result.success(granted)
                }
            }
            "syncData" -> {
                coroutineScope.launch {
                    // Check and request permissions
                    val hasPerm = ensurePermissions()
                    if (!hasPerm) {
                        result.error("PERMISSION_DENIED", "Permissions not granted", null)
                        return@launch
                    }
                    // Check and enable Bluetooth
                    val btOk = ensureBluetoothEnabled()
                    if (!btOk) {
                        result.error("BLUETOOTH_OFF", "Bluetooth not enabled", null)
                        return@launch
                    }
                    // TODO: perform actual smartwatch data sync here
                    val data = fetchDataFromSmartwatch()
                    result.success(data)
                }
            }
            else -> result.notImplemented()
        }
    }

Here, ensurePermissions() is a suspend function that checks which permissions are missing and requests them. We use ContextCompat.checkSelfPermission and if anything isn’t granted, we call ActivityCompat.requestPermissions(...). We keep a CompletableDeferred to await the user’s response asynchronously. For example:

private suspend fun ensurePermissions(): Boolean {
        val currentActivity = activity ?: return false
        val needed = mutableListOf<String>()

        // Check Fine Location permission (required for Bluetooth scanning)
        if (ContextCompat.checkSelfPermission(currentActivity,
                Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            needed.add(Manifest.permission.ACCESS_FINE_LOCATION)
        }
        // On Android 12+, also check BLUETOOTH_SCAN and BLUETOOTH_CONNECT
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            if (ContextCompat.checkSelfPermission(currentActivity,
                    Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
                needed.add(Manifest.permission.BLUETOOTH_SCAN)
            }
            if (ContextCompat.checkSelfPermission(currentActivity,
                    Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                needed.add(Manifest.permission.BLUETOOTH_CONNECT)
            }
        }
        if (needed.isNotEmpty()) {
            // Ask the user for the permissions
            permissionResult = CompletableDeferred()
            ActivityCompat.requestPermissions(
                currentActivity,
                needed.toTypedArray(),
                PERMISSION_REQUEST_CODE
            )
            return permissionResult!!.await()
        }
        return true
    }

We also override onRequestPermissionsResult to capture the result. If our PERMISSION_REQUEST_CODE matches, we complete the deferred so the coroutine can resume:

override fun onRequestPermissionsResult(requestCode: Int,
                                            permissions: Array<out String>?,
                                            grantResults: IntArray?): Boolean {
        if (requestCode == PERMISSION_REQUEST_CODE) {
            // Check if all permissions were granted
            val granted = grantResults?.all { it == PackageManager.PERMISSION_GRANTED } == true
            permissionResult?.complete(granted)
            permissionResult = null
            return true
        }
        return false
    }

This pattern (taken from best-practice guides) allows us to suspend for the user’s response without blocking the plugin thread.

Enabling Bluetooth via Intent

Similarly, we may need to prompt the user to enable Bluetooth if it’s turned off. We do this by firing an Intent for BluetoothAdapter.ACTION_REQUEST_ENABLE, which displays the system dialog. We wrap this in another suspend function:

private suspend fun ensureBluetoothEnabled(): Boolean {
        val currentActivity = activity ?: return false
        val adapter = BluetoothAdapter.getDefaultAdapter()
        if (adapter == null || adapter.isEnabled) return true
        // Ask user to enable Bluetooth
        bluetoothEnableResult = CompletableDeferred()
        val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        currentActivity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BLUETOOTH)
        return bluetoothEnableResult!!.await()
    }

We then override onActivityResult to catch the user’s choice (OK or Cancel) and complete the deferred:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
        if (requestCode == REQUEST_ENABLE_BLUETOOTH) {
            val enabled = (resultCode == Activity.RESULT_OK)
            bluetoothEnableResult?.complete(enabled)
            bluetoothEnableResult = null
            return true
        }
        return false
    }

Putting it all together, our native Android code can now asynchronously check and request both permissions and Bluetooth status, and only proceed with syncing after everything is in place. This keeps the background task robust and user-friendly.

Dart Interface and Background Fetch Integration

On the Flutter (Dart) side, we provide a simple interface to call the native plugin methods. For example, in lib/smartwatch_sync.dart:

import 'package:flutter/services.dart';

class SmartwatchSyncPlugin {
  static const MethodChannel _channel =
      MethodChannel('smartwatch_sync');
  /// Ensure Bluetooth/Location permissions are granted.
  static Future<bool> ensurePermissions() async {
    final bool? granted = await _channel.invokeMethod<bool>('ensurePermissions');
    return granted == true;
  }
  /// Trigger a data sync. Returns the synced data string (dummy example).
  static Future<String> syncData() async {
    final String? data = await _channel.invokeMethod<String>('syncData');
    return data ?? '';
  }
}

With this interface, your Flutter app can call await SmartwatchSyncPlugin.syncData() from anywhere (including background tasks).

To actually run this in the background, we use the background_fetch plugin. In your Flutter main.dart, you might configure it like this:

import 'package:flutter/material.dart';
import 'package:background_fetch/background_fetch.dart';
import 'smartwatch_sync.dart';

/// This is the callback that background_fetch will invoke.
void backgroundFetchHeadlessTask(HeadlessTask task) async {
  print('[BackgroundFetch] Headless event received.');
  final data = await SmartwatchSyncPlugin.syncData();
  print('[BackgroundFetch] Synced data: $data');
  BackgroundFetch.finish(task.taskId);
}
void main() {
  runApp(MyApp());
  // Register the headless task
  BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
}
class MyApp extends StatefulWidget {
  @override Widget build(BuildContext context) => MaterialApp(home: Scaffold());
  @override
  void initState() {
    super.initState();
    // Configure background fetch to run periodically even if app is in background/terminated.
    BackgroundFetch.configure(
      BackgroundFetchConfig(
        minimumFetchInterval: 15,
        stopOnTerminate: false,
        enableHeadless: true,
      ),
      (String taskId) async {
        print("[BackgroundFetch] Event received: $taskId");
        final data = await SmartwatchSyncPlugin.syncData();
        print("Background fetch synced data: $data");
        BackgroundFetch.finish(taskId);
      }
    );
  }
}

Here we register a headless task so that even after the app is terminated, the native code can wake and run our sync method. This ties the background-fetch event to our plugin call. In practice, you must ensure the user has granted permissions (via your plugin) before relying on background fetch. You might do an initial permissions check in your app UI. The stopOnTerminate: false, enableHeadless: true flags ensure the fetch still works when the app is not in the foreground.

Testing the Background Sync

To test the setup:

  1. Run on a real device (Android emulators may not accurately simulate background fetch). Install the app and grant Bluetooth/Location permissions when prompted.

  2. Enable the plugin’s background fetch in debug logs. You can trigger a fetch event manually via ADB (useful during development):

    adb shell cmd jobscheduler run -f com.yourapplicationid.app 999

  • Replace com.yourapplicationid.app with your application actual package name. This forces a background fetch event

  • Observe logs. In logcat, you should see your print statements from the headless task, e.g. [BackgroundFetch] Synced data: .... Verify that the plugin correctly checked permissions (or logged an error) before syncing.

  • Simulate permission denial: If you deny a permission, the plugin will attempt to request it (bringing up the system dialog). Ensure you handle that gracefully and that your Flutter code catches the plugin’s error if needed.

By walking through these steps, you can validate that your Flutter plugin correctly handles background tasks and native Android permissions. The key is that all permission checks and requests happen in the native plugin using the Activity context, while Flutter simply triggers the sync when needed. This clean separation of concerns aligns with modern plugin best practices. Nevertheless, if you want to handle permissions in your application itself, you can use third party packages such as permission_handler. While code structures, optimizations and best practices may differ, you get the gist!

References

  • https://csdcorp.com/blog/coding/handling-permission-requests-in-android-for-flutter-plugin/

About

Professional portfolio showcasing my work, articles, and achievements.

Quick Links

  • Projects
  • Articles
  • Certificates
  • Contact

Connect

GitHubGitHubLinkedInMediumMedium

Subscribe

© 2026 Portfolio. All rights reserved.