From 4d5a09671ad4aa837ee9a9c83122d6839604ccbe Mon Sep 17 00:00:00 2001 From: CraZyLegenD Date: Mon, 17 Jun 2019 13:19:59 +0200 Subject: [PATCH] permission handlers --- app/build.gradle | 12 +- app/src/main/AndroidManifest.xml | 2 +- .../MainAbstractActivity.kt | 15 ++ kotlinextensions/build.gradle | 2 +- .../codestyle/BaseAbstractActivity.kt | 1 + .../BasePermissionManager.kt | 77 ++++++++ .../permissionHandlers/PermissionManager.kt | 168 ++++++++++++++++++ .../permissionHandlers/PermissionObserver.kt | 17 ++ .../permissionHandlers/PermissionResult.kt | 20 +++ .../permissionHandlers/SingleLiveEvent.kt | 51 ++++++ .../coroutines/PermissionCouroutineManager.kt | 167 +++++++++++++++++ 11 files changed, 525 insertions(+), 7 deletions(-) create mode 100644 kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/BasePermissionManager.kt create mode 100644 kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionManager.kt create mode 100644 kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionObserver.kt create mode 100644 kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionResult.kt create mode 100644 kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/SingleLiveEvent.kt create mode 100644 kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/coroutines/PermissionCouroutineManager.kt diff --git a/app/build.gradle b/app/build.gradle index e55d54db2..7afddb016 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,15 +40,17 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0-alpha05' + implementation 'androidx.appcompat:appcompat:1.1.0-beta01' implementation 'androidx.core:core-ktx:1.2.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1' testImplementation 'junit:junit:4.13-beta-3' - androidTestImplementation 'androidx.test:runner:1.2.0-beta01' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0-beta01' - implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha06' implementation project(path: ':kotlinextensions', transitive: false) - + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1" implementation 'androidx.biometric:biometric:1.0.0-alpha04' + implementation 'androidx.preference:preference:1.1.0-beta01' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f17dd0fd2..acca3327a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,8 +4,8 @@ - + () + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = true + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (grantResults.isNotEmpty() && + grantResults.all { it == PackageManager.PERMISSION_GRANTED } + ) { + onPermissionResult(PermissionResult.PermissionGranted(requestCode)) + } else if (permissions.any { shouldShowRequestPermissionRationale(it) }) { + onPermissionResult( + PermissionResult.PermissionDenied(requestCode, + permissions.filterIndexed { index, _ -> + grantResults[index] == PackageManager.PERMISSION_DENIED + } + ) + ) + } else { + onPermissionResult( + PermissionResult.PermissionDeniedPermanently(requestCode, + permissions.filterIndexed { index, _ -> + grantResults[index] == PackageManager.PERMISSION_DENIED + } + )) + } + } + + protected fun requestPermissions(requestId: Int, vararg permissions: String) { + + rationalRequest[requestId]?.let { + requestPermissions(permissions, requestId) + rationalRequest.remove(requestId) + return + } + + val notGranted = permissions.filter { + ContextCompat.checkSelfPermission( + requireActivity(), + it + ) != PackageManager.PERMISSION_GRANTED + }.toTypedArray() + + when { + notGranted.isEmpty() -> + onPermissionResult(PermissionResult.PermissionGranted(requestId)) + notGranted.any { shouldShowRequestPermissionRationale(it) } -> { + rationalRequest[requestId] = true + onPermissionResult(PermissionResult.ShowRationale(requestId)) + } + else -> { + requestPermissions(notGranted, requestId) + } + } + } + + protected abstract fun onPermissionResult(permissionResult: PermissionResult) +} diff --git a/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionManager.kt b/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionManager.kt new file mode 100644 index 000000000..07ef57c12 --- /dev/null +++ b/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionManager.kt @@ -0,0 +1,168 @@ +package com.crazylegend.kotlinextensions.permissionHandlers + +import android.arch.lifecycle.LiveData +import android.content.Context +import android.support.annotation.MainThread +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment + + +/** + * Interface definition for a callback to get [LiveData] of [PermissionResult] + * +interface PermissionObserver { +fun setupObserver(permissionResultLiveData: LiveData) +} + + * Implement this interface to get [LiveData] for observing permission request result. + */ + +/** + +override fun setupObserver(permissionResultLiveData: LiveData) { +permissionResultLiveData.observe(this, Observer { +when (it) { +is PermissionResult.PermissionGranted -> { +if (it.requestId == REQUEST_ID) { +//Add your logic here after user grants permission(s) +} +} +is PermissionResult.PermissionDenied -> { +if (it.requestId == REQUEST_ID) { +//Add your logic to handle permission denial +} +} +is PermissionResult.PermissionDeniedPermanently -> { +if (it.requestId == REQUEST_ID) { +//Add your logic here if user denied permission(s) permanently. +//Ideally you should ask user to manually go to settings and enable permission(s) +} +} +is PermissionResult.ShowRational -> { +if (it.requestId == REQUEST_ID) { +//If user denied permission frequently then she/he is not clear about why you are asking this permission. +//This is your chance to explain them why you need permission. +} +} +} +}) +} + + + * + */ + + + +/** + * Created by hristijan on 6/17/19 to long live and prosper ! + */ + +class PermissionManager : BasePermissionManager() { + + private val permissionResultLiveEvent: SingleLiveEvent by lazy { + SingleLiveEvent() + } + + override fun onPermissionResult(permissionResult: PermissionResult) { + permissionResultLiveEvent.postValue(permissionResult) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (parentFragment != null) { + (parentFragment as PermissionObserver).setupObserver(permissionResultLiveEvent) + } else { + (context as PermissionObserver).setupObserver(permissionResultLiveEvent) + } + } + + + companion object { + + private const val TAG = "PermissionManager" + + /** + * A static factory method to request permission from activity. + * Your activity must implement [PermissionObserver] + * + * @param activity an instance of [AppCompatActivity] which is also [PermissionObserver] + * @param requestId Request ID for permission request + * @param permissions Permission(s) to request + * + * @throws [IllegalArgumentException] if your activity doesn't implement [PermissionObserver] + */ + @JvmStatic + @MainThread + fun requestPermissions( + activity: AppCompatActivity, + requestId: Int, + vararg permissions: String + ) { + _requestPermissions( + activity, + requestId, + *permissions + ) + } + + /** + * A static factory method to request permission from fragment. + * Your fragment must implement [PermissionObserver] + * + * @param fragment an instance of [Fragment] which is also [PermissionObserver] + * @param requestId Request ID for permission request + * @param permissions Permission(s) to request + * + * @throws [IllegalArgumentException] if your fragment doesn't implement [PermissionObserver] + */ + @JvmStatic + @MainThread + fun requestPermissions( + fragment: Fragment, + requestId: Int, + vararg permissions: String + ) { + _requestPermissions( + fragment, + requestId, + *permissions + ) + } + + private fun _requestPermissions( + activityOrFragment: Any, + requestId: Int, + vararg permissions: String + ) { + + val fragmentManager = if (activityOrFragment is AppCompatActivity) { + activityOrFragment.supportFragmentManager + } else { + (activityOrFragment as Fragment).childFragmentManager + } + + if (fragmentManager.findFragmentByTag(TAG) != null) { + (fragmentManager.findFragmentByTag(TAG) as PermissionManager).requestPermissions( + requestId, + *permissions + ) + } else { + if (activityOrFragment !is PermissionObserver) { + throw IllegalArgumentException( + "Activity/Fragment must implement PermissionObserver" + ) + } else { + val permissionManager = PermissionManager() + fragmentManager.beginTransaction().add( + permissionManager, + TAG + ).commitNow() + permissionManager.requestPermissions(requestId, *permissions) + } + } + } + } + + +} \ No newline at end of file diff --git a/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionObserver.kt b/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionObserver.kt new file mode 100644 index 000000000..b750e48e3 --- /dev/null +++ b/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionObserver.kt @@ -0,0 +1,17 @@ +package com.crazylegend.kotlinextensions.permissionHandlers + +import android.arch.lifecycle.LiveData + + +/** + * Created by hristijan on 6/17/19 to long live and prosper ! + */ + +/** + * Interface definition for a callback to get [LiveData] of [PermissionResult] + * + * Implement this interface to get [LiveData] for observing permission request result. + */ +interface PermissionObserver { + fun setupObserver(permissionResultLiveData: LiveData) +} \ No newline at end of file diff --git a/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionResult.kt b/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionResult.kt new file mode 100644 index 000000000..8f8a7172d --- /dev/null +++ b/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/PermissionResult.kt @@ -0,0 +1,20 @@ +package com.crazylegend.kotlinextensions.permissionHandlers + + +/** + * Created by hristijan on 6/17/19 to long live and prosper ! + */ + +sealed class PermissionResult { + class PermissionGranted(val requestId: Int) : PermissionResult() + class PermissionDenied( + val requestId: Int, + val deniedPermissions: List + ) : PermissionResult() + + class ShowRationale(val requestId: Int) : PermissionResult() + class PermissionDeniedPermanently( + val requestId: Int, + val permanentlyDeniedPermissions: List + ) : PermissionResult() +} \ No newline at end of file diff --git a/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/SingleLiveEvent.kt b/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/SingleLiveEvent.kt new file mode 100644 index 000000000..3cd6e14c5 --- /dev/null +++ b/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/SingleLiveEvent.kt @@ -0,0 +1,51 @@ +package com.crazylegend.kotlinextensions.permissionHandlers + +import android.arch.lifecycle.LifecycleOwner +import android.arch.lifecycle.MutableLiveData +import android.arch.lifecycle.Observer +import android.support.annotation.MainThread +import android.util.Log +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Created by hristijan on 6/17/19 to long live and prosper ! + */ + +class SingleLiveEvent : MutableLiveData() { + + private val mPending = AtomicBoolean(false) + + @MainThread + override fun observe(owner: LifecycleOwner, observer: Observer) { + + if (hasActiveObservers()) { + Log.w(TAG, "Multiple observers registered but only one will be notified of changes.") + } + + // Observe the internal MutableLiveData + super.observe(owner, Observer { t -> + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t) + } + }) + } + + @MainThread + override fun setValue(t: T?) { + mPending.set(true) + super.setValue(t) + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + fun call() { + value = null + } + + companion object { + + private const val TAG = "SingleLiveEvent" + } +} \ No newline at end of file diff --git a/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/coroutines/PermissionCouroutineManager.kt b/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/coroutines/PermissionCouroutineManager.kt new file mode 100644 index 000000000..9ef497e14 --- /dev/null +++ b/kotlinextensions/src/main/java/com/crazylegend/kotlinextensions/permissionHandlers/coroutines/PermissionCouroutineManager.kt @@ -0,0 +1,167 @@ +package com.crazylegend.kotlinextensions.permissionHandlers.coroutines + + +/** + * Created by hristijan on 6/17/19 to long live and prosper ! + */ +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.crazylegend.kotlinextensions.permissionHandlers.BasePermissionManager +import com.crazylegend.kotlinextensions.permissionHandlers.PermissionResult +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + + +/** + * + * +. +. +. +launch { +//CoroutineScope + +val permissionResult = PermissionManager.requestPermissions( //Suspends the coroutine +this@Fragment, +REQUEST_ID, +Manifest.permission.ACCESS_FINE_LOCATION, +Manifest.permission.READ_CONTACTS, +Manifest.permission.CAMERA +) + +//Resume coroutine once result is ready +when(permissionResult) { +is PermissionResult.PermissionGranted -> { +//Add your logic here after user grants permission(s) +} +is PermissionResult.PermissionDenied -> { +//Add your logic to handle permission denial +} +is PermissionResult.PermissionDeniedPermanently -> { +//Add your logic here if user denied permission(s) permanently. +//Ideally you should ask user to manually go to settings and enable permission(s) +} +is PermissionResult.ShowRational -> { +//If user denied permission frequently then she/he is not clear about why you are asking this permission. +//This is your chance to explain them why you need permission. +} +} + +} + + * + * + */ + +/** + * Permission manager which handles checking permission is granted or not and if not then will request permission. + * This is nothing but a headless fragment which wraps the boilerplate code for checking and requesting permission + * and suspends the coroutines until result is available. + * A simple [Fragment] subclass. + */ +class PermissionCouroutineManager : BasePermissionManager() { + + private lateinit var completableDeferred: CompletableDeferred + + override fun onPermissionResult(permissionResult: PermissionResult) { + completableDeferred.complete(permissionResult) + } + + companion object { + + private const val TAG = "PermissionManager" + + /** + * A static factory method to request permission from activity. + * + * @param activity an instance of [AppCompatActivity] + * @param requestId Request ID for permission request + * @param permissions Permission(s) to request + * + * @return [PermissionResult] + * + * Suspends the coroutines until result is available. + */ + suspend fun requestPermissions( + activity: AppCompatActivity, + requestId: Int, + vararg permissions: String + ): PermissionResult { + return withContext(Dispatchers.Main) { + return@withContext _requestPermissions( + activity, + requestId, + *permissions + ) + } + } + + /** + * A static factory method to request permission from fragment. + * + * @param fragment an instance of [Fragment] + * @param requestId Request ID for permission request + * @param permissions Permission(s) to request + * + * @return [PermissionResult] + * + * Suspends the coroutines until result is available. + */ + suspend fun requestPermissions( + fragment: Fragment, + requestId: Int, + vararg permissions: String + ): PermissionResult { + return withContext(Dispatchers.Main) { + return@withContext _requestPermissions( + fragment, + requestId, + *permissions + ) + } + } + + private suspend fun _requestPermissions( + activityOrFragment: Any, + requestId: Int, + vararg permissions: String + ): PermissionResult { + val fragmentManager = if (activityOrFragment is AppCompatActivity) { + activityOrFragment.supportFragmentManager + } else { + (activityOrFragment as Fragment).childFragmentManager + } + + return if (fragmentManager.findFragmentByTag(TAG) != null) { + val permissionManager = fragmentManager.findFragmentByTag(TAG) as PermissionCouroutineManager + permissionManager.completableDeferred = CompletableDeferred() + permissionManager.requestPermissions( + requestId, + *permissions + ) + permissionManager.completableDeferred.await() + } else { + val permissionManager = PermissionCouroutineManager().apply { + completableDeferred = CompletableDeferred() + } + fragmentManager.beginTransaction().add( + permissionManager, + TAG + ).commitNow() + permissionManager.requestPermissions(requestId, *permissions) + permissionManager.completableDeferred.await() + } + } + } + + override fun onDestroy() { + super.onDestroy() + if (completableDeferred.isActive) { + completableDeferred.cancel() + } + } +} + + +