Skip to content

Instantly share code, notes, and snippets.

@vinaysshenoy
Last active April 9, 2020 10:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vinaysshenoy/30a581f8333744655cf9015877486349 to your computer and use it in GitHub Desktop.
Save vinaysshenoy/30a581f8333744655cf9015877486349 to your computer and use it in GitHub Desktop.
Automatic permission management
import android.app.Activity
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import io.reactivex.Observable
import io.reactivex.ObservableSource
import io.reactivex.ObservableTransformer
import io.reactivex.rxkotlin.cast
import io.reactivex.rxkotlin.ofType
import io.reactivex.subjects.PublishSubject
import org.simple.clinic.router.screen.ActivityPermissionResult
import org.simple.clinic.util.RuntimePermissionResult.DENIED
import org.simple.clinic.util.RuntimePermissionResult.GRANTED
import javax.inject.Inject
enum class RuntimePermissionResult {
GRANTED,
DENIED
}
class RuntimePermissions @Inject constructor() {
fun check(activity: Activity, permission: String): RuntimePermissionResult {
val permissionGranted = ActivityCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED
return if (permissionGranted) DENIED else GRANTED
}
fun request(activity: Activity, permission: String, requestCode: Int) {
ActivityCompat.requestPermissions(activity, arrayOf(permission), requestCode)
}
}
interface RequiresPermission {
var permission: Optional<RuntimePermissionResult>
val permissionString: String
val permissionRequestCode: Int
}
class RequestPermissions<T : Any>(
private val runtimePermissions: RuntimePermissions,
private val activity: Activity,
private val permissionResults: Observable<ActivityPermissionResult>
) : ObservableTransformer<T, T> {
private val permissionCompletedEvents = PublishSubject.create<T>()
private var inFlightPermissionRequests = mapOf<Int, RequiresPermission>()
override fun apply(upstream: Observable<T>): ObservableSource<T> {
val sharedUpstream = upstream.share()
val eventsRequiringPermission = sharedUpstream.ofType<RequiresPermission>()
val eventsNotRequiringPermission = sharedUpstream.filter { it !is RequiresPermission }
return Observable.merge(
eventsNotRequiringPermission,
permissionCompletedEvents,
requestPermissions(eventsRequiringPermission),
handlePermissionResults()
)
}
@Suppress("UNCHECKED_CAST")
private fun requestPermissions(
events: Observable<RequiresPermission>
): Observable<T> {
return events
.cast<RequiresPermission>()
.doOnNext { event -> event.permission = Just(runtimePermissions.check(activity, event.permissionString)) }
.doOnNext { event ->
when ((event.permission as Just).value) {
DENIED -> {
// This means that the permission needs to be requested
// since the framework runtime permissions framework
// does not have a way for us to check if the user has
// ever been asked for this permission before.
val permission = event.permissionString
val requestCode = event.permissionRequestCode
inFlightPermissionRequests = inFlightPermissionRequests + (requestCode to event)
runtimePermissions.request(activity, permission, requestCode)
}
GRANTED -> permissionCompletedEvents.onNext(event as T)
}
}
.flatMap { Observable.empty<T>() }
}
@Suppress("UNCHECKED_CAST")
private fun handlePermissionResults(): Observable<T> {
return permissionResults
.map { it.requestCode }
.doOnNext { requestCode ->
val event = inFlightPermissionRequests[requestCode]
if (event != null) {
event.permission = Just(runtimePermissions.check(activity, event.permissionString))
permissionCompletedEvents.onNext(event as T)
}
}
.doOnNext { requestCode ->
inFlightPermissionRequests = inFlightPermissionRequests - requestCode
}
.flatMap { Observable.empty<T>() }
}
}
package org.simple.clinic.util
import android.app.Activity
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.never
import com.nhaarman.mockito_kotlin.verify
import com.nhaarman.mockito_kotlin.whenever
import io.reactivex.subjects.PublishSubject
import org.junit.After
import org.junit.Test
import org.simple.clinic.router.screen.ActivityPermissionResult
import org.simple.clinic.util.RequestPermissionsTest.Event.FirstEvent
import org.simple.clinic.util.RequestPermissionsTest.Event.FourthEvent
import org.simple.clinic.util.RequestPermissionsTest.Event.SecondEvent
import org.simple.clinic.util.RequestPermissionsTest.Event.ThirdEvent
import org.simple.clinic.util.RuntimePermissionResult.DENIED
import org.simple.clinic.util.RuntimePermissionResult.GRANTED
class RequestPermissionsTest {
private val events = PublishSubject.create<Event>()
private val permissionResults = PublishSubject.create<ActivityPermissionResult>()
private val runtimePermissions = mock<RuntimePermissions>()
private val activity = mock<Activity>()
private val receivedEvents = events
.compose(RequestPermissions<Event>(runtimePermissions, activity, permissionResults))
.test()
@After
fun tearDown() {
receivedEvents.dispose()
}
@Test
fun `events requiring permission should not be forwarded if the permission is denied`() {
// given
whenever(runtimePermissions.check(activity, "permission_1")) doReturn DENIED
whenever(runtimePermissions.check(activity, "permission_2")) doReturn DENIED
// when
events.onNext(FirstEvent)
events.onNext(SecondEvent())
events.onNext(ThirdEvent)
events.onNext(FourthEvent())
events.onNext(FirstEvent)
// then
receivedEvents
.assertValues(FirstEvent, ThirdEvent, FirstEvent)
.assertNotTerminated()
}
@Test
fun `events requiring permission should be forwarded if the current permission is granted`() {
// given
whenever(runtimePermissions.check(activity, "permission_1")) doReturn GRANTED
whenever(runtimePermissions.check(activity, "permission_2")) doReturn GRANTED
// when
events.onNext(FirstEvent)
events.onNext(SecondEvent())
events.onNext(ThirdEvent)
events.onNext(FourthEvent())
events.onNext(FirstEvent)
// then
receivedEvents
.assertValues(
FirstEvent,
SecondEvent(permission = Just(GRANTED)),
ThirdEvent,
FourthEvent(permission = Just(GRANTED)),
FirstEvent
)
.assertNotTerminated()
}
@Test
fun `permission should be requested if it currently is denied`() {
whenever(runtimePermissions.check(activity, "permission_1")) doReturn DENIED
whenever(runtimePermissions.check(activity, "permission_2")) doReturn DENIED
// when
events.onNext(FirstEvent)
events.onNext(SecondEvent())
events.onNext(ThirdEvent)
events.onNext(FourthEvent())
events.onNext(FirstEvent)
// then
verify(runtimePermissions).request(activity, "permission_1", 1)
verify(runtimePermissions).request(activity, "permission_2", 2)
}
@Test
fun `permission should not be requested if it currently is granted`() {
whenever(runtimePermissions.check(activity, "permission_2")) doReturn GRANTED
// when
events.onNext(FirstEvent)
events.onNext(SecondEvent())
events.onNext(ThirdEvent)
events.onNext(FourthEvent())
events.onNext(FirstEvent)
// then
verify(runtimePermissions, never()).request(activity, "permission_2", 2)
}
@Test
fun `when a permission is granted, the event should be forwarded`() {
// given
whenever(runtimePermissions.check(activity, "permission_1")).doReturn(DENIED, GRANTED)
whenever(runtimePermissions.check(activity, "permission_2")).doReturn(DENIED, DENIED)
// when
events.onNext(FirstEvent)
events.onNext(SecondEvent())
events.onNext(ThirdEvent)
events.onNext(FourthEvent())
events.onNext(FirstEvent)
// then
permissionResults.onNext(ActivityPermissionResult(requestCode = 2))
receivedEvents
.assertValues(
FirstEvent,
ThirdEvent,
FirstEvent,
FourthEvent(permission = Just(DENIED))
)
.assertNotTerminated()
permissionResults.onNext(ActivityPermissionResult(requestCode = 1))
receivedEvents
.assertValues(
FirstEvent,
ThirdEvent,
FirstEvent,
FourthEvent(permission = Just(DENIED)),
SecondEvent(permission = Just(GRANTED))
)
.assertNotTerminated()
}
sealed class Event {
object FirstEvent : Event()
data class SecondEvent(
override var permission: Optional<RuntimePermissionResult> = None,
override val permissionRequestCode: Int = 1,
override val permissionString: String = "permission_1"
) : Event(), RequiresPermission
object ThirdEvent : Event()
data class FourthEvent(
override var permission: Optional<RuntimePermissionResult> = None,
override val permissionRequestCode: Int = 2,
override val permissionString: String = "permission_2"
) : Event(), RequiresPermission
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment