Last active
April 9, 2020 10:00
-
-
Save vinaysshenoy/30a581f8333744655cf9015877486349 to your computer and use it in GitHub Desktop.
Automatic permission management
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>() } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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