Skip to content

Repository files navigation

███████╗ █████╗ ███████╗██╗   ██╗
██╔════╝██╔══██╗██╔════╝╚██╗ ██╔╝
█████╗  ███████║███████╗ ╚████╔╝ 
██╔══╝  ██╔══██║╚════██║  ╚██╔╝  
███████╗██║  ██║███████║   ██║   
╚══════╝╚═╝  ╚═╝╚══════╝   ╚═╝  

██████╗ ███████╗██████╗ ███╗   ███╗██╗███████╗███████╗██╗ ██████╗ ███╗   ██╗███████╗
██╔══██╗██╔════╝██╔══██╗████╗ ████║██║██╔════╝██╔════╝██║██╔═══██╗████╗  ██║██╔════╝
██████╔╝█████╗  ██████╔╝██╔████╔██║██║███████╗███████╗██║██║   ██║██╔██╗ ██║███████╗
██╔═══╝ ██╔══╝  ██╔══██╗██║╚██╔╝██║██║╚════██║╚════██║██║██║   ██║██║╚██╗██║╚════██║
██║     ███████╗██║  ██║██║ ╚═╝ ██║██║███████║███████║██║╚██████╔╝██║ ╚████║███████║
╚═╝     ╚══════╝╚═╝  ╚═╝╚═╝     ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝  ╚═══╝╚══════╝
                                                                            K T X

Permissions are still messy. This fixes that.


Version API Kotlin Compose Coroutines License


EasyPermissions-KTX is a Kotlin-first Android permission library built on top of AndroidX
ActivityResultContracts. It covers every permission scenario — single, multiple, background
location, photo picker — through four cohesive APIs: Callback DSL, Suspend, Flow, and Compose.


✨ Features

Feature Details
🎯 Callback / DSL API Fluent .permissions().onGranted{}.check() builder
Suspend API val result = requestPermission(CAMERA) — awaitable in coroutines
🌊 Flow API Cold Flow<PermissionResult> per permission
🎨 Compose API rememberPermissionState() / rememberMultiplePermissionsState()
📍 Background Location Automatic two-step flow (foreground → background) for Android 10+
🖼️ Photo Picker Wraps PickVisualMedia with graceful fallback to ACTION_GET_CONTENT
🔒 Permanent Denial Tracks "Don't ask again" via SharedPreferences — not just shouldShowRationale
♻️ Lifecycle-aware Compose state auto-refreshes when user returns from system Settings
🏗️ No boilerplate Invisible Fragment handles all ActivityResultLauncher registrations
🧪 Tested Unit tests (Robolectric) + Instrumented tests for all core logic

📦 Installation

Step 1 — Add to settings.gradle.kts:

include(":easypermissions-ktx")

Step 2 — Add to your module's build.gradle.kts:

dependencies {
    // EasyPermissions-KTX v1.0.0
    implementation(project(":easypermissions-ktx"))
}

Step 3 — Your activity must extend FragmentActivity (or its subclass AppCompatActivity):

// ✅ Correct
class MainActivity : FragmentActivity() { ... }
class MainActivity : AppCompatActivity() { ... }  // AppCompatActivity extends FragmentActivity

// ❌ Won't work — ComponentActivity is not supported
class MainActivity : ComponentActivity() { ... }

JitPack / Maven Central publishing coming soon. Tag v1.0.0 on GitHub to trigger a JitPack build.


🚀 Quick Start

Declare permissions in AndroidManifest.xml

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

The library does not declare permissions automatically — you control your manifest.


📖 API Guide

1 · Callback DSL

The classic builder pattern. Works from FragmentActivity or Fragment.

// Single permission
EasyPermissions.with(this)
    .permissions(Manifest.permission.CAMERA)
    .rationale("Camera is needed to take photos")
    .onGranted           { startCamera() }
    .onDenied            { showRationaleDialog() }
    .onPermanentlyDenied { EasyPermissions.openAppSettings(this) }
    .check()

// Multiple permissions at once
EasyPermissions.with(this)
    .permissions(
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO
    )
    .onGranted { startVideoCall() }
    .onResult  { result ->
        if (result.anyPermanentlyDenied) {
            EasyPermissions.openAppSettings(this)
        }
    }
    .check()

2 · Suspend API

Best for sequential flows inside a coroutineScope or lifecycleScope.

// Single permission
lifecycleScope.launch {
    when (val result = requestPermission(Manifest.permission.CAMERA)) {
        is PermissionResult.Granted           -> startCamera()
        is PermissionResult.Denied            -> showRationale(result.shouldShowRationale)
        is PermissionResult.PermanentlyDenied -> EasyPermissions.openAppSettings(this@Activity)
    }
}

