
Production-ready permission manager eliminating lifecycle boilerplate and fragments; automatic rationale/settings dialogs, service checks for GPS/Bluetooth, dead-click and gallery permission edge-case fixes, thread-safe API.
A Modern Permission Library for Kotlin Multiplatform
Grant simplifies permission handling across Android and iOS with a clean, type-safe API. No Fragment/Activity required, no binding boilerplate, and built-in support for service checking (GPS, Bluetooth, etc.).
Key Features:
// 1️⃣ In your ViewModel
class CameraViewModel(grantManager: GrantManager) : ViewModel() {
val cameraGrant = GrantHandler(
grantManager = grantManager,
grant = AppGrant.CAMERA,
scope = viewModelScope
)
fun openCamera() {
cameraGrant.request {
// ✅ This runs ONLY when permission is granted
startCameraCapture()
}
}
}
// 2️⃣ In your Compose UI
@Composable
fun CameraScreen(viewModel: CameraViewModel) {
GrantDialog(handler = viewModel.cameraGrant) // Handles all dialogs automatically
Button(onClick = { viewModel.openCamera() }) {
Text("Take Photo")
}
}Simple and straightforward - no Fragment, no BindEffect, no manual configuration.
| Platform | Version | Notes |
|---|---|---|
| Android | API 24+ | Full support for Android 12, 13, 14 (Partial Gallery Access) |
| iOS | 13.0+ | Crash-guard & Main thread safety built-in |
| Compose | 1.7.1+ | Separate grant-compose module with GrantDialog |
💡 Note: See iOS Info.plist Setup and iOS Setup Guide for detailed configuration.
Grant currently provides full implementations for Android and iOS only. If your Kotlin Multiplatform project targets additional platforms like js (browser) or jvm (desktop), you cannot add Grant directly to commonMain as Gradle requires artifacts for all declared targets.
The best practice is to create a mobile-specific source set (e.g., mobileMain) that sits between commonMain and your Android/iOS source sets:
// shared/build.gradle.kts
kotlin {
// Define your targets
androidTarget()
iosArm64()
iosSimulatorArm64()
js() // Web target
jvm() // Desktop target
sourceSets {
// Create mobile-specific source set
val mobileMain by creating {
dependsOn(commonMain.get())
}
// Android and iOS depend on mobileMain
androidMain.get().dependsOn(mobileMain)
iosMain.get().dependsOn(mobileMain)
// Add Grant dependencies to mobileMain
mobileMain.dependencies {
implementation("dev.brewkits:grant-core:1.0.2")
implementation("dev.brewkits:grant-compose:1.0.2") // Optional
}
// Your JS/JVM code remains in commonMain without Grant
commonMain.dependencies {
// Other shared dependencies
}
}
}Benefits:
We're exploring native implementations for JS (Browser Permissions API) and JVM (macOS TCC) in future releases, rather than simple no-op stubs. This would provide real value for web and desktop applications.
Why not no-op stubs? Modern browsers and desktop platforms (macOS, Windows) have their own permission systems. A no-op implementation that returns
GRANTEDby default would be misleading and cause runtime failures when your app tries to access protected resources (camera, microphone, location). Any future implementation will properly integrate with platform-specific permission APIs.
For updates, follow Issue #19.
Run the demo app to see all 14 permissions in action:
./gradlew :demo:installDebug # Android
# Or open iosApp in Xcode for iOSTraditional permission handling requires extensive boilerplate and lifecycle management:
// ❌ TRADITIONAL: Fragment/Activity required + Boilerplate
class MyFragment : Fragment() {
private val permissionHelper = PermissionHelper(this) // Needs Fragment!
fun requestCamera() {
permissionHelper.bindToLifecycle() // BindEffect boilerplate
permissionHelper.request(Permission.CAMERA) {
// Complex state management
}
}
}// ✅ GRANT WAY: Works anywhere, zero boilerplate
@Composable
fun CameraScreen() {
val grantManager = remember { GrantFactory.create(context) }
Button(onClick = {
when (grantManager.request(AppGrant.CAMERA)) {
GrantStatus.GRANTED -> openCamera()
GrantStatus.DENIED -> showRationale()
GrantStatus.DENIED_ALWAYS -> openSettings()
}
}) { Text("Take Photo") }
}Simple, clean, and works anywhere.
iOS Info.plist Validation - Validates keys before calling native APIs
Android Process Death Recovery - Handles process death gracefully
Permissions don't guarantee services are enabled. Grant includes service status checking:
val serviceManager = ServiceFactory.create(context)
// ✅ Check if Location service is enabled (not just permission!)
when {
!serviceManager.isLocationEnabled() -> {
// GPS is OFF - guide user to enable it
serviceManager.openLocationSettings()
}
grantManager.checkStatus(AppGrant.LOCATION) == GrantStatus.GRANTED -> {
// Both permission AND service are ready!
startLocationTracking()
}
}
// ✅ Check Bluetooth service status
if (!serviceManager.isBluetoothEnabled()) {
serviceManager.openBluetoothSettings()
}Supported Services:
This helps you detect when users grant permission but forget to enable the required service.
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
// shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.brewkits:grant-core:1.0.2")
implementation("dev.brewkits:grant-compose:1.0.2") // Optional
}
}
}
⚠️ Important for Multi-Platform Projects: If your project targets platforms beyond Android and iOS (e.g.,js,jvm), do not add Grant tocommonMain. See Using with Web/Desktop Targets for the recommended configuration.
import dev.brewkits.grant.*
// 1. Create manager (in ViewModel, Repository, or Composable)
val grantManager = GrantFactory.create(context)
// 2. Check current status
suspend fun checkCameraAccess() {
when (grantManager.checkStatus(AppGrant.CAMERA)) {
GrantStatus.GRANTED -> println("Camera ready!")
GrantStatus.NOT_DETERMINED -> println("Never asked")
GrantStatus.DENIED -> println("User denied, can ask again")
GrantStatus.DENIED_ALWAYS -> println("Permanently denied, go to Settings")
}
}
// 3. Request permission
suspend fun requestCamera() {
val status = grantManager.request(AppGrant.CAMERA)
when (status) {
GrantStatus.GRANTED -> openCamera()
GrantStatus.DENIED -> showRationale()
GrantStatus.DENIED_ALWAYS -> showSettingsPrompt()
GrantStatus.NOT_DETERMINED -> { /* shouldn't happen after request */ }
}
}
// 4. Check Service Status (bonus feature!)
val serviceManager = ServiceFactory.create(context)
suspend fun requestLocationWithServiceCheck() {
// First check if Location service is enabled
if (!serviceManager.isLocationEnabled()) {
// Guide user to enable GPS
serviceManager.openLocationSettings()
return
}
// Then request permission
when (grantManager.request(AppGrant.LOCATION)) {
GrantStatus.GRANTED -> startLocationTracking() // Both permission AND service ready!
else -> showError()
}
}See Quick Start Guide for complete setup.
| Permission | Android | iOS | Notes |
|---|---|---|---|
CAMERA |
✅ API 23+ | ✅ iOS 13+ | Photo/Video capture |
MICROPHONE |
✅ API 23+ | ✅ iOS 13+ | Audio recording |
GALLERY |
✅ API 23+ | ✅ iOS 13+ | Images + Videos |
GALLERY_IMAGES_ONLY |
✅ API 33+ | ✅ iOS 13+ | Images only (prevents silent denial) |
GALLERY_VIDEO_ONLY |
✅ API 33+ | ✅ iOS 13+ | Videos only (prevents silent denial) |
STORAGE |
✅ API 23+ | N/A | External storage (deprecated) |
LOCATION |
✅ API 23+ | ✅ iOS 13+ | While app in use |
LOCATION_ALWAYS |
✅ API 29+ | ✅ iOS 13+ | Background location |
NOTIFICATION |
✅ API 33+ | ✅ iOS 13+ | Push notifications |
SCHEDULE_EXACT_ALARM |
✅ API 31+ | N/A | Exact alarm scheduling |
BLUETOOTH |
✅ API 31+ | ✅ iOS 13+ | BLE scanning/connecting |
CONTACTS |
✅ API 23+ | ✅ iOS 13+ | Read contacts |
MOTION |
✅ API 29+ | ✅ iOS 13+ | Activity recognition |
CALENDAR |
✅ API 23+ | ✅ iOS 13+ | Calendar events access |
Grant supports custom permissions through the RawPermission API. This allows you to use new OS permissions or platform-specific features without waiting for library updates.
AppGrant
// Use new Android permissions immediately
val predictiveBackPermission = RawPermission(
identifier = "PREDICTIVE_BACK",
androidPermissions = listOf("android.permission.PREDICTIVE_BACK"),
iosUsageKey = null // Android-only permission
)
suspend fun requestPredictiveBack() {
when (grantManager.request(predictiveBackPermission)) {
GrantStatus.GRANTED -> enablePredictiveBack()
else -> useFallback()
}
}// Use new iOS permissions
val healthKit = RawPermission(
identifier = "HEALTH_KIT",
androidPermissions = emptyList(), // iOS-only
iosUsageKey = "NSHealthShareUsageDescription"
)
val status = grantManager.request(healthKit)// Enterprise custom permission
val biometric = RawPermission(
identifier = "BIOMETRIC_AUTH",
androidPermissions = listOf("android.permission.USE_BIOMETRIC"),
iosUsageKey = "NSFaceIDUsageDescription"
)
// Works exactly like AppGrant.CAMERA
val handler = GrantHandler(
grantManager = grantManager,
grant = biometric, // RawPermission works seamlessly
scope = viewModelScope
)// Custom implementation for READ_MEDIA_VISUAL_USER_SELECTED (Android 14+)
val partialGallery = RawPermission(
identifier = "PARTIAL_GALLERY",
androidPermissions = listOf(
"android.permission.READ_MEDIA_IMAGES",
"android.permission.READ_MEDIA_VIDEO",
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
),
iosUsageKey = "NSPhotoLibraryUsageDescription"
)Grant uses a sealed interface architecture:
sealed interface GrantPermission {
val identifier: String
}
// Built-in permissions (type-safe, documented)
enum class AppGrant : GrantPermission {
CAMERA, LOCATION, MICROPHONE, ...
}
// Custom permissions (extensible, user-defined)
data class RawPermission(
override val identifier: String,
val androidPermissions: List<String>,
val iosUsageKey: String?
) : GrantPermissionDesign:
AppGrant enum for common permissions (type-safe)RawPermission for custom permissions (flexible)Platform Compatibility: You're responsible for checking API levels
if (Build.VERSION.SDK_INT >= 34) {
grantManager.request(android14Permission)
}Manifest Declaration: Remember to add permissions to AndroidManifest.xml
<uses-permission android:name="android.permission.YOUR_CUSTOM_PERMISSION" />iOS Info.plist: Add usage description keys
<key>NSYourCustomUsageDescription</key>
<string>We need this permission because...</string>| Feature | Grant | Other KMP Libraries | Native APIs |
|---|---|---|---|
| No Fragment/Activity | ✅ | Varies | ❌ |
| Info.plist Validation | ✅ | ❌ | ❌ |
| Process Death Recovery | ✅ | Limited | Manual |
| Custom Permissions | RawPermission | Limited | Full control |
| Service Checking | Built-in | Separate | Separate APIs |
| Android 14 Partial Gallery | ✅ | Varies | ✅ |
| Enum-Based Status | ✅ | Varies | Multiple APIs |
| Cross-Platform | Android + iOS | Android + iOS | Platform-specific |
Key differences:
GrantLogger.isEnabled = false
import dev.brewkits.grant.utils.GrantLogger
// Enable logging during development
GrantLogger.isEnabled = true
// ⚠️ IMPORTANT: Disable for production release
GrantLogger.isEnabled = falseLogging helps with:
// Integrate with your logging framework (Timber, Napier, etc.)
GrantLogger.logHandler = { level, tag, message ->
when (level) {
GrantLogger.LogLevel.ERROR -> Timber.e("[$tag] $message")
GrantLogger.LogLevel.WARNING -> Timber.w("[$tag] $message")
GrantLogger.LogLevel.INFO -> Timber.i("[$tag] $message")
GrantLogger.LogLevel.DEBUG -> Timber.d("[$tag] $message")
}
}We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Copyright 2026 BrewKits
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
http://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.
License: Apache 2.0 • Author: Nguyễn Tuấn Việt • BrewKits
A Modern Permission Library for Kotlin Multiplatform
Grant simplifies permission handling across Android and iOS with a clean, type-safe API. No Fragment/Activity required, no binding boilerplate, and built-in support for service checking (GPS, Bluetooth, etc.).
Key Features:
// 1️⃣ In your ViewModel
class CameraViewModel(grantManager: GrantManager) : ViewModel() {
val cameraGrant = GrantHandler(
grantManager = grantManager,
grant = AppGrant.CAMERA,
scope = viewModelScope
)
fun openCamera() {
cameraGrant.request {
// ✅ This runs ONLY when permission is granted
startCameraCapture()
}
}
}
// 2️⃣ In your Compose UI
@Composable
fun CameraScreen(viewModel: CameraViewModel) {
GrantDialog(handler = viewModel.cameraGrant) // Handles all dialogs automatically
Button(onClick = { viewModel.openCamera() }) {
Text("Take Photo")
}
}Simple and straightforward - no Fragment, no BindEffect, no manual configuration.
| Platform | Version | Notes |
|---|---|---|
| Android | API 24+ | Full support for Android 12, 13, 14 (Partial Gallery Access) |
| iOS | 13.0+ | Crash-guard & Main thread safety built-in |
| Compose | 1.7.1+ | Separate grant-compose module with GrantDialog |
💡 Note: See iOS Info.plist Setup and iOS Setup Guide for detailed configuration.
Grant currently provides full implementations for Android and iOS only. If your Kotlin Multiplatform project targets additional platforms like js (browser) or jvm (desktop), you cannot add Grant directly to commonMain as Gradle requires artifacts for all declared targets.
The best practice is to create a mobile-specific source set (e.g., mobileMain) that sits between commonMain and your Android/iOS source sets:
// shared/build.gradle.kts
kotlin {
// Define your targets
androidTarget()
iosArm64()
iosSimulatorArm64()
js() // Web target
jvm() // Desktop target
sourceSets {
// Create mobile-specific source set
val mobileMain by creating {
dependsOn(commonMain.get())
}
// Android and iOS depend on mobileMain
androidMain.get().dependsOn(mobileMain)
iosMain.get().dependsOn(mobileMain)
// Add Grant dependencies to mobileMain
mobileMain.dependencies {
implementation("dev.brewkits:grant-core:1.0.2")
implementation("dev.brewkits:grant-compose:1.0.2") // Optional
}
// Your JS/JVM code remains in commonMain without Grant
commonMain.dependencies {
// Other shared dependencies
}
}
}Benefits:
We're exploring native implementations for JS (Browser Permissions API) and JVM (macOS TCC) in future releases, rather than simple no-op stubs. This would provide real value for web and desktop applications.
Why not no-op stubs? Modern browsers and desktop platforms (macOS, Windows) have their own permission systems. A no-op implementation that returns
GRANTEDby default would be misleading and cause runtime failures when your app tries to access protected resources (camera, microphone, location). Any future implementation will properly integrate with platform-specific permission APIs.
For updates, follow Issue #19.
Run the demo app to see all 14 permissions in action:
./gradlew :demo:installDebug # Android
# Or open iosApp in Xcode for iOSTraditional permission handling requires extensive boilerplate and lifecycle management:
// ❌ TRADITIONAL: Fragment/Activity required + Boilerplate
class MyFragment : Fragment() {
private val permissionHelper = PermissionHelper(this) // Needs Fragment!
fun requestCamera() {
permissionHelper.bindToLifecycle() // BindEffect boilerplate
permissionHelper.request(Permission.CAMERA) {
// Complex state management
}
}
}// ✅ GRANT WAY: Works anywhere, zero boilerplate
@Composable
fun CameraScreen() {
val grantManager = remember { GrantFactory.create(context) }
Button(onClick = {
when (grantManager.request(AppGrant.CAMERA)) {
GrantStatus.GRANTED -> openCamera()
GrantStatus.DENIED -> showRationale()
GrantStatus.DENIED_ALWAYS -> openSettings()
}
}) { Text("Take Photo") }
}Simple, clean, and works anywhere.
iOS Info.plist Validation - Validates keys before calling native APIs
Android Process Death Recovery - Handles process death gracefully
Permissions don't guarantee services are enabled. Grant includes service status checking:
val serviceManager = ServiceFactory.create(context)
// ✅ Check if Location service is enabled (not just permission!)
when {
!serviceManager.isLocationEnabled() -> {
// GPS is OFF - guide user to enable it
serviceManager.openLocationSettings()
}
grantManager.checkStatus(AppGrant.LOCATION) == GrantStatus.GRANTED -> {
// Both permission AND service are ready!
startLocationTracking()
}
}
// ✅ Check Bluetooth service status
if (!serviceManager.isBluetoothEnabled()) {
serviceManager.openBluetoothSettings()
}Supported Services:
This helps you detect when users grant permission but forget to enable the required service.
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
// shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.brewkits:grant-core:1.0.2")
implementation("dev.brewkits:grant-compose:1.0.2") // Optional
}
}
}
⚠️ Important for Multi-Platform Projects: If your project targets platforms beyond Android and iOS (e.g.,js,jvm), do not add Grant tocommonMain. See Using with Web/Desktop Targets for the recommended configuration.
import dev.brewkits.grant.*
// 1. Create manager (in ViewModel, Repository, or Composable)
val grantManager = GrantFactory.create(context)
// 2. Check current status
suspend fun checkCameraAccess() {
when (grantManager.checkStatus(AppGrant.CAMERA)) {
GrantStatus.GRANTED -> println("Camera ready!")
GrantStatus.NOT_DETERMINED -> println("Never asked")
GrantStatus.DENIED -> println("User denied, can ask again")
GrantStatus.DENIED_ALWAYS -> println("Permanently denied, go to Settings")
}
}
// 3. Request permission
suspend fun requestCamera() {
val status = grantManager.request(AppGrant.CAMERA)
when (status) {
GrantStatus.GRANTED -> openCamera()
GrantStatus.DENIED -> showRationale()
GrantStatus.DENIED_ALWAYS -> showSettingsPrompt()
GrantStatus.NOT_DETERMINED -> { /* shouldn't happen after request */ }
}
}
// 4. Check Service Status (bonus feature!)
val serviceManager = ServiceFactory.create(context)
suspend fun requestLocationWithServiceCheck() {
// First check if Location service is enabled
if (!serviceManager.isLocationEnabled()) {
// Guide user to enable GPS
serviceManager.openLocationSettings()
return
}
// Then request permission
when (grantManager.request(AppGrant.LOCATION)) {
GrantStatus.GRANTED -> startLocationTracking() // Both permission AND service ready!
else -> showError()
}
}See Quick Start Guide for complete setup.
| Permission | Android | iOS | Notes |
|---|---|---|---|
CAMERA |
✅ API 23+ | ✅ iOS 13+ | Photo/Video capture |
MICROPHONE |
✅ API 23+ | ✅ iOS 13+ | Audio recording |
GALLERY |
✅ API 23+ | ✅ iOS 13+ | Images + Videos |
GALLERY_IMAGES_ONLY |
✅ API 33+ | ✅ iOS 13+ | Images only (prevents silent denial) |
GALLERY_VIDEO_ONLY |
✅ API 33+ | ✅ iOS 13+ | Videos only (prevents silent denial) |
STORAGE |
✅ API 23+ | N/A | External storage (deprecated) |
LOCATION |
✅ API 23+ | ✅ iOS 13+ | While app in use |
LOCATION_ALWAYS |
✅ API 29+ | ✅ iOS 13+ | Background location |
NOTIFICATION |
✅ API 33+ | ✅ iOS 13+ | Push notifications |
SCHEDULE_EXACT_ALARM |
✅ API 31+ | N/A | Exact alarm scheduling |
BLUETOOTH |
✅ API 31+ | ✅ iOS 13+ | BLE scanning/connecting |
CONTACTS |
✅ API 23+ | ✅ iOS 13+ | Read contacts |
MOTION |
✅ API 29+ | ✅ iOS 13+ | Activity recognition |
CALENDAR |
✅ API 23+ | ✅ iOS 13+ | Calendar events access |
Grant supports custom permissions through the RawPermission API. This allows you to use new OS permissions or platform-specific features without waiting for library updates.
AppGrant
// Use new Android permissions immediately
val predictiveBackPermission = RawPermission(
identifier = "PREDICTIVE_BACK",
androidPermissions = listOf("android.permission.PREDICTIVE_BACK"),
iosUsageKey = null // Android-only permission
)
suspend fun requestPredictiveBack() {
when (grantManager.request(predictiveBackPermission)) {
GrantStatus.GRANTED -> enablePredictiveBack()
else -> useFallback()
}
}// Use new iOS permissions
val healthKit = RawPermission(
identifier = "HEALTH_KIT",
androidPermissions = emptyList(), // iOS-only
iosUsageKey = "NSHealthShareUsageDescription"
)
val status = grantManager.request(healthKit)// Enterprise custom permission
val biometric = RawPermission(
identifier = "BIOMETRIC_AUTH",
androidPermissions = listOf("android.permission.USE_BIOMETRIC"),
iosUsageKey = "NSFaceIDUsageDescription"
)
// Works exactly like AppGrant.CAMERA
val handler = GrantHandler(
grantManager = grantManager,
grant = biometric, // RawPermission works seamlessly
scope = viewModelScope
)// Custom implementation for READ_MEDIA_VISUAL_USER_SELECTED (Android 14+)
val partialGallery = RawPermission(
identifier = "PARTIAL_GALLERY",
androidPermissions = listOf(
"android.permission.READ_MEDIA_IMAGES",
"android.permission.READ_MEDIA_VIDEO",
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
),
iosUsageKey = "NSPhotoLibraryUsageDescription"
)Grant uses a sealed interface architecture:
sealed interface GrantPermission {
val identifier: String
}
// Built-in permissions (type-safe, documented)
enum class AppGrant : GrantPermission {
CAMERA, LOCATION, MICROPHONE, ...
}
// Custom permissions (extensible, user-defined)
data class RawPermission(
override val identifier: String,
val androidPermissions: List<String>,
val iosUsageKey: String?
) : GrantPermissionDesign:
AppGrant enum for common permissions (type-safe)RawPermission for custom permissions (flexible)Platform Compatibility: You're responsible for checking API levels
if (Build.VERSION.SDK_INT >= 34) {
grantManager.request(android14Permission)
}Manifest Declaration: Remember to add permissions to AndroidManifest.xml
<uses-permission android:name="android.permission.YOUR_CUSTOM_PERMISSION" />iOS Info.plist: Add usage description keys
<key>NSYourCustomUsageDescription</key>
<string>We need this permission because...</string>| Feature | Grant | Other KMP Libraries | Native APIs |
|---|---|---|---|
| No Fragment/Activity | ✅ | Varies | ❌ |
| Info.plist Validation | ✅ | ❌ | ❌ |
| Process Death Recovery | ✅ | Limited | Manual |
| Custom Permissions | RawPermission | Limited | Full control |
| Service Checking | Built-in | Separate | Separate APIs |
| Android 14 Partial Gallery | ✅ | Varies | ✅ |
| Enum-Based Status | ✅ | Varies | Multiple APIs |
| Cross-Platform | Android + iOS | Android + iOS | Platform-specific |
Key differences:
GrantLogger.isEnabled = false
import dev.brewkits.grant.utils.GrantLogger
// Enable logging during development
GrantLogger.isEnabled = true
// ⚠️ IMPORTANT: Disable for production release
GrantLogger.isEnabled = falseLogging helps with:
// Integrate with your logging framework (Timber, Napier, etc.)
GrantLogger.logHandler = { level, tag, message ->
when (level) {
GrantLogger.LogLevel.ERROR -> Timber.e("[$tag] $message")
GrantLogger.LogLevel.WARNING -> Timber.w("[$tag] $message")
GrantLogger.LogLevel.INFO -> Timber.i("[$tag] $message")
GrantLogger.LogLevel.DEBUG -> Timber.d("[$tag] $message")
}
}We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Copyright 2026 BrewKits
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
http://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.
License: Apache 2.0 • Author: Nguyễn Tuấn Việt • BrewKits