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 }