├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── 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
│ │ │ ├── dimens.xml
│ │ │ ├── colors.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ ├── values-w820dp
│ │ │ └── dimens.xml
│ │ └── layout
│ │ │ ├── permission_group.xml
│ │ │ ├── main_activity.xml
│ │ │ └── permission.xml
│ │ ├── java
│ │ └── net
│ │ │ └── ralphpina
│ │ │ └── permissionsmanager
│ │ │ └── sample
│ │ │ ├── MainActivity.kt
│ │ │ ├── PMApplication.kt
│ │ │ └── PermissionsListAdapter.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── permissions_manager
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── net
│ └── ralphpina
│ └── permissionsmanager
│ ├── PermissionsManager.kt
│ ├── PermissionResult.kt
│ └── Permission.kt
├── permissions_manager_android
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ └── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── net
│ │ │ └── ralphpina
│ │ │ └── permissionsmanager
│ │ │ └── android
│ │ │ ├── PermissionsRationaleDelegate.kt
│ │ │ ├── AppLifecycleObserver.kt
│ │ │ ├── RequestStatusRepository.kt
│ │ │ ├── foregroundutils
│ │ │ ├── ForegroundUtils.kt
│ │ │ └── ActivityRepository.kt
│ │ │ ├── PermissionsService.kt
│ │ │ ├── Navigator.kt
│ │ │ ├── PermissionsComponent.kt
│ │ │ ├── PermissionsRequestActivity.kt
│ │ │ └── PermissionsRepository.kt
│ └── test
│ │ └── java
│ │ └── net
│ │ └── ralphpina
│ │ └── permissionsmanager
│ │ └── android
│ │ ├── AndroidNavigatorTest.kt
│ │ └── PermissionsRepositoryImplTest.kt
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── bintray_library_config.gradle
├── gradlew.bat
├── gradlew
└── README.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/permissions_manager/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/permissions_manager_android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':permissions_manager', ':permissions_manager_android'
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ralphpina/Android-Permissions-Manager/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Permissions Manager
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ralphpina/Android-Permissions-Manager/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ralphpina/Android-Permissions-Manager/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ralphpina/Android-Permissions-Manager/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ralphpina/Android-Permissions-Manager/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ralphpina/Android-Permissions-Manager/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Mar 05 15:27:37 PST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 | #FFFF0000
8 | #FF0000FF
9 | #FFFFFF00
10 | #FF00FF00
11 | #FF000000
12 |
13 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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/res/layout/permission_group.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
11 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/permissions_manager/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 | apply plugin: 'kotlin'
3 |
4 | ext {
5 | libraryName = 'permissions-manager'
6 | artifact = 'permissions-manager'
7 | }
8 |
9 | dependencies {
10 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
11 |
12 | implementation 'io.reactivex.rxjava2:rxjava:2.2.6'
13 |
14 | // TESTING
15 | testImplementation 'junit:junit:4.12'
16 | testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.3.21'
17 | testImplementation "com.google.truth:truth:0.43"
18 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
19 | }
20 |
21 | apply from: rootProject.file('bintray_library_config.gradle')
22 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/permissions_manager_android/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/permissions_manager/src/main/java/net/ralphpina/permissionsmanager/PermissionsManager.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager
2 |
3 | import io.reactivex.Observable
4 | import io.reactivex.Single
5 |
6 | /**
7 | * This is the main API for the SDK. It provides methods to observe and request permissions.
8 | */
9 | interface PermissionsManager {
10 | /**
11 | * Observe the state of permissions. You will get notified when there's new changes to it.
12 | */
13 | fun observe(vararg permissions: Permission): Observable>
14 |
15 | /**
16 | * Request a permission from the system. The Single will return the permissions result from the OS.
17 | */
18 | fun request(vararg permissions: Permission): Single>
19 |
20 | /**
21 | * Navigate to your app's settings. Here the user can manually change permissions.
22 | */
23 | fun navigateToOsAppSettings()
24 | }
25 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | android.useAndroidX=true
21 | android.enableJetifier=true
22 |
23 | kotlin.code.style=official
24 |
25 | android.databinding.enableV2=true
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/java/net/ralphpina/permissionsmanager/android/PermissionsRationaleDelegate.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android
2 |
3 | import androidx.core.app.ActivityCompat
4 | import net.ralphpina.permissionsmanager.Permission
5 | import net.ralphpina.permissionsmanager.android.foregroundutils.ForegroundUtils
6 |
7 | /**
8 | * Delegate to wrap the shouldShowRequestPermissionRationale call to the OS.
9 | */
10 | internal interface PermissionsRationaleDelegate {
11 | fun shouldShowRequestPermissionRationale(permission: Permission): Boolean
12 | }
13 |
14 | internal class AndroidPermissionsRationaleDelegate(
15 | private val foregroundUtils: ForegroundUtils
16 | ) : PermissionsRationaleDelegate {
17 | override fun shouldShowRequestPermissionRationale(permission: Permission): Boolean {
18 | // in the event this gets called without an Activity, we will just return false
19 | val activity = foregroundUtils.getActivity() ?: return false
20 | return ActivityCompat.shouldShowRequestPermissionRationale(
21 | activity,
22 | permission.value
23 | )
24 | }
25 | }
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/java/net/ralphpina/permissionsmanager/android/AppLifecycleObserver.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleObserver
5 | import androidx.lifecycle.OnLifecycleEvent
6 | import androidx.lifecycle.ProcessLifecycleOwner
7 |
8 | /**
9 | * Permissions may have changed while the app was backgrounded. Let's notify our repository to update
10 | * if needed. For example, the user granting a permissions via settings, then coming back to the app.
11 | */
12 | internal class AppLifecycleObserver(
13 | private val permissionsRepository: PermissionsRepository
14 | ) : LifecycleObserver {
15 | @OnLifecycleEvent(Lifecycle.Event.ON_START)
16 | fun onAppDidEnterForeground() = permissionsRepository.refreshPermissions()
17 |
18 | companion object {
19 | fun init(permissionsRepository: PermissionsRepository) =
20 | ProcessLifecycleOwner
21 | .get()
22 | .lifecycle
23 | .addObserver(
24 | AppLifecycleObserver(checkNotNull(permissionsRepository))
25 | )
26 | }
27 | }
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/java/net/ralphpina/permissionsmanager/android/RequestStatusRepository.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android
2 |
3 | import android.content.SharedPreferences
4 | import net.ralphpina.permissionsmanager.Permission
5 |
6 | internal interface RequestStatusRepository {
7 | /**
8 | * Set that a permission has been requested by the app. Regardless or not of
9 | * whether it was granted.
10 | */
11 | fun setHasAsked(permission: Permission)
12 |
13 | /**
14 | * Have we asked the user for the permission before.
15 | */
16 | fun getHasAsked(permission: Permission): Boolean
17 |
18 | /**
19 | * Clear the data in the database. From app's perspective no permissions have been requested.
20 | * This is probably not something you'll want to use outside of testing.
21 | */
22 | fun clearData()
23 | }
24 |
25 | internal class RequestStatusRepositoryImpl(private val preferences: SharedPreferences) :
26 | RequestStatusRepository {
27 |
28 | override fun setHasAsked(permission: Permission) =
29 | preferences.edit()
30 | .putBoolean(permission.value, true)
31 | .apply()
32 |
33 | override fun getHasAsked(permission: Permission) =
34 | preferences.getBoolean(permission.value, false)
35 |
36 | override fun clearData() = preferences.edit().clear().apply()
37 | }
38 |
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/java/net/ralphpina/permissionsmanager/android/foregroundutils/ForegroundUtils.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android.foregroundutils
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.content.Context
6 |
7 | interface ForegroundUtils {
8 | fun getForegroundedActivity(): Activity?
9 | fun getActivity(): Activity?
10 | }
11 |
12 | internal class ForegroundUtilsImpl(
13 | private val activityRepository: ActivityRepository
14 | ) : ForegroundUtils {
15 |
16 | override fun getForegroundedActivity(): Activity? = activityRepository.foregroundedActivity
17 |
18 | override fun getActivity(): Activity? = activityRepository.foregroundedActivity
19 | ?: activityRepository.startedActivity ?: activityRepository.createdActivity
20 | }
21 |
22 | object ForegroundUtilsComponent {
23 | class Builder {
24 | private lateinit var context: Context
25 |
26 | fun context(context: Context): Builder {
27 | this.context = context
28 | return this
29 | }
30 |
31 | fun build(): ForegroundUtils {
32 | val activityRepository = AndroidActivityRepository()
33 | (context.applicationContext as Application).registerActivityLifecycleCallbacks(activityRepository)
34 | return ForegroundUtilsImpl(activityRepository)
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/bintray_library_config.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.jfrog.bintray'
2 | apply plugin: 'com.github.dcendents.android-maven'
3 |
4 | ext {
5 |
6 | bintrayRepo = "maven"
7 | bintrayName = "permissions-manager"
8 |
9 | publishedGroupId = 'net.ralphpina.permissionsmanager'
10 |
11 | libraryDescription = 'Easily manage Android Marshmallow and up runtime permissions. This library is backwards compatible. In pre-Marshmallow devices permissions are returned as given.'
12 |
13 | siteUrl = 'https://github.com/ralphpina/Android-Permissions-Manager'
14 | gitUrl = 'https://github.com/ralphpina/Android-Permissions-Manager.git'
15 |
16 | // Make sure to bump versionCode and versionName in permissions_manager_android/build.gradle and README
17 | libraryVersion = '3.0.1'
18 |
19 | developerId = 'ralphpina'
20 | developerName = 'Ralph Pina'
21 | developerEmail = 'ralph.pina@gmail.com'
22 |
23 | licenseName = 'The MIT License'
24 | licenseUrl = 'https://opensource.org/licenses/MIT'
25 | allLicenses = ["MIT"]
26 | }
27 |
28 | //Add these lines to publish library to bintray. This is the readymade scripts made by github user nuuneoi to make uploading to bintray easy.
29 | //Place it at the end of the file
30 | if (project.rootProject.file('local.properties').exists()) {
31 | apply from: 'https://raw.githubusercontent.com/wajahatkarim3/JCenter-Gradle-Scripts/master/install.gradle'
32 | apply from: 'https://raw.githubusercontent.com/wajahatkarim3/JCenter-Gradle-Scripts/master/bintray.gradle'
33 | }
34 |
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/java/net/ralphpina/permissionsmanager/android/PermissionsService.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android
2 |
3 | import android.content.Context
4 | import android.content.pm.PackageManager
5 | import androidx.core.content.PermissionChecker
6 | import net.ralphpina.permissionsmanager.Permission
7 | import net.ralphpina.permissionsmanager.Result
8 |
9 | /**
10 | * Service to wrap calls to check permissions.
11 | */
12 | internal interface PermissionsService {
13 | val manifestPermissions: List
14 | fun checkPermission(permission: Permission): Result
15 | }
16 |
17 | internal class AndroidPermissionsService(private val context: Context) :
18 | PermissionsService {
19 |
20 | override val manifestPermissions: List by lazy {
21 | context
22 | .packageManager
23 | .getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
24 | .requestedPermissions
25 | .toList()
26 | }
27 |
28 | override fun checkPermission(permission: Permission) =
29 | PermissionChecker.checkSelfPermission(context, permission.value).mapToResults()
30 | }
31 |
32 | internal fun Int.mapToResults() =
33 | when(this) {
34 | PermissionChecker.PERMISSION_GRANTED -> Result.GRANTED
35 | PermissionChecker.PERMISSION_DENIED -> Result.DENIED
36 | PermissionChecker.PERMISSION_DENIED_APP_OP -> Result.DENIED_APP_OP
37 | else -> throw IllegalArgumentException("Permissions result passed an unknown value.")
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/main_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/permissions_manager_android/src/test/java/net/ralphpina/permissionsmanager/android/AndroidNavigatorTest.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import com.nhaarman.mockitokotlin2.*
6 | import net.ralphpina.permissionsmanager.Permission
7 | import net.ralphpina.permissionsmanager.android.foregroundutils.ForegroundUtils
8 | import kotlin.test.BeforeTest
9 | import kotlin.test.Test
10 |
11 | class AndroidNavigatorTest {
12 |
13 | private lateinit var context: Context
14 | private lateinit var foregroundUtils: ForegroundUtils
15 |
16 | private lateinit var navigator: net.ralphpina.permissionsmanager.android.Navigator
17 |
18 | @BeforeTest
19 | fun setup() {
20 | context = mock()
21 | foregroundUtils = mock()
22 |
23 | navigator = AndroidNavigator(context, foregroundUtils)
24 | }
25 |
26 | @Test
27 | fun `GIVEN app has activity WHEN navigating to permissions request THEN use activity`() {
28 | val activity = mock()
29 | whenever(foregroundUtils.getActivity()).thenReturn(activity)
30 |
31 | verify(activity, never()).startActivity(any())
32 | verify(context, never()).startActivity(any())
33 |
34 | navigator.navigateToPermissionRequestActivity(listOf(Permission.Camera))
35 |
36 | verify(activity, times(1)).startActivity(any())
37 | verify(context, never()).startActivity(any())
38 | }
39 |
40 | @Test
41 | fun `GIVEN app doesn't have activity WHEN navigating to permissions request THEN use context`() {
42 | verify(context, never()).startActivity(any())
43 |
44 | navigator.navigateToPermissionRequestActivity(listOf(Permission.Camera))
45 |
46 | verify(context, times(1)).startActivity(any())
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/java/net/ralphpina/permissionsmanager/android/Navigator.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.provider.Settings
7 | import net.ralphpina.permissionsmanager.Permission
8 | import net.ralphpina.permissionsmanager.android.foregroundutils.ForegroundUtils
9 |
10 | /**
11 | * Used to navigate to app settings and to our permissions requesting activity.
12 | * This is marked internal because we need to use [PermissionsRequestActivity]
13 | * for the SDK to properly work.
14 | */
15 | internal interface Navigator {
16 | fun navigateToPermissionRequestActivity(permissions: List)
17 | fun navigateToOsAppSettings()
18 | }
19 |
20 | internal class AndroidNavigator(
21 | private val context: Context,
22 | private val foregroundUtils: ForegroundUtils
23 | ) : Navigator {
24 | override fun navigateToPermissionRequestActivity(permissions: List) {
25 | val starter = foregroundUtils.getActivity() ?: context
26 | PermissionsRequestActivity.startActivity(
27 | starter,
28 | *permissions.map { it.value }.toTypedArray()
29 | )
30 | }
31 |
32 | /**
33 | * Open the app's settings page so the user could switch an activity.
34 | */
35 | override fun navigateToOsAppSettings() {
36 | val starter = foregroundUtils.getActivity() ?: context
37 | //Open the specific App Info page:
38 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).also {
39 | it.data = Uri.parse("package:" + context.packageName)
40 | }
41 | if (intent.resolveActivity(starter.packageManager) != null) {
42 | starter.startActivity(intent)
43 | } else {
44 | with (Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS)) {
45 | if (resolveActivity(starter.packageManager) != null) {
46 | starter.startActivity(this)
47 | }
48 | }
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/java/net/ralphpina/permissionsmanager/android/foregroundutils/ActivityRepository.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android.foregroundutils
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.os.Bundle
6 | import java.lang.ref.WeakReference
7 |
8 | internal interface ActivityRepository {
9 | val foregroundedActivity: Activity?
10 | val startedActivity: Activity?
11 | val createdActivity: Activity?
12 | }
13 |
14 | internal class AndroidActivityRepository : ActivityRepository, Application.ActivityLifecycleCallbacks {
15 |
16 | private var _foregroundActivityRef: WeakReference? = null
17 | override val foregroundedActivity: Activity?
18 | get() = _foregroundActivityRef?.get()
19 |
20 | private var _startedActivityRef: WeakReference? = null
21 | override val startedActivity: Activity?
22 | get() = _startedActivityRef?.get()
23 |
24 | private var _createdActivityRef: WeakReference? = null
25 | override val createdActivity: Activity?
26 | get() = _createdActivityRef?.get()
27 |
28 | override fun onActivityResumed(activity: Activity) {
29 | _foregroundActivityRef = WeakReference(activity)
30 | }
31 |
32 | override fun onActivityPaused(activity: Activity) {
33 | if (_foregroundActivityRef?.get() == activity) {
34 | _foregroundActivityRef = null
35 | }
36 | }
37 |
38 | override fun onActivityStarted(activity: Activity) {
39 | _startedActivityRef = WeakReference(activity)
40 | }
41 |
42 | override fun onActivityStopped(activity: Activity) {
43 | if (_startedActivityRef?.get() == activity) {
44 | _startedActivityRef = null
45 | }
46 | }
47 |
48 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
49 | _createdActivityRef = WeakReference(activity)
50 | }
51 |
52 | override fun onActivityDestroyed(activity: Activity) {
53 | if (_createdActivityRef?.get() == activity) {
54 | _createdActivityRef = null
55 | }
56 | }
57 |
58 | override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {
59 | }
60 | }
--------------------------------------------------------------------------------
/permissions_manager_android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 |
4 | /**
5 | * Fixes this error:
6 | *
7 | * FAILURE: Build failed with an exception.
8 | *
9 | * * What went wrong:
10 | * Execution failed for task ':permissions_manager_android:javadoc'.
11 | * > Javadoc generation failed. Generated Javadoc options file (useful for troubleshooting):
12 | * '/Users/ralphpina/Documents/Android-Permissions-Manager/permissions_manager_android/build/tmp/javadoc/javadoc.options'
13 | */
14 | tasks.withType(Javadoc).all { enabled = false }
15 |
16 | ext {
17 | libraryName = 'permissions-manager-android'
18 | artifact = 'permissions-manager-android'
19 | }
20 |
21 | android {
22 | compileSdkVersion 28
23 |
24 | defaultConfig {
25 | minSdkVersion 21
26 | targetSdkVersion 28
27 | versionCode 4
28 | versionName "3.0.1"
29 |
30 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
31 |
32 | }
33 |
34 | buildTypes {
35 | release {
36 | minifyEnabled false
37 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
38 | }
39 | }
40 |
41 | dataBinding {
42 | enabled = true
43 | }
44 |
45 | // gets rid of this message:
46 | // java.lang.RuntimeException: Method putExtra in android.content.Intent not mocked. See http://g.co/androidstudio/not-mocked for details.
47 | testOptions {
48 | unitTests.returnDefaultValues = true
49 | }
50 |
51 | }
52 |
53 | dependencies {
54 | implementation project(':permissions_manager')
55 |
56 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
57 |
58 | implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
59 |
60 | implementation 'io.reactivex.rxjava2:rxjava:2.2.6'
61 |
62 | implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0-alpha03'
63 |
64 | // TESTING
65 | testImplementation 'junit:junit:4.12'
66 | testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.3.21'
67 | testImplementation "com.google.truth:truth:0.43"
68 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
69 | }
70 |
71 | apply from: rootProject.file('bintray_library_config.gradle')
72 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/java/net/ralphpina/permissionsmanager/android/PermissionsComponent.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android
2 |
3 | import android.content.Context
4 | import net.ralphpina.permissionsmanager.PermissionsManager
5 | import net.ralphpina.permissionsmanager.android.foregroundutils.ForegroundUtilsComponent
6 |
7 | private const val PERMISSIONS_SHARED_PREFS_STORAGE = "permissions_shared_prefs_storage"
8 |
9 | /**
10 | * This component is used to provide dependencies the SDK needs under the hood. It is also
11 | * used to init it. This class would be used to wire up [PermissionsManager] into Dagger
12 | * or another DI framework.
13 | */
14 | object PermissionsComponent {
15 |
16 | private var _permissionsRepository: PermissionsRepository? = null
17 | internal val permissionsRepository: PermissionsRepository
18 | get() = checkNotNull(_permissionsRepository) {
19 | "PermissionsManager has not been built yet. Use PermissionsComponent.Builder."
20 | }
21 |
22 | /**
23 | * Initialize the permissions SDK. You can use this to customize the [PermissionsManager] implementation.
24 | * The only requirement is to pass a [Context].
25 | */
26 | class Initializer {
27 | private var context: Context? = null
28 |
29 | /**
30 | * Required to initialize the [PermissionsManager]. Should be your application context.
31 | */
32 | fun context(context: Context): Initializer {
33 | this.context = context
34 | return this
35 | }
36 |
37 | /**
38 | * Should only be called once to get an instance of [PermissionsManager] to inject/use in your app.
39 | */
40 | fun prepare(): PermissionsManager {
41 | check(PermissionsComponent._permissionsRepository == null) {
42 | "PermissionsManager has been initiated. This build method has been called els ewhere."
43 | }
44 | val c = checkNotNull(context) { "You must pass a context to your buider." }
45 | val foregroundUtils = ForegroundUtilsComponent.Builder().context(c).build()
46 | val sharedPreferences = c.getSharedPreferences(PERMISSIONS_SHARED_PREFS_STORAGE, Context.MODE_PRIVATE)
47 |
48 | _permissionsRepository = PermissionsRepositoryImpl(
49 | navigator = AndroidNavigator(c, foregroundUtils),
50 | requestStatusRepository = RequestStatusRepositoryImpl(sharedPreferences),
51 | permissionsService = AndroidPermissionsService(c),
52 | permissionsRationaleDelegate = AndroidPermissionsRationaleDelegate(foregroundUtils)
53 | )
54 |
55 | AppLifecycleObserver.init(checkNotNull(_permissionsRepository))
56 |
57 | return checkNotNull(_permissionsRepository as PermissionsManager)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/permissions_manager/src/main/java/net/ralphpina/permissionsmanager/PermissionResult.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager
2 |
3 | /**
4 | * Result class wrapping the state or a requested permission.
5 | *
6 | * @param permission: The [Permission] this class is representing.
7 | *
8 | * @param result: whether the permission is granted or not, see [Result].
9 | *
10 | * @param hasAskedForPermissions: whether the user has previously asked for this permission. Keep in mind that
11 | * [hasAskedForPermissions] may be false, but [isMarkedAsDontAsk] may be true. See docs below for an explanation.
12 | *
13 | * @param isMarkedAsDontAsk: If a user ticks the "Don't ask again" box while denying a permission, the system will
14 | * automatically reject it whenever you try to request it again. In that situation, the only way a user can
15 | * grant the permission is to navigate to the app settings and then grant the permission.
16 | *
17 | * [isMarkedAsDontAsk] is hard to figure out because the OS does not give you an API to know if the user has
18 | * marked the permission as "Don't ask again".
19 | *
20 | * Another complication is that the [isMarkedAsDontAsk] flag could be determined by other permissions in
21 | * the same group, and these groups can change at any time. For example, currently, there's two permissions
22 | * in the LOCATION group: ACCESS_COARSE_LOCATION, and ACCESS_FINE_LOCATION. If you request ACCESS_COARSE_LOCATION
23 | * from the user, and they select "Don't ask again", then the system will automatically reject whenever you ask
24 | * for ACCESS_FINE_LOCATION. This will happen regardless of whether you have ever asked for ACCESS_FINE_LOCATION
25 | * in the past.
26 | *
27 | * An implementation that sets the [isMarkedAsDontAsk] flag on this class needs to take these
28 | * rules into account:
29 | * - is the permission granted? - if so, then `isMarkedAsDontAsk == false`. The system will automatically
30 | * grant the permission if asked regardless.
31 | * - If permission is not granted, then we need to look at two interrelated flags: [hasAskedForPermissions] and
32 | * [ActivityCompat.shouldShowRequestPermissionRationale]. And we need to look at this flag for all permissions
33 | * in the group of the permission we're looking at. Therefore, in order to know whether the OS will show the
34 | * permission dialog for ACCESS_FINE_LOCATION we need to check whether they clicked "Don't ask again" on either
35 | * ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION.
36 | *
37 | * Take a look at [PermissionsRepositoryImpl] where this is currently being calculated.
38 | */
39 | data class PermissionResult(
40 | val permission: Permission,
41 | val result: Result,
42 | val hasAskedForPermissions: Boolean,
43 | val isMarkedAsDontAsk: Boolean = false
44 | )
45 |
46 | /**
47 | * Represents the result from the user. Maps to the values in [PermissionChecker].
48 | */
49 | enum class Result {
50 | GRANTED, // PermissionChecker.PERMISSION_GRANTED
51 | DENIED, // PermissionChecker.PERMISSION_DENIED
52 | DENIED_APP_OP // PermissionChecker.PERMISSION_DENIED_APP_OP
53 | }
54 |
55 | fun PermissionResult.isGranted() = result == Result.GRANTED
56 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Permissions Manager
3 |
4 | Camera Permission
5 | Permission Status:
6 | Not given
7 | Given!!!
8 | Has Asked:
9 | No
10 | Yes
11 | Never ask again
12 | Go to app permissions
13 | Request Camera Permission
14 | Camera is granted, don\'t ask again!
15 | User denied us and said don\'t ask again!
16 | Location Permission
17 | Request Location Permission
18 | Request Audio Permission
19 | Audio Recording Permission
20 | Request Calendar Permission
21 | Calendar Permission
22 | Request Contacts Permission
23 | Contacts Permission
24 | Request Calling Permission
25 | Calling Permission
26 | Request Storage Permission
27 | Request Body Sensor Permission
28 | Request SMS Permission
29 | Storage Permission
30 | Body Sensors Permission
31 | SMS Permission
32 | Location is granted, don\'t ask again!
33 | Audio is granted, don\'t ask again!
34 | Calendar is granted, don\'t ask again!
35 | Contacts is granted, don\'t ask again!
36 | Calling is granted, don\'t ask again!
37 | Storage is granted, don\'t ask again!
38 | Body Sensor is granted, don\'t ask again!
39 | SMS is granted, don\'t ask again!
40 | Settings
41 | Request %s Permission
42 | Status:
43 | Has asked?
44 | Never ask again?
45 |
46 |
--------------------------------------------------------------------------------
/permissions_manager/src/main/java/net/ralphpina/permissionsmanager/Permission.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager
2 |
3 | /**
4 | * Maps to permission groups and permissions found here:
5 | * https://developer.android.com/guide/topics/permissions/overview#permission-groups
6 | *
7 | * However, not all permissions groups and their permissions are documented there.
8 | * So the AOSP codebase is a better source:
9 | * https://android.googlesource.com/platform/frameworks/base/+/master/core/res/AndroidManifest.xml
10 | */
11 | sealed class Permission(val value: String) {
12 | sealed class Calendar(value: String) : Permission(value) {
13 | object Read : Calendar("android.permission.READ_CALENDAR")
14 | object Write : Calendar("android.permission.WRITE_CALENDAR")
15 | }
16 |
17 | sealed class CallLog(value: String) : Permission(value) {
18 | object Read : CallLog("android.permission.READ_CALL_LOG")
19 | object Write : CallLog("android.permission.WRITE_CALL_LOG")
20 | object ProcessOutgoing : CallLog("android.permission.PROCESS_OUTGOING_CALLS")
21 | }
22 |
23 | // Uses the permission group name, since there's a single permission
24 | // If new permissions are added, we may want to change this.
25 | object Camera : Permission("android.permission.CAMERA")
26 |
27 | sealed class Contacts(value: String) : Permission(value) {
28 | object Read : Contacts("android.permission.READ_CONTACTS")
29 | object Write : Contacts("android.permission.WRITE_CONTACTS")
30 | object GetAccounts : Contacts("android.permission.GET_ACCOUNTS")
31 | }
32 |
33 | sealed class Location(value: String) : Permission(value) {
34 | object Fine : Location("android.permission.ACCESS_FINE_LOCATION")
35 | object Coarse : Location("android.permission.ACCESS_COARSE_LOCATION")
36 | }
37 |
38 | // Uses the permission group name, since there's a single permission
39 | // If new permissions are added, we may want to change this.
40 | object Microphone : Permission("android.permission.RECORD_AUDIO")
41 |
42 | sealed class Phone(value: String) : Permission(value) {
43 | object ReadState : Phone("android.permission.READ_PHONE_STATE")
44 | object ReadNumbers : Phone("android.permission.READ_PHONE_NUMBERS")
45 | object Call : Phone("android.permission.CALL_PHONE")
46 | object Answer : Phone("android.permission.ANSWER_PHONE_CALLS")
47 | object AddVoiceMail : Phone("com.android.voicemail.permission.ADD_VOICEMAIL")
48 | object UseSip : Phone("android.permission.USE_SIP")
49 | object AcceptHandover : Phone("android.permission.ACCEPT_HANDOVER")
50 | }
51 |
52 | // Uses the permission group name, since there's a single permission
53 | // If new permissions are added, we may want to change this.
54 | object Sensors : Permission("android.permission.BODY_SENSORS")
55 |
56 | sealed class Sms(value: String) : Permission(value) {
57 | object Send : Sms("android.permission.SEND_SMS")
58 | object Receive : Sms("android.permission.RECEIVE_SMS")
59 | object Read : Sms("android.permission.READ_SMS")
60 | object ReceiveWapPush : Sms("android.permission.RECEIVE_WAP_PUSH")
61 | object ReceiveMms : Sms("android.permission.RECEIVE_MMS")
62 | }
63 |
64 | sealed class Storage(value: String) : Permission(value) {
65 | object ReadExternal : Storage("android.permission.READ_EXTERNAL_STORAGE")
66 | object WriteExternal : Storage("android.permission.WRITE_EXTERNAL_STORAGE")
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/java/net/ralphpina/permissionsmanager/android/PermissionsRequestActivity.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android
2 |
3 | import android.Manifest
4 | import android.annotation.TargetApi
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.os.Build
8 | import android.os.Bundle
9 | import android.view.WindowManager
10 | import androidx.appcompat.app.AppCompatActivity
11 | import net.ralphpina.permissionsmanager.Permission
12 | import net.ralphpina.permissionsmanager.PermissionResult
13 |
14 | /**
15 | * The sole purpose of this Activity is to request a permission so we can get the callback via the
16 | * [onRequestPermissionsResult] callback, which then passes this to our [PermissionsRepository].
17 | *
18 | * This activity is transparent, so there's no UI for the user.
19 | */
20 | @TargetApi(Build.VERSION_CODES.M)
21 | internal class PermissionsRequestActivity : AppCompatActivity() {
22 |
23 | private val permissionsRepository by lazy {
24 | PermissionsComponent.permissionsRepository
25 | }
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 | window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
30 | if (savedInstanceState == null) {
31 | handleIntent(intent)
32 | }
33 | }
34 |
35 | override fun onNewIntent(intent: Intent) {
36 | super.onNewIntent(intent)
37 | handleIntent(intent)
38 | }
39 |
40 | private fun handleIntent(intent: Intent) {
41 | val permissions = intent.getStringArrayExtra(PERMISSIONS_KEY)
42 | requestPermissions(permissions,
43 | PERMISSIONS_REQUEST
44 | )
45 | }
46 |
47 | override fun onRequestPermissionsResult(requestCode: Int,
48 | permissions: Array,
49 | grantResults: IntArray) {
50 | // If request is cancelled, the result arrays are empty.
51 | if (requestCode == PERMISSIONS_REQUEST || permissions.isNotEmpty()) {
52 | val permissionResults = permissions
53 | .map { it.toPermission() }
54 | .mapIndexed { index, permission ->
55 | PermissionResult(
56 | permission,
57 | grantResults[index].mapToResults(),
58 | true
59 | )
60 | }
61 | permissionsRepository.update(permissionResults)
62 | }
63 | finish()
64 | overridePendingTransition(0, 0)
65 | }
66 |
67 | companion object {
68 | private const val PERMISSIONS_KEY = "permissions"
69 | private const val PERMISSIONS_REQUEST = 420
70 |
71 | @JvmStatic
72 | internal fun startActivity(context: Context, vararg permissions: String) =
73 | with(Intent(context, PermissionsRequestActivity::class.java)) {
74 | putExtra(PERMISSIONS_KEY, permissions)
75 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION
76 | context.startActivity(this)
77 | }
78 | }
79 | }
80 |
81 | /**
82 | * The OS callback after requesting permissions will pass the permission String, so we want to
83 | * convert it back into our types.
84 | */
85 | internal fun String.toPermission() =
86 | when (this) {
87 | Manifest.permission.READ_CALENDAR -> Permission.Calendar.Read
88 | Manifest.permission.WRITE_CALENDAR -> Permission.Calendar.Write
89 | Manifest.permission.READ_CALL_LOG -> Permission.CallLog.Read
90 | Manifest.permission.WRITE_CALL_LOG -> Permission.CallLog.Write
91 | Manifest.permission.PROCESS_OUTGOING_CALLS -> Permission.CallLog.ProcessOutgoing
92 | Manifest.permission.CAMERA -> Permission.Camera
93 | Manifest.permission.READ_CONTACTS -> Permission.Contacts.Read
94 | Manifest.permission.WRITE_CONTACTS -> Permission.Contacts.Write
95 | Manifest.permission.GET_ACCOUNTS -> Permission.Contacts.GetAccounts
96 | Manifest.permission.ACCESS_FINE_LOCATION -> Permission.Location.Fine
97 | Manifest.permission.ACCESS_COARSE_LOCATION -> Permission.Location.Coarse
98 | Manifest.permission.RECORD_AUDIO -> Permission.Microphone
99 | Manifest.permission.READ_PHONE_STATE -> Permission.Phone.ReadState
100 | Manifest.permission.READ_PHONE_NUMBERS -> Permission.Phone.ReadNumbers
101 | Manifest.permission.CALL_PHONE -> Permission.Phone.Call
102 | Manifest.permission.ANSWER_PHONE_CALLS -> Permission.Phone.Answer
103 | Manifest.permission.ADD_VOICEMAIL -> Permission.Phone.AddVoiceMail
104 | Manifest.permission.USE_SIP -> Permission.Phone.UseSip
105 | Manifest.permission.ACCEPT_HANDOVER -> Permission.Phone.AcceptHandover
106 | Manifest.permission.BODY_SENSORS -> Permission.Sensors
107 | Manifest.permission.SEND_SMS -> Permission.Sms.Send
108 | Manifest.permission.RECEIVE_SMS -> Permission.Sms.Receive
109 | Manifest.permission.READ_SMS -> Permission.Sms.Read
110 | Manifest.permission.RECEIVE_WAP_PUSH -> Permission.Sms.ReceiveWapPush
111 | Manifest.permission.RECEIVE_MMS -> Permission.Sms.ReceiveMms
112 | Manifest.permission.READ_EXTERNAL_STORAGE -> Permission.Storage.ReadExternal
113 | Manifest.permission.WRITE_EXTERNAL_STORAGE -> Permission.Storage.WriteExternal
114 | else -> throw StringToPermissionParseException(this)
115 | }
116 |
117 | class StringToPermissionParseException(value: String) : Throwable("$value is not a known permission")
118 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/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 |
116 |
117 |
130 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/permissions_manager_android/src/main/java/net/ralphpina/permissionsmanager/android/PermissionsRepository.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android
2 |
3 | import io.reactivex.Observable
4 | import io.reactivex.Single
5 | import io.reactivex.subjects.BehaviorSubject
6 | import io.reactivex.subjects.SingleSubject
7 | import net.ralphpina.permissionsmanager.Permission
8 | import net.ralphpina.permissionsmanager.PermissionResult
9 | import net.ralphpina.permissionsmanager.PermissionsManager
10 | import net.ralphpina.permissionsmanager.Result
11 |
12 | internal interface PermissionsRepository {
13 | /**
14 | * Update permissions granted by the OS.
15 | */
16 | fun update(results: List)
17 |
18 | /**
19 | * Force refresh the permissions that are being listened to. This would be useful
20 | * when coming from the background. The user may change permissions outside of the app, and we
21 | * want to show these updates to potential listeners.
22 | */
23 | fun refreshPermissions()
24 | }
25 |
26 | internal class PermissionsRepositoryImpl(
27 | private val navigator: Navigator,
28 | private val requestStatusRepository: RequestStatusRepository,
29 | private val permissionsService: PermissionsService,
30 | private val permissionsRationaleDelegate: PermissionsRationaleDelegate
31 | ) : PermissionsRepository,
32 | PermissionsManager {
33 |
34 | private val observeSubjects = mutableMapOf, BehaviorSubject>>()
35 | // I wanted the response of the request() to come directly from the OS, not the map we may have above.
36 | // So I have a SingleSubject I return. As opposed to clients having to use something like
37 | // skip(1) if I returned the BehaviorSubject from observeSubjects.
38 | private val requestSubjects = mutableMapOf, SingleSubject>>()
39 |
40 | override fun observe(vararg permissions: Permission): Observable> =
41 | with(permissions.sortedBy { it.value }) {
42 | validatePermissions()
43 | if (observeSubjects.contains(this)) {
44 | checkNotNull(observeSubjects[this]).distinctUntilChanged()
45 | } else {
46 | val results = map { it.toResult() }
47 | val subject = BehaviorSubject.createDefault(results)
48 | observeSubjects[this] = subject
49 | subject.distinctUntilChanged()
50 | }
51 | }
52 |
53 | override fun request(vararg permissions: Permission): Single> {
54 | try {
55 | permissions.checkListedInManifest()
56 | permissions.validatePermissions()
57 | } catch (t: Throwable) {
58 | return Single.error(t)
59 | }
60 | permissions.markAllAsAsked()
61 | // if all permissions are already granted. Let's just return the results.
62 | return if (permissionsGranted(*permissions)) {
63 | // refresh, since the above method method calls permissionsService.checkPermission()
64 | // which may grant a permission in the system. For example, you are asking for
65 | // Permission.Storage.WriteExternal when you already have Permission.Storage.ReadExternal
66 | refreshPermissions()
67 | Single.just(permissions.map { it.toResult() })
68 | } else {
69 | with(permissions.sortedBy { it.value }) {
70 | if (requestSubjects.contains(this)) {
71 | checkNotNull(requestSubjects[this])
72 | } else {
73 | val subject = SingleSubject.create>()
74 | requestSubjects[this] = subject
75 | navigator.navigateToPermissionRequestActivity(this)
76 | subject
77 | }
78 | }
79 | }
80 | }
81 |
82 | override fun navigateToOsAppSettings() = navigator.navigateToOsAppSettings()
83 |
84 | override fun update(results: List) {
85 | val permissions = results.permissions()
86 | permissions.validatePermissions()
87 |
88 | // permissions have been updated. lets refresh all our caches
89 | refreshPermissions()
90 |
91 | // let's notify whomever requested the permissions
92 | with(permissions.sortedBy { it.value }) {
93 | requestSubjects.remove(this)?.onSuccess(results.toList())
94 | }
95 | }
96 |
97 | override fun refreshPermissions() =
98 | observeSubjects.keys.forEach { perms ->
99 | checkNotNull(observeSubjects[perms]).onNext(perms.map { it.toResult() })
100 | }
101 |
102 | private fun Array.markAllAsAsked() =
103 | forEach { requestStatusRepository.setHasAsked(it) }
104 |
105 | private fun permissionsGranted(vararg permissions: Permission): Boolean {
106 | for (i in permissions) {
107 | if (permissionsService.checkPermission(i) != Result.GRANTED) {
108 | return false
109 | }
110 | }
111 | return true
112 | }
113 |
114 | private fun Permission.toResult() =
115 | PermissionResult(
116 | this,
117 | result = permissionsService.checkPermission(this),
118 | hasAskedForPermissions = requestStatusRepository.getHasAsked(this),
119 | isMarkedAsDontAsk = isMarkedAsDontAsk()
120 | )
121 |
122 | /**
123 | * If a user ticks the "Don't ask again" box while denying a permission, the system will
124 | * automatically reject it whenever you select it. There are complex rules that determine the state
125 | * of this flag. They are documented in [PermissionResult.isMarkedAsDontAsk].
126 | *
127 | * If the permission is granted, just set this to false. Which is the default. Otherwise, check
128 | * each permission in the group:
129 | * - have we asked for that permission? If so, [PermissionsRationaleDelegate.shouldShowRequestPermissionRationale]
130 | * should return true. It returns false if we already have the permission, or if the user
131 | * checked "Don't ask again". So the only reason [RequestStatusRepository.getHasAsked] would return true, and
132 | * [PermissionsRationaleDelegate.shouldShowRequestPermissionRationale] returns false is when the user clicked
133 | * "Don't ask again".
134 | */
135 | private fun Permission.isMarkedAsDontAsk() =
136 | getPermissionsInGroup().any {
137 | permissionsService.checkPermission(it) != Result.GRANTED &&
138 | requestStatusRepository.getHasAsked(it) &&
139 | !permissionsRationaleDelegate.shouldShowRequestPermissionRationale(it)
140 | }
141 |
142 | private fun Array.checkListedInManifest() =
143 | forEach {
144 | if (!permissionsService.manifestPermissions.contains(it.value))
145 | throw PermissionNotRequestedInManifestException(it)
146 | }
147 | }
148 |
149 | /**
150 | * We need to know the permissions in a group to check if the user has checked "Don't ask again" for any of them.
151 | */
152 | private fun Permission.getPermissionsInGroup(): List =
153 | when (this) {
154 | is Permission.Calendar -> listOf(
155 | Permission.Calendar.Read,
156 | Permission.Calendar.Write
157 | )
158 | is Permission.CallLog -> listOf(
159 | Permission.CallLog.Read,
160 | Permission.CallLog.Write,
161 | Permission.CallLog.ProcessOutgoing
162 | )
163 | is Permission.Camera -> listOf(Permission.Camera)
164 | is Permission.Contacts -> listOf(
165 | Permission.Contacts.Read,
166 | Permission.Contacts.Write,
167 | Permission.Contacts.GetAccounts
168 | )
169 | is Permission.Location -> listOf(
170 | Permission.Location.Fine,
171 | Permission.Location.Coarse
172 | )
173 | is Permission.Microphone -> listOf(Permission.Microphone)
174 | is Permission.Phone -> listOf(
175 | Permission.Phone.ReadState,
176 | Permission.Phone.ReadNumbers,
177 | Permission.Phone.Call,
178 | Permission.Phone.Answer,
179 | Permission.Phone.AddVoiceMail,
180 | Permission.Phone.UseSip,
181 | Permission.Phone.AcceptHandover
182 | )
183 | is Permission.Sensors -> listOf(Permission.Sensors)
184 | is Permission.Sms -> listOf(
185 | Permission.Sms.Send,
186 | Permission.Sms.Receive,
187 | Permission.Sms.Read,
188 | Permission.Sms.ReceiveWapPush,
189 | Permission.Sms.ReceiveMms
190 | )
191 | is Permission.Storage -> listOf(
192 | Permission.Storage.ReadExternal,
193 | Permission.Storage.WriteExternal
194 | )
195 | }
196 |
197 | private fun List.permissions(): List = map { it.permission }
198 |
199 | private fun List.validatePermissions() =
200 | forEach { if (it.value.trim().isEmpty()) throw InvalidPermissionValueException(it) }
201 |
202 | private fun Array.validatePermissions() =
203 | forEach { if (it.value.trim().isEmpty()) throw InvalidPermissionValueException(it) }
204 |
205 | private class InvalidPermissionValueException(permission: Permission) :
206 | Throwable("No value passed for permission $permission")
207 |
208 | private class PermissionNotRequestedInManifestException(it: Permission) :
209 | Throwable("${it.value} has not been listed in the AndroidManifest file.")
210 |
--------------------------------------------------------------------------------
/permissions_manager_android/src/test/java/net/ralphpina/permissionsmanager/android/PermissionsRepositoryImplTest.kt:
--------------------------------------------------------------------------------
1 | package net.ralphpina.permissionsmanager.android
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import com.nhaarman.mockitokotlin2.*
5 | import net.ralphpina.permissionsmanager.Permission
6 | import net.ralphpina.permissionsmanager.PermissionResult
7 | import net.ralphpina.permissionsmanager.Result
8 | import kotlin.test.BeforeTest
9 | import kotlin.test.Test
10 |
11 | class PermissionsRepositoryImplTest {
12 |
13 | private lateinit var navigator: Navigator
14 | private lateinit var requestStatusRepository: RequestStatusRepository
15 | private lateinit var permissionsService: PermissionsService
16 | private lateinit var permissionsRationaleDelegate: PermissionsRationaleDelegate
17 |
18 | private lateinit var permissionRepository: PermissionsRepositoryImpl
19 |
20 | @BeforeTest
21 | fun setup() {
22 | navigator = mock()
23 | requestStatusRepository = mock()
24 | permissionsService = mock()
25 | permissionsRationaleDelegate = mock()
26 |
27 | permissionRepository = PermissionsRepositoryImpl(
28 | navigator,
29 | requestStatusRepository,
30 | permissionsService,
31 | permissionsRationaleDelegate
32 | )
33 | }
34 |
35 | @Test
36 | fun `GIVEN no permissions given WHEN observing THEN return results with no permission`() {
37 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.DENIED)
38 |
39 | permissionRepository.observe(Permission.Location.Coarse)
40 | .test()
41 | .assertNotTerminated()
42 | .assertValue(
43 | listOf(
44 | PermissionResult(
45 | Permission.Location.Coarse,
46 | result = Result.DENIED,
47 | hasAskedForPermissions = false
48 | )
49 | )
50 | )
51 | .dispose()
52 | }
53 |
54 | @Test
55 | fun `GIVEN permission is granted WHEN observing THEN return results with permission`() {
56 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.GRANTED)
57 |
58 | permissionRepository.observe(Permission.Location.Coarse)
59 | .test()
60 | .assertNotTerminated()
61 | .assertValue(
62 | listOf(
63 | PermissionResult(
64 | Permission.Location.Coarse,
65 | result = Result.GRANTED,
66 | hasAskedForPermissions = false
67 | )
68 | )
69 | )
70 | .dispose()
71 | }
72 |
73 | @Test
74 | fun `GIVEN some permissions are granted WHEN observing THEN return results with correct permission`() {
75 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.GRANTED)
76 | whenever(permissionsService.checkPermission(Permission.Location.Fine)).thenReturn(Result.DENIED)
77 |
78 | permissionRepository.observe(
79 | Permission.Location.Coarse,
80 | Permission.Location.Fine
81 | )
82 | .test()
83 | .assertNotTerminated()
84 | .assertValue(
85 | listOf(
86 | PermissionResult(
87 | Permission.Location.Coarse,
88 | result = Result.GRANTED,
89 | hasAskedForPermissions = false
90 | ),
91 | PermissionResult(
92 | Permission.Location.Fine,
93 | result = Result.DENIED,
94 | hasAskedForPermissions = false
95 | )
96 | )
97 | )
98 | .dispose()
99 | }
100 |
101 | @Test
102 | fun `GIVEN permissions are not listed in AndroidManifes WHEN requesting THEN return error`() {
103 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.GRANTED)
104 |
105 | permissionRepository.request(Permission.Location.Coarse)
106 | .test()
107 | .assertErrorMessage("${Permission.Location.Coarse.value} has not been listed in the AndroidManifest file.")
108 | .dispose()
109 | }
110 |
111 | @Test
112 | fun `GIVEN permissions are granted WHEN requesting THEN return results`() {
113 | whenever(permissionsService.manifestPermissions).thenReturn(listOf(Permission.Location.Coarse.value))
114 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.GRANTED)
115 |
116 | // haven't requested anything
117 | verify(navigator, never()).navigateToPermissionRequestActivity(any())
118 |
119 | permissionRepository.request(Permission.Location.Coarse)
120 | .test()
121 | .assertComplete()
122 | .assertValue(
123 | listOf(
124 | PermissionResult(
125 | Permission.Location.Coarse,
126 | result = Result.GRANTED,
127 | hasAskedForPermissions = false
128 | )
129 | )
130 | )
131 | .dispose()
132 |
133 | // haven't requested anything
134 | verify(navigator, never()).navigateToPermissionRequestActivity(any())
135 | }
136 |
137 | @Test
138 | fun `GIVEN some permissions are granted, others not WHEN requesting THEN request permissions`() {
139 | whenever(permissionsService.manifestPermissions).thenReturn(
140 | listOf(Permission.Location.Fine.value, Permission.Location.Coarse.value)
141 | )
142 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.GRANTED)
143 |
144 | // haven't requested anything
145 | verify(navigator, never()).navigateToPermissionRequestActivity(any())
146 |
147 | permissionRepository.request(
148 | Permission.Location.Fine,
149 | Permission.Location.Coarse
150 | )
151 | .test()
152 | .assertNotComplete()
153 | .assertNoValues()
154 | .dispose()
155 |
156 | // need to request
157 | verify(navigator, times(1)).navigateToPermissionRequestActivity(any())
158 | }
159 |
160 | @Test
161 | fun `GIVEN permissions WHEN requesting THEN mark them as asked`() {
162 | whenever(permissionsService.manifestPermissions).thenReturn(
163 | listOf(Permission.Location.Fine.value, Permission.Location.Coarse.value)
164 | )
165 | verify(requestStatusRepository, never()).setHasAsked(Permission.Location.Coarse)
166 | verify(requestStatusRepository, never()).setHasAsked(Permission.Location.Fine)
167 |
168 | permissionRepository.request(
169 | Permission.Location.Fine,
170 | Permission.Location.Coarse
171 | )
172 | .test()
173 | .assertNotComplete()
174 | .assertNoValues()
175 | .dispose()
176 |
177 | verify(requestStatusRepository, times(1)).setHasAsked(Permission.Location.Coarse)
178 | verify(requestStatusRepository, times(1)).setHasAsked(Permission.Location.Fine)
179 | }
180 |
181 | @Test
182 | fun `GIVEN request is in progress WHEN requesting THEN return same subject`() {
183 | whenever(permissionsService.manifestPermissions).thenReturn(
184 | listOf(Permission.Location.Fine.value, Permission.Location.Coarse.value)
185 | )
186 | assertThat(
187 | permissionRepository.request(
188 | Permission.Location.Fine,
189 | Permission.Location.Coarse
190 | )
191 | )
192 | .isSameAs(
193 | permissionRepository.request(
194 | Permission.Location.Fine,
195 | Permission.Location.Coarse
196 | )
197 | )
198 | }
199 |
200 | @Test
201 | fun `GIVEN request is in progress WHEN updating results THEN refresh the permissions cache`() {
202 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.GRANTED)
203 | whenever(permissionsService.checkPermission(Permission.Location.Fine)).thenReturn(Result.DENIED)
204 |
205 | val testObserver = permissionRepository.observe(
206 | Permission.Location.Coarse,
207 | Permission.Location.Fine
208 | )
209 | .test()
210 | .assertNotTerminated()
211 | .assertValueCount(1)
212 | .assertValue(
213 | listOf(
214 | PermissionResult(
215 | Permission.Location.Coarse,
216 | result = Result.GRANTED,
217 | hasAskedForPermissions = false
218 | ),
219 | PermissionResult(
220 | Permission.Location.Fine,
221 | result = Result.DENIED,
222 | hasAskedForPermissions = false
223 | )
224 | )
225 | )
226 |
227 | // let's pretend the system has granted this now
228 | whenever(permissionsService.checkPermission(Permission.Location.Fine)).thenReturn(Result.GRANTED)
229 | whenever(requestStatusRepository.getHasAsked(Permission.Location.Fine)).thenReturn(true)
230 | whenever(permissionsRationaleDelegate.shouldShowRequestPermissionRationale(Permission.Location.Fine)).thenReturn(
231 | true
232 | )
233 |
234 | permissionRepository.update(
235 | listOf(
236 | PermissionResult(
237 | Permission.Location.Fine,
238 | result = Result.GRANTED,
239 | hasAskedForPermissions = true
240 | )
241 | )
242 | )
243 |
244 | testObserver
245 | .assertValueCount(2)
246 | .assertValueAt(
247 | 1,
248 | listOf(
249 | PermissionResult(
250 | Permission.Location.Coarse,
251 | result = Result.GRANTED,
252 | hasAskedForPermissions = false
253 | ),
254 | PermissionResult(
255 | Permission.Location.Fine,
256 | result = Result.GRANTED,
257 | hasAskedForPermissions = true
258 | )
259 | )
260 | )
261 | .dispose()
262 | }
263 |
264 | @Test
265 | fun `GIVEN request is in progress WHEN updating results THEN noop if no one is observing`() {
266 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.GRANTED)
267 | whenever(permissionsService.checkPermission(Permission.Location.Fine)).thenReturn(Result.DENIED)
268 |
269 | permissionRepository.update(
270 | listOf(
271 | PermissionResult(
272 | Permission.Location.Fine,
273 | result = Result.GRANTED,
274 | hasAskedForPermissions = true
275 | )
276 | )
277 | )
278 |
279 | permissionRepository.observe(
280 | Permission.Location.Coarse,
281 | Permission.Location.Fine
282 | )
283 | .test()
284 | .assertNotTerminated()
285 | .assertValueCount(1)
286 | .assertValue(
287 | listOf(
288 | PermissionResult(
289 | Permission.Location.Coarse,
290 | result = Result.GRANTED,
291 | hasAskedForPermissions = false
292 | ),
293 | PermissionResult(
294 | Permission.Location.Fine,
295 | result = Result.DENIED,
296 | hasAskedForPermissions = false
297 | )
298 | )
299 | )
300 | .dispose()
301 | }
302 |
303 | @Test
304 | fun `GIVEN request is in progress WHEN updating results THEN notify requesters`() {
305 | whenever(permissionsService.manifestPermissions).thenReturn(listOf(Permission.Location.Coarse.value))
306 |
307 | val testObserver = permissionRepository.request(Permission.Location.Coarse)
308 | .test()
309 | .assertNoValues()
310 | .assertNotComplete()
311 |
312 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.GRANTED)
313 |
314 | permissionRepository.update(
315 | listOf(
316 | PermissionResult(
317 | Permission.Location.Coarse,
318 | result = Result.GRANTED,
319 | hasAskedForPermissions = true
320 | )
321 | )
322 | )
323 |
324 | testObserver
325 | .assertValueCount(1)
326 | .assertComplete()
327 | .assertValue(
328 | listOf(
329 | PermissionResult(
330 | Permission.Location.Coarse,
331 | result = Result.GRANTED,
332 | hasAskedForPermissions = true
333 | )
334 | )
335 | )
336 | .dispose()
337 | }
338 |
339 | @Test
340 | fun `GIVEN requesting permissions WHEN updating results twice THEN notify requesters once`() {
341 | whenever(permissionsService.manifestPermissions).thenReturn(listOf(Permission.Location.Coarse.value))
342 |
343 | val testObserver = permissionRepository.request(Permission.Location.Coarse)
344 | .test()
345 | .assertNoValues()
346 | .assertNotComplete()
347 |
348 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.GRANTED)
349 |
350 | permissionRepository.update(
351 | listOf(
352 | PermissionResult(
353 | Permission.Location.Coarse,
354 | result = Result.GRANTED,
355 | hasAskedForPermissions = true
356 | )
357 | )
358 | )
359 |
360 | permissionRepository.update(
361 | listOf(
362 | PermissionResult(
363 | Permission.Location.Coarse,
364 | result = Result.GRANTED,
365 | hasAskedForPermissions = true
366 | )
367 | )
368 | )
369 |
370 | testObserver
371 | .assertValueCount(1)
372 | .assertComplete()
373 | .assertValue(
374 | listOf(
375 | PermissionResult(
376 | Permission.Location.Coarse,
377 | result = Result.GRANTED,
378 | hasAskedForPermissions = true
379 | )
380 | )
381 | )
382 | .dispose()
383 | }
384 |
385 | @Test
386 | fun `GIVEN camera permission granted WHEN observing THEN isMarkedAsDontAsk is false`() {
387 | whenever(permissionsService.checkPermission(Permission.Camera)).thenReturn(Result.GRANTED)
388 | whenever(requestStatusRepository.getHasAsked(Permission.Camera)).thenReturn(true)
389 |
390 | permissionRepository.observe(Permission.Camera)
391 | .test()
392 | .assertNotTerminated()
393 | .assertValueCount(1)
394 | .assertValue(
395 | listOf(
396 | PermissionResult(
397 | Permission.Camera,
398 | result = Result.GRANTED,
399 | hasAskedForPermissions = true,
400 | isMarkedAsDontAsk = false
401 | )
402 | )
403 | )
404 | .dispose()
405 | }
406 |
407 | @Test
408 | fun `GIVEN camera permission not granted and never asked WHEN observing THEN isMarkedAsDontAsk is false`() {
409 | whenever(permissionsService.checkPermission(Permission.Camera)).thenReturn(Result.DENIED)
410 | permissionRepository.observe(Permission.Camera)
411 | .test()
412 | .assertNotTerminated()
413 | .assertValueCount(1)
414 | .assertValue(
415 | listOf(
416 | PermissionResult(
417 | Permission.Camera,
418 | result = Result.DENIED,
419 | hasAskedForPermissions = false,
420 | isMarkedAsDontAsk = false
421 | )
422 | )
423 | )
424 | .dispose()
425 | }
426 |
427 | @Test
428 | fun `GIVEN camera permission not granted and has asked and should show rationale WHEN observing THEN isMarkedAsDontAsk is false`() {
429 | whenever(permissionsService.checkPermission(Permission.Camera)).thenReturn(Result.DENIED)
430 | whenever(requestStatusRepository.getHasAsked(Permission.Camera)).thenReturn(true)
431 | whenever(permissionsRationaleDelegate.shouldShowRequestPermissionRationale(Permission.Camera)).thenReturn(true)
432 |
433 | permissionRepository.observe(Permission.Camera)
434 | .test()
435 | .assertNotTerminated()
436 | .assertValueCount(1)
437 | .assertValue(
438 | listOf(
439 | PermissionResult(
440 | Permission.Camera,
441 | result = Result.DENIED,
442 | hasAskedForPermissions = true,
443 | isMarkedAsDontAsk = false
444 | )
445 | )
446 | )
447 | .dispose()
448 | }
449 |
450 | @Test
451 | fun `GIVEN camera permission not granted and has asked and should not show rationale WHEN observing THEN isMarkedAsDontAsk is true`() {
452 | whenever(permissionsService.checkPermission(Permission.Camera)).thenReturn(Result.DENIED)
453 | whenever(requestStatusRepository.getHasAsked(Permission.Camera)).thenReturn(true)
454 | whenever(permissionsRationaleDelegate.shouldShowRequestPermissionRationale(Permission.Camera)).thenReturn(false)
455 |
456 | permissionRepository.observe(Permission.Camera)
457 | .test()
458 | .assertNotTerminated()
459 | .assertValueCount(1)
460 | .assertValue(
461 | listOf(
462 | PermissionResult(
463 | Permission.Camera,
464 | result = Result.DENIED,
465 | hasAskedForPermissions = true,
466 | isMarkedAsDontAsk = true
467 | )
468 | )
469 | )
470 | .dispose()
471 | }
472 |
473 | @Test
474 | fun `GIVEN fine location permission marked as don't ask WHEN observing coarse location THEN isMarkedAsDontAsk is true`() {
475 | whenever(permissionsService.checkPermission(Permission.Location.Coarse)).thenReturn(Result.DENIED)
476 | whenever(requestStatusRepository.getHasAsked(Permission.Location.Fine)).thenReturn(true)
477 | whenever(permissionsRationaleDelegate.shouldShowRequestPermissionRationale(Permission.Location.Fine)).thenReturn(
478 | false
479 | )
480 |
481 | permissionRepository.observe(Permission.Location.Coarse)
482 | .test()
483 | .assertNotTerminated()
484 | .assertValueCount(1)
485 | .assertValue(
486 | listOf(
487 | PermissionResult(
488 | Permission.Location.Coarse,
489 | result = Result.DENIED,
490 | hasAskedForPermissions = false,
491 | isMarkedAsDontAsk = true
492 | )
493 | )
494 | )
495 | .dispose()
496 | }
497 | }
498 |
--------------------------------------------------------------------------------