// Multiple permissions
lifecycleScope.launch {
    val result = requestPermissions(
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO
    )
    when {
        result.allGranted            -> startRecording()
        result.anyPermanentlyDenied  -> EasyPermissions.openAppSettings(this@Activity)
        result.anyDenied             -> showPartialDenialMessage(result.deniedPermissions)
    }
}

3 · Flow API

Useful when you want to react to permissions as a stream or chain them with other flows.

lifecycleScope.launch {
    // Single
    permissionFlow(Manifest.permission.CAMERA)
        .collect { result ->
            when (result) {
                is PermissionResult.Granted           -> startCamera()
                is PermissionResult.Denied            -> showRationale()
                is PermissionResult.PermanentlyDenied -> openSettings()
            }
        }

    // Multiple
    permissionsFlow(
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO
    ).collect { result ->
        if (result.allGranted) startRecording()
    }
}

4 · Compose API

Reactive permission state that integrates naturally with Compose's recomposition model. Status is automatically refreshed on every ON_RESUME event (e.g. user returning from Settings).

Single permission

@Composable
fun CameraScreen() {
    val camera = rememberPermissionState(
        permission = Manifest.permission.CAMERA,
        onPermissionResult = { granted -> analytics.log("camera_result", granted) }
    )

    when (val status = camera.status) {
        is PermissionStatus.Granted -> {
            CameraPreview()
        }
        is PermissionStatus.Denied -> {
            if (status.shouldShowRationale) {
                RationaleCard(
                    message = "Camera lets you take photos right in the app.",
                    onConfirm = { camera.launchPermissionRequest() }
                )
            } else {
                PermissionRequestButton(
                    text = "Enable Camera",
                    onClick = { camera.launchPermissionRequest() }
                )
            }
        }
    }
}

Multiple permissions

@Composable
fun RecordScreen() {
    val state = rememberMultiplePermissionsState(
        permissions = listOf(
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO
        )
    )

    if (state.allGranted) {
        RecordingContent()
    } else {
        Column {
            // Show individual status per permission
            state.permissionStates.forEach { perm ->
                PermissionRow(
                    name  = perm.permission.substringAfterLast('.'),
                    status = perm.status
                )
            }

            if (state.shouldShowRationale) {
                Text("Camera and microphone are required to record video.")
            }

            Button(onClick = { state.launchMultiplePermissionRequest() }) {
                Text("Grant Permissions")
            }
        }
    }
}

5 · Background Location

Android 10+ requires a mandatory two-step flow: foreground location → background location. BackgroundLocationManager handles the sequencing automatically, including the pre-Q fallback.

BackgroundLocationManager.with(this)
    .foregroundPermission(Manifest.permission.ACCESS_FINE_LOCATION) // default
    .onForegroundGranted  { showStatus("Foreground granted — requesting background…") }
    .onFullyGranted       { startGeofenceTracking() }
    .onForegroundDenied   { showError("Foreground location denied") }
    .onBackgroundDenied   { showError("Background location denied — go to Settings") }
    .onPermanentlyDenied  { EasyPermissions.openAppSettings(this) }
    .onResult             { result -> logEvent(result) }
    .request()

Android 9 and below: background location is not a separate concept.
onFullyGranted fires as soon as foreground location is granted.


6 · Photo Picker

Wraps the modern Android Photo Picker. Requires no permission on Android 13+; falls back to ACTION_GET_CONTENT automatically.

val picker = PhotoPickerManager.with(this)

// Single picks
picker.pickSingleImage  { uri -> uri?.let { display(it) } }
picker.pickSingleVideo  { uri -> uri?.let { play(it) }   }
picker.pickSingleMedia  { uri -> uri?.let { load(it) }   }

// Multiple picks
picker.pickMultipleImages { uris -> showGrid(uris) }
picker.pickMultipleVideos { uris -> buildPlaylist(uris) }
picker.pickMultipleMedia  { uris -> buildGallery(uris) }

// Custom media type
picker.pickSingle(PickVisualMedia.SingleMimeType("image/gif")) { uri -> ... }

// Check availability (informational — fallback is automatic)
if (picker.isPhotoPickerAvailable()) { /* modern picker will be used */ }

7 · Utility Helpers

// Check status without requesting
EasyPermissions.isGranted(context, Manifest.permission.CAMERA)
EasyPermissions.areAllGranted(context, Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)

// Send user to app Settings (for permanently denied permissions)
EasyPermissions.openAppSettings(context)

8 · Library Version

Query the library version at runtime:

import com.majid2851.easypermissions.LibraryVersion

