Coverage Summary for Class: PermissionWatchmen (dev.shreyaspatil.permissionFlow.watchmen)

Class Method, % Branch, % Line, % Instruction, %
PermissionWatchmen 100% (11/11) 90.9% (20/22) 100% (37/37) 98.2% (220/224)
PermissionWatchmen$Companion 100% (1/1) 100% (1/1) 100% (2/2)
PermissionWatchmen$notifyPermissionsChanged$1 100% (1/1) 100% (6/6) 100% (42/42)
PermissionWatchmen$PermissionEvent 100% (1/1) 100% (1/1) 100% (12/12)
PermissionWatchmen$PermissionStateFlowDelegate 100% (2/2) 100% (4/4) 100% (17/17)
PermissionWatchmen$watchActivities$1 100% (1/1) 100% (3/3) 100% (16/16)
PermissionWatchmen$watchMultiple$$inlined$combineStates$1 0% (0/1)
PermissionWatchmen$watchMultiple$$inlined$combineStates$2 0% (0/2)
PermissionWatchmen$watchMultiple$$inlined$combineStates$2$2 0% (0/1)
PermissionWatchmen$watchMultiple$$inlined$combineStates$2$3 0% (0/1)
PermissionWatchmen$watchPermissionEvents$1 100% (1/1) 100% (2/2) 100% (28/28)
PermissionWatchmen$watchPermissionEvents$1$1 100% (1/1) 50% (1/2) 100% (2/2) 95.5% (21/22)
Total 79.2% (19/24) 87.5% (21/24) 100% (56/56) 98.6% (358/363)


1 /** 2  * Copyright 2022 Shreyas Patil 3  * 4  * Licensed under the Apache License, Version 2.0 (the "License"); 5  * you may not use this file except in compliance with the License. 6  * You may obtain a copy of the License at 7  * 8  * http://www.apache.org/licenses/LICENSE-2.0 9  * 10  * Unless required by applicable law or agreed to in writing, software 11  * distributed under the License is distributed on an "AS IS" BASIS, 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13  * See the License for the specific language governing permissions and 14  * limitations under the License. 15  */ 16 package dev.shreyaspatil.permissionFlow.watchmen 17  18 import android.app.Application 19 import android.content.pm.PackageManager 20 import androidx.core.content.ContextCompat 21 import dev.shreyaspatil.permissionFlow.MultiplePermissionState 22 import dev.shreyaspatil.permissionFlow.PermissionState 23 import dev.shreyaspatil.permissionFlow.utils.activityForegroundEventFlow 24 import dev.shreyaspatil.permissionFlow.utils.stateFlow.combineStates 25 import kotlinx.coroutines.CoroutineDispatcher 26 import kotlinx.coroutines.CoroutineName 27 import kotlinx.coroutines.CoroutineScope 28 import kotlinx.coroutines.FlowPreview 29 import kotlinx.coroutines.Job 30 import kotlinx.coroutines.SupervisorJob 31 import kotlinx.coroutines.cancelChildren 32 import kotlinx.coroutines.flow.MutableSharedFlow 33 import kotlinx.coroutines.flow.MutableStateFlow 34 import kotlinx.coroutines.flow.StateFlow 35 import kotlinx.coroutines.flow.asStateFlow 36 import kotlinx.coroutines.flow.debounce 37 import kotlinx.coroutines.flow.launchIn 38 import kotlinx.coroutines.flow.onEach 39 import kotlinx.coroutines.launch 40 import kotlinx.coroutines.yield 41  42 /** 43  * A watchmen which keeps watching state changes of permissions and events of permissions. 44  */ 45 @Suppress("OPT_IN_IS_NOT_ENABLED", "unused") 46 internal class PermissionWatchmen( 47  private val application: Application, 48  dispatcher: CoroutineDispatcher, 49 ) { 50  private val watchmenScope = CoroutineScope( 51  dispatcher + 52  SupervisorJob() + 53  CoroutineName("PermissionWatchmen"), 54  ) 55  56  private var watchEventsJob: Job? = null 57  private var watchActivityEventJob: Job? = null 58  59  /** 60  * A in-memory store for storing permission and its state holder i.e. [StateFlow] 61  */ 62  private val permissionFlows = mutableMapOf<String, PermissionStateFlowDelegate>() 63  64  private val permissionEvents = MutableSharedFlow<PermissionEvent>() 65  66  fun watch(permission: String): StateFlow<PermissionState> { 67  // Wakeup watchmen if sleeping 68  wakeUp() 69  return getOrCreatePermissionStateFlow(permission) 70  } 71  72  fun watchMultiple(permissions: Array<String>): StateFlow<MultiplePermissionState> { 73  // Wakeup watchmen if sleeping 74  wakeUp() 75  76  val permissionStates = permissions 77  .distinct() 78  .map { getOrCreatePermissionStateFlow(it) } 79  .toTypedArray() 80  81  return combineStates(*permissionStates) { MultiplePermissionState(it.toList()) } 82  } 83  84  fun notifyPermissionsChanged(permissions: Array<String>) { 85  watchmenScope.launch { 86  permissions.forEach { permission -> 87  permissionEvents.emit( 88  PermissionEvent( 89  permission = permission, 90  isGranted = isPermissionGranted(permission), 91  ), 92  ) 93  } 94  } 95  } 96  97  @Synchronized 98  fun wakeUp() { 99  watchPermissionEvents() 100  watchActivities() 101  notifyAllPermissionsChanged() 102  } 103  104  @Synchronized 105  fun sleep() { 106  watchmenScope.coroutineContext.cancelChildren() 107  } 108  109  /** 110  * First finds for existing flow (if available) otherwise creates a new [MutableStateFlow] 111  * for [permission] and returns a read-only [StateFlow] for a [permission]. 112  */ 113  @Synchronized 114  private fun getOrCreatePermissionStateFlow(permission: String): StateFlow<PermissionState> { 115  val delegate = permissionFlows[permission] ?: run { 116  val initialState = PermissionState(permission, isPermissionGranted(permission)) 117  PermissionStateFlowDelegate(initialState).also { permissionFlows[permission] = it } 118  } 119  return delegate.state 120  } 121  122  /** 123  * Watches for the permission events and updates appropriate state holders of permission 124  */ 125  private fun watchPermissionEvents() { 126  if (watchEventsJob != null && watchEventsJob?.isActive == true) return 127  watchEventsJob = watchmenScope.launch { 128  permissionEvents.collect { (permission, isGranted) -> 129  permissionFlows[permission]?.setState(PermissionState(permission, isGranted)) 130  } 131  } 132  } 133  134  /** 135  * Watches for activity foreground events (to detect whether user has changed permission by 136  * going in settings) and recalculates state of the permissions which are currently being 137  * observed. 138  */ 139  @OptIn(FlowPreview::class) 140  private fun watchActivities() { 141  if (watchActivityEventJob != null && watchActivityEventJob?.isActive == true) return 142  watchActivityEventJob = application.activityForegroundEventFlow 143  // This is just to avoid frequent events. 144  .debounce(DEFAULT_DEBOUNCE_FOR_ACTIVITY_CALLBACK) 145  .onEach { 146  // Since this is not priority task, we want to yield current thread for other 147  // tasks for the watchmen. 148  yield() 149  notifyAllPermissionsChanged() 150  } 151  .launchIn(watchmenScope) 152  } 153  154  private fun notifyAllPermissionsChanged() { 155  if (permissionFlows.isEmpty()) return 156  notifyPermissionsChanged(permissionFlows.keys.toTypedArray()) 157  } 158  159  private fun isPermissionGranted(permission: String): Boolean { 160  return ContextCompat.checkSelfPermission( 161  application, 162  permission, 163  ) == PackageManager.PERMISSION_GRANTED 164  } 165  166  /** 167  * A event model for permission event. 168  * 169  * @property permission Name of a permission 170  * @property isGranted State of permission whether it's granted / denied 171  */ 172  private data class PermissionEvent(val permission: String, val isGranted: Boolean) 173  174  /** 175  * A delegate for [MutableStateFlow] which creates flow for holding state of a permission. 176  */ 177  private class PermissionStateFlowDelegate(initialState: PermissionState) { 178  179  private val _state = MutableStateFlow(initialState) 180  val state = _state.asStateFlow() 181  182  fun setState(newState: PermissionState) { 183  _state.value = newState 184  } 185  } 186  187  companion object { 188  private const val DEFAULT_DEBOUNCE_FOR_ACTIVITY_CALLBACK = 5_000L 189  } 190 }