├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── net │ │ └── ralphpina │ │ └── permissionsmanager │ │ └── sample │ │ ├── MainActivity.kt │ │ ├── PMApplication.kt │ │ └── PermissionsListAdapter.kt │ └── res │ ├── layout │ ├── main_activity.xml │ ├── permission.xml │ └── permission_group.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── bintray_library_config.gradle ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── permissions_manager ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── net │ └── ralphpina │ └── permissionsmanager │ ├── Permission.kt │ ├── PermissionResult.kt │ └── PermissionsManager.kt ├── permissions_manager_android ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── net │ │ │ └── ralphpina │ │ │ └── permissionsmanager │ │ │ └── android │ │ │ ├── AppLifecycleObserver.kt │ │ │ ├── Navigator.kt │ │ │ ├── PermissionsComponent.kt │ │ │ ├── PermissionsRationaleDelegate.kt │ │ │ ├── PermissionsRepository.kt │ │ │ ├── PermissionsRequestActivity.kt │ │ │ ├── PermissionsService.kt │ │ │ ├── RequestStatusRepository.kt │ │ │ └── foregroundutils │ │ │ ├── ActivityRepository.kt │ │ │ └── ForegroundUtils.kt │ └── res │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── net │ └── ralphpina │ └── permissionsmanager │ └── android │ ├── AndroidNavigatorTest.kt │ └── PermissionsRepositoryImplTest.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | /*/build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Intellij project files 30 | *.iml 31 | *.ipr 32 | *.iws 33 | .idea/ 34 | 35 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Permissions Manager 2 | Easily manage Android runtime permissions in API 23 `Marshmallow` and up. This library uses RXJava to skip all the painful parts of the `Activity`/`Fragment` lifecycle management. 3 | 4 | # Features 5 | - Reactive API 6 | - Usage in non-Android modules 7 | - Kotlin implementation 8 | - Support for "Don't ask again" 9 | - Catch missing `AndroidManifest.xml` permissions 10 | 11 | # Install 12 | APM is split into two modules. `permissions-manager` and `permissions-manager-android`. They can be used independently. 13 | 14 | `permissions-manager` provides the API to consume the library. 15 | 16 | `permissions-manager-android` provides `PermissionsComponent` to init the Android specific implementation of `PermissionsManager`. 17 | 18 | If your app is a monolith with a single `app` module, then you want to include both modules in your `app/build.gradle` like so: 19 | 20 | ```groovy 21 | dependencies { 22 | implemetation 'net.ralphpina.permissionsmanager:permissions-manager:3.0.1' 23 | implemetation 'net.ralphpina.permissionsmanager:permissions-manager-android:3.0.1' 24 | } 25 | ``` 26 | 27 | However, if your app is a multi-module project, you only need to include `permissions-manager` in the modules where you will consume the library. These modules could be Android or pure Kotlin/Java modules. 28 | 29 | Let's say you have an app with the following `settings.gradle`: 30 | ```groovy 31 | include ':app', 32 | ':feature1', // pure Kotlin module, no com.android.library plugin 33 | ':feature2' // Android module with com.android.library plugin 34 | ``` 35 | 36 | Let's say that you use Dagger to build and inject dependencies in your `app` module. In `feature1/build.gradle` and `feature2/build.gradle` you would use the library by including the `permissions-manager` package: 37 | ```groovy 38 | dependencies { 39 | implemetation 'net.ralphpina.permissionsmanager:permissions-manager-android:3.0.1' 40 | } 41 | ``` 42 | 43 | Then, in your `app/build.gradle` module you would include both packages to inject `PermissionsManager` using `PermissionsComponent`: 44 | ```groovy 45 | dependencies { 46 | implemetation 'net.ralphpina.permissionsmanager:permissions-manager:3.0.1' 47 | implemetation 'net.ralphpina.permissionsmanager:permissions-manager-android:3.0.1' 48 | } 49 | ``` 50 | 51 | # Initing 52 | Setting up the library the should be done once. If you are using Dagger you will want to provide it in one of your app scoped modules. Most likely in your `app` module. 53 | 54 | ```kotlin 55 | @Module 56 | class AppModule { 57 | @AppScope 58 | @Provides 59 | fun providePermissionsManager(context: Context): PermissionsManager = 60 | PermissionsComponent.Initializer() 61 | .context(context) 62 | .prepare() 63 | } 64 | ``` 65 | 66 | # Usage 67 | 1. Observe one of more permissions: 68 | ```kotlin 69 | permissionsManager.observe(Permission.Location.Fine) 70 | .doOnNext { 71 | println("Fine location granted: ${it[0].isGranted()}") 72 | } 73 | .subscribe() 74 | ``` 75 | 76 | For multiple permissions: 77 | ```kotlin 78 | permissionsManager.observe(Permission.Location.Fine, Permission.Location.Coarse) 79 | .doOnNext { 80 | println("${it[0].permission.value} granted: ${it[0].isGranted()}") 81 | println("${it[1].permission.value} granted: ${it[1].isGranted()}") 82 | } 83 | .subscribe() 84 | ``` 85 | 86 | 2. Request one of more permissions: 87 | ```kotlin 88 | permissionsManager.request(Permission.Location.Fine) 89 | .doOnSuccess { 90 | Toast.makeText( 91 | dataBinding.root.context, 92 | "Permission result for ${it[0].permission.value}, given: ${it[0].isGranted()}", 93 | Toast.LENGTH_SHORT 94 | ).show() 95 | } 96 | .subscribe() 97 | ``` 98 | 99 | For multiple permissions: 100 | ```kotlin 101 | permissionsManager.request(Permission.Location.Fine, Permission.Location.Coarse) 102 | .doOnSuccess { 103 | Toast.makeText( 104 | context, 105 | "Permission result for ${it[0].permission.value}, given: ${it[0].isGranted()} and ${it[1].permission.value}, given: ${it[1].isGranted()}", 106 | Toast.LENGTH_SHORT 107 | ).show() 108 | } 109 | .subscribe() 110 | ``` 111 | 112 | 3. Navigate to settings: 113 | ```kotlin 114 | permissionsManager.navigateToOsAppSettings() 115 | ``` 116 | 117 | That is the entirety of the API: 118 | ```kotlin 119 | interface PermissionsManager { 120 | fun observe(vararg permissions: Permission): Observable> 121 | fun request(vararg permissions: Permission): Single> 122 | fun navigateToOsAppSettings() 123 | } 124 | ``` 125 | 126 | `PermissionResult` provides the permission this result applies to, the result from the OS, and two flags, whether we've requested the permission before, and whether the user has selected "Don't ask again" for that or another permission in it's group. 127 | ```kotlin 128 | data class PermissionResult( 129 | val permission: Permission, 130 | val result: Result, 131 | val hasAskedForPermissions: Boolean, 132 | val isMarkedAsDontAsk: Boolean = false 133 | ) 134 | 135 | enum class Result { 136 | GRANTED, // PermissionChecker.PERMISSION_GRANTED 137 | DENIED, // PermissionChecker.PERMISSION_DENIED 138 | DENIED_APP_OP // PermissionChecker.PERMISSION_DENIED_APP_OP 139 | } 140 | ``` 141 | 142 | 143 | # License 144 | ``` 145 | Copyright 2019 Ralph Pina. 146 | 147 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 148 | 149 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 150 | 151 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 152 | ``` 153 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 28 7 | defaultConfig { 8 | applicationId "net.ralphpina.permissionsmanager.sample" 9 | minSdkVersion 21 10 | targetSdkVersion 28 11 | versionCode 3 12 | versionName "3.0" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled true 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | dataBinding { 22 | enabled = true 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation project(':permissions_manager') 28 | implementation project(':permissions_manager_android') 29 | 30 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 31 | 32 | implementation 'io.reactivex.rxjava2:rxjava:2.2.6' 33 | 34 | // Support Libs 35 | implementation 'androidx.appcompat:appcompat:1.1.0-alpha02' 36 | implementation 'com.google.android.material:material:1.0.0' 37 | implementation 'androidx.core:core-ktx:1.1.0-alpha04' 38 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 39 | implementation 'androidx.recyclerview:recyclerview:1.0.0' 40 | 41 | // TESTING 42 | testImplementation 'junit:junit:4.12' 43 | testImplementation 'org.assertj:assertj-core:3.0.0' 44 | 45 | // Force usage of support annotations in the test app, since it is internally used by the runner module. 46 | androidTestImplementation 'androidx.test:rules:1.1.1' 47 | androidTestImplementation 'androidx.test:runner:1.1.2-alpha01' 48 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.2-alpha01' 49 | implementation 'androidx.cardview:cardview:1.0.0' 50 | } 51 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/ralphpina/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 66 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/java/net/ralphpina/permissionsmanager/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package net.ralphpina.permissionsmanager.sample 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import kotlinx.android.synthetic.main.main_activity.* 6 | 7 | class MainActivity : AppCompatActivity() { 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | setContentView(R.layout.main_activity) 11 | setSupportActionBar(toolbar) 12 | recycler_view.adapter = PermissionsListAdapter(PMApplication.instance.permissionsManager) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/net/ralphpina/permissionsmanager/sample/PMApplication.kt: -------------------------------------------------------------------------------- 1 | package net.ralphpina.permissionsmanager.sample 2 | 3 | import android.app.Application 4 | import net.ralphpina.permissionsmanager.PermissionsManager 5 | import net.ralphpina.permissionsmanager.android.PermissionsComponent 6 | 7 | class PMApplication : Application() { 8 | 9 | lateinit var permissionsManager: PermissionsManager 10 | 11 | override fun onCreate() { 12 | super.onCreate() 13 | instance = this 14 | permissionsManager = PermissionsComponent.Initializer() 15 | .context(this) 16 | .prepare() 17 | } 18 | 19 | companion object { 20 | lateinit var instance: PMApplication 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/net/ralphpina/permissionsmanager/sample/PermissionsListAdapter.kt: -------------------------------------------------------------------------------- 1 | package net.ralphpina.permissionsmanager.sample 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.Toast 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.recyclerview.widget.RecyclerView 9 | import io.reactivex.disposables.Disposable 10 | import net.ralphpina.permissionsmanager.Permission 11 | import net.ralphpina.permissionsmanager.PermissionsManager 12 | import net.ralphpina.permissionsmanager.isGranted 13 | import net.ralphpina.permissionsmanager.sample.databinding.PermissionBinding 14 | import net.ralphpina.permissionsmanager.sample.databinding.PermissionGroupBinding 15 | 16 | sealed class PermissionListItem(view: View) : RecyclerView.ViewHolder(view) 17 | 18 | private class PermissionGroupViewHolder( 19 | private val dataBinding: PermissionGroupBinding 20 | ) : PermissionListItem(dataBinding.root) { 21 | fun bind(group: String) { 22 | dataBinding.group = group 23 | dataBinding.executePendingBindings() 24 | } 25 | } 26 | 27 | private class PermissionViewHolder( 28 | private val dataBinding: PermissionBinding, 29 | private val permissionsManager: PermissionsManager 30 | ) : PermissionListItem(dataBinding.root) { 31 | 32 | private var disposable: Disposable? = null 33 | 34 | fun bind(permission: Permission) { 35 | disposable?.dispose() 36 | dataBinding.settingsButton.setOnClickListener { permissionsManager.navigateToOsAppSettings() } 37 | dataBinding.requestPermissionButton.setOnClickListener { 38 | permissionsManager.request(permission) 39 | .doOnSuccess { 40 | Toast.makeText( 41 | dataBinding.root.context, 42 | "Permission result for ${it[0].permission}, given: ${it[0].isGranted()}", 43 | Toast.LENGTH_SHORT 44 | ).show() 45 | } 46 | .subscribe() 47 | } 48 | disposable = permissionsManager.observe(permission) 49 | .doOnNext { 50 | check(it.size == 1) { "List has no result!" } 51 | dataBinding.result = it[0] 52 | dataBinding.executePendingBindings() 53 | } 54 | .subscribe() 55 | } 56 | 57 | fun dispose() = disposable?.dispose() 58 | } 59 | 60 | private sealed class PermissionsModel { 61 | class Group(val title: String) : PermissionsModel() 62 | class Perm(val permission: Permission) : PermissionsModel() 63 | } 64 | 65 | private const val GROUP = 101 66 | private const val PERMISSION = 222 67 | 68 | class PermissionsListAdapter( 69 | private val permissionsManager: PermissionsManager 70 | ) : RecyclerView.Adapter() { 71 | 72 | private val models: List = buildViewModels() 73 | 74 | override fun getItemViewType(position: Int) = 75 | when (models[position]) { 76 | is PermissionsModel.Group -> GROUP 77 | is PermissionsModel.Perm -> PERMISSION 78 | } 79 | 80 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 81 | when (viewType) { 82 | GROUP -> { 83 | val binding: PermissionGroupBinding = DataBindingUtil.inflate( 84 | LayoutInflater.from(parent.context), 85 | R.layout.permission_group, 86 | parent, 87 | false 88 | ) 89 | PermissionGroupViewHolder(binding) 90 | } 91 | PERMISSION -> { 92 | val binding: PermissionBinding = DataBindingUtil.inflate( 93 | LayoutInflater.from(parent.context), 94 | R.layout.permission, 95 | parent, 96 | false 97 | ) 98 | PermissionViewHolder(binding, permissionsManager) 99 | } 100 | else -> throw IllegalStateException() 101 | } 102 | 103 | override fun getItemCount() = models.size 104 | 105 | override fun onBindViewHolder(holder: PermissionListItem, position: Int) = 106 | when (holder) { 107 | is PermissionGroupViewHolder -> holder.bind((models[position] as PermissionsModel.Group).title) 108 | is PermissionViewHolder -> holder.bind((models[position] as PermissionsModel.Perm).permission) 109 | } 110 | 111 | override fun onViewDetachedFromWindow(holder: PermissionListItem) { 112 | super.onViewDetachedFromWindow(holder) 113 | when (holder) { 114 | is PermissionViewHolder -> holder.dispose() 115 | } 116 | } 117 | } 118 | 119 | private fun buildViewModels() = listOf( 120 | PermissionsModel.Group("Calendar"), 121 | PermissionsModel.Perm(Permission.Calendar.Read), 122 | PermissionsModel.Perm(Permission.Calendar.Write), 123 | PermissionsModel.Group("CallLog"), 124 | PermissionsModel.Perm(Permission.CallLog.Read), 125 | PermissionsModel.Perm(Permission.CallLog.Write), 126 | PermissionsModel.Perm(Permission.CallLog.ProcessOutgoing), 127 | PermissionsModel.Group("Camera"), 128 | PermissionsModel.Perm(Permission.Camera), 129 | PermissionsModel.Group("Contacts"), 130 | PermissionsModel.Perm(Permission.Contacts.Read), 131 | PermissionsModel.Perm(Permission.Contacts.Write), 132 | PermissionsModel.Perm(Permission.Contacts.GetAccounts), 133 | PermissionsModel.Group("Location"), 134 | PermissionsModel.Perm(Permission.Location.Fine), 135 | PermissionsModel.Perm(Permission.Location.Coarse), 136 | PermissionsModel.Group("Microphone"), 137 | PermissionsModel.Perm(Permission.Microphone), 138 | PermissionsModel.Group("Phone"), 139 | PermissionsModel.Perm(Permission.Phone.ReadState), 140 | PermissionsModel.Perm(Permission.Phone.ReadNumbers), 141 | PermissionsModel.Perm(Permission.Phone.Call), 142 | PermissionsModel.Perm(Permission.Phone.Answer), 143 | PermissionsModel.Perm(Permission.Phone.AddVoiceMail), 144 | PermissionsModel.Perm(Permission.Phone.UseSip), 145 | PermissionsModel.Perm(Permission.Phone.AcceptHandover), 146 | PermissionsModel.Group("Sensors"), 147 | PermissionsModel.Perm(Permission.Sensors), 148 | PermissionsModel.Group("Sms"), 149 | PermissionsModel.Perm(Permission.Sms.Send), 150 | PermissionsModel.Perm(Permission.Sms.Receive), 151 | PermissionsModel.Perm(Permission.Sms.Read), 152 | PermissionsModel.Perm(Permission.Sms.ReceiveWapPush), 153 | PermissionsModel.Perm(Permission.Sms.ReceiveMms), 154 | PermissionsModel.Group("Storage"), 155 | PermissionsModel.Perm(Permission.Storage.ReadExternal), 156 | PermissionsModel.Perm(Permission.Storage.WriteExternal) 157 | ) 158 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/permission.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 20 | 21 | 25 | 26 | 38 | 39 | 50 | 51 | 61 | 62 | 72 | 73 | 83 | 84 | 94 | 95 | 105 | 106 |