Log.d("App", "EasyPermissions-KTX v${LibraryVersion.NAME} (code ${LibraryVersion.CODE})")
// → "EasyPermissions-KTX v1.0.0 (code 1)"

🔢 Versioning

Version metadata lives in a single place — gradle.properties:

# gradle.properties
libraryGroupId=com.majid2851
libraryArtifactId=easypermissions-ktx
libraryVersion=1.0.0
libraryVersionCode=1

These values are read by easypermissions-ktx/build.gradle.kts to set the Gradle group, version, and Maven publication coordinates. To cut a new release, update both gradle.properties and LibraryVersion.kt:

# gradle.properties — bump to 1.1.0
libraryVersion=1.1.0
libraryVersionCode=2
// LibraryVersion.kt — keep in sync
const val NAME: String = "1.1.0"
const val CODE: Int    = 2

Publish to local Maven repository

./gradlew :easypermissions-ktx:publishReleasePublicationToLocalMavenRepository

Output: build/maven-repo/com/majid2851/easypermissions-ktx/1.0.0/

Publish via JitPack

git tag v1.0.0
git push origin v1.0.0

Consumers can then depend on it as:

implementation("com.github.majid2851:EasyPermissions-KTX:v1.0.0")

📐 Architecture

easypermissions-ktx/
│
├── EasyPermissions.kt                    ← Public singleton entry point
│
├── model/
│   ├── PermissionResult.kt               ← sealed: Granted | Denied | PermanentlyDenied
│   ├── MultiplePermissionsResult.kt      ← allGranted / anyDenied / anyPermanentlyDenied
│   ├── PermissionStatus.kt               ← Compose: Granted | Denied(shouldShowRationale)
│   └── BackgroundLocationResult.kt       ← sealed: Granted | ForegroundDenied | BackgroundDenied
│
├── core/
│   ├── EasyPermissionsFragment.kt        ← Invisible Fragment — owns all ActivityResultLaunchers
│   ├── PermissionChecker.kt              ← Static helpers: isGranted / areAllGranted
│   └── PermissionPreferences.kt          ← SharedPrefs: tracks "asked before" per permission
│
├── builder/
│   └── PermissionRequestBuilder.kt       ← Fluent DSL (.permissions().onGranted{}.check())
│
├── extensions/
│   └── PermissionExtensions.kt           ← suspend requestPermission() + callbackFlow API
│
├── compose/
│   ├── PermissionState.kt                ← @Stable state for single permission
│   ├── MultiplePermissionsState.kt       ← @Stable state for multiple permissions
│   └── PermissionComposables.kt          ← rememberPermissionState / rememberMultiplePermissionsState
│
├── location/
│   └── BackgroundLocationManager.kt      ← Automatic two-step background location flow
│
└── photo/
    └── PhotoPickerManager.kt             ← PickVisualMedia wrapper with fallback

How it works

Your Activity / Fragment / Composable
         │
         ▼
  EasyPermissions.with(this)           ← public entry point
  requestPermission(CAMERA)            ← suspend extension
  rememberPermissionState(CAMERA)      ← Compose composable
         │
         ▼
  EasyPermissionsFragment              ← invisible Fragment, attached once per Activity
  (commitNow, tag: EasyPermissionsFragment)
         │
         ├── ActivityResultLauncher<String>           (single permission)
         ├── ActivityResultLauncher<Array<String>>    (multiple permissions)
         ├── ActivityResultLauncher<PickVisualMediaRequest> (single photo)
         └── ActivityResultLauncher<PickVisualMediaRequest> (multiple photos)
         │
         ▼
  Result delivered via callback → suspend continuation / Flow / Compose state

🧪 Testing

The library ships with 4 test suites covering all core logic:

Suite Type Runs on
PermissionResultTest Unit JVM
MultiplePermissionsResultTest Unit JVM
PermissionStatusTest Unit JVM
PermissionCheckerTest Unit (Robolectric) JVM
PermissionPreferencesTest Unit (Robolectric) JVM
PermissionCheckerInstrumentedTest Instrumented Device / Emulator
PermissionPreferencesInstrumentedTest Instrumented Device / Emulator

Run local unit tests

./gradlew :easypermissions-ktx:test

Run instrumented tests (requires device/emulator)

./gradlew :easypermissions-ktx:connectedAndroidTest

Test coverage areas

✅ PermissionResult        — sealed class properties (isGranted, isDenied, isPermanentlyDenied)
✅ MultiplePermissionsResult — computed booleans (allGranted, anyGranted, anyDenied, noneGranted)
✅ PermissionStatus         — Compose status sealed class, shouldShowRationale
✅ PermissionChecker        — isGranted / areAllGranted / getGranted / getDenied (real PackageManager)
✅ PermissionPreferences    — markAsked / hasAsked / isolation across permissions / persistence

📋 API Reference

PermissionResult (sealed)

sealed class PermissionResult {
    data class Granted(val permission: String)
    data class Denied(val permission: String, val shouldShowRationale: Boolean)
    data class PermanentlyDenied(val permission: String)
    
    val isGranted: Boolean
    val isDenied: Boolean
    val isPermanentlyDenied: Boolean
}

MultiplePermissionsResult

Property Type Description
grantedPermissions List<String> All permissions that were granted
deniedPermissions List<String> Denied but not permanently
permanentlyDeniedPermissions List<String> User selected "Don't ask again"
allGranted Boolean All permissions granted
anyGranted Boolean At least one granted
anyDenied Boolean At least one denied
anyPermanentlyDenied Boolean At least one permanently denied
noneGranted Boolean No permissions granted

PermissionStatus (Compose, sealed)

sealed class PermissionStatus {
    object Granted : PermissionStatus()
    data class Denied(override val shouldShowRationale: Boolean) : PermissionStatus()
    
    abstract val shouldShowRationale: Boolean
    val isGranted: Boolean
    val isDenied: Boolean
}

PermissionState (Compose)

Member Type Description
permission String The permission string
status PermissionStatus Reactive — triggers recomposition on change
launchPermissionRequest() fun Shows the system dialog

MultiplePermissionsState (Compose)

Member Type Description
permissions List<String> All requested permissions
permissionStates List<PermissionState> Individual reactive state per permission
allGranted Boolean All granted
anyGranted Boolean At least one granted
revokedPermissions List<PermissionState> Not-yet-granted states
shouldShowRationale Boolean At least one needs rationale
statusOf(permission) PermissionStatus Status for a specific permission
launchMultiplePermissionRequest() fun Shows all system dialogs

PermissionRequestBuilder DSL

Method Description
.permissions(vararg) Permissions to request
.rationale(message) Informational rationale string
.onGranted { } All granted callback
.onDenied { } At least one denied (not permanently)
.onPermanentlyDenied { } At least one permanently denied
.onResult { result } Full MultiplePermissionsResult
.onSingleResult { result } Single PermissionResult
.check() Execute the request

Suspend extensions (FragmentActivity / Fragment)

suspend fun FragmentActivity.requestPermission(permission: String): PermissionResult
suspend fun FragmentActivity.requestPermissions(vararg permissions: String): MultiplePermissionsResult

// Fragment overloads
suspend fun Fragment.requestPermission(permission: String): PermissionResult
suspend fun Fragment.requestPermissions(vararg permissions: String): MultiplePermissionsResult

Flow extensions (FragmentActivity / Fragment)

fun FragmentActivity.permissionFlow(permission: String): Flow<PermissionResult>
fun FragmentActivity.permissionsFlow(vararg permissions: String): Flow<MultiplePermissionsResult>

// Fragment overloads
fun Fragment.permissionFlow(permission: String): Flow<PermissionResult>
fun Fragment.permissionsFlow(vararg permissions: String): Flow<MultiplePermissionsResult>

LibraryVersion

Member Type Value
NAME String (const) "1.0.0"
CODE Int (const) 1

🔑 Permanent Denial Detection

shouldShowRequestPermissionRationale() alone cannot distinguish between:

State shouldShowRationale
First-time request false
After first denial true
After "Don't ask again" false

EasyPermissions-KTX stores whether each permission has been asked before in SharedPreferences under the key easy_permissions_ktx_prefs. The isPermanentlyDenied() check is:

isPermanentlyDenied = !isGranted && !shouldShowRationale && hasAsked

This means first-time denials are never misclassified as permanent.


⚙️ Requirements

Requirement Version / Note
Min SDK 24 (Android 7.0 Nougat)
Compile / Target SDK 34
Kotlin 1.9.0+
Jetpack Compose BOM 2023.08.00+
AndroidX Activity 1.8.0+
AndroidX Fragment 1.6.0+
Kotlin Coroutines 1.7.0+
Host Activity Must extend FragmentActivity (or AppCompatActivity)

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Write tests for any new behaviour
  4. Commit with a clear message: git commit -m "feat: add X"
  5. Push and open a Pull Request

Please follow the existing code style (Kotlin official style guide, no wildcard imports).


📄 License

Copyright 2024 Majid

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Made with ❤️ for the Android community

If this library saved you time, give it a ⭐

About

Easy way for handling permissions in android development

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors