Coverage Summary for Class: ApplicationStateMonitor (dev.shreyaspatil.permissionFlow.internal)
Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
ApplicationStateMonitor |
100%
(6/6)
|
83.3%
(5/6)
|
100%
(11/11)
|
100%
(52/52)
|
ApplicationStateMonitor$activityForegroundEvents$1 |
100%
(1/1)
|
|
100%
(5/5)
|
100%
(35/35)
|
ApplicationStateMonitor$activityForegroundEvents$1$1 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(7/7)
|
ApplicationStateMonitor$activityForegroundEvents$1$callback$1 |
90%
(9/10)
|
92.3%
(24/26)
|
95.2%
(20/21)
|
97.5%
(119/122)
|
Total |
94.4%
(17/18)
|
90.6%
(29/32)
|
97.4%
(37/38)
|
98.6%
(213/216)
|
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.internal
17
18 import android.app.Activity
19 import android.app.Application
20 import android.content.pm.PackageManager
21 import android.os.Build
22 import android.os.Bundle
23 import androidx.annotation.RequiresApi
24 import androidx.annotation.VisibleForTesting
25 import androidx.core.app.ActivityCompat
26 import androidx.core.content.ContextCompat
27 import dev.shreyaspatil.permissionFlow.PermissionState
28 import java.lang.ref.WeakReference
29 import kotlinx.coroutines.channels.awaitClose
30 import kotlinx.coroutines.flow.callbackFlow
31
32 /**
33 * Monitors the state of the application and provides information about the info and state of
34 * application.
35 */
36 internal class ApplicationStateMonitor(private val application: Application) {
37 private var currentActivity: WeakReference<Activity>? = null
38
39 /** Returns the current state of the permission. */
40 fun getPermissionState(permission: String): PermissionState {
41 val isGranted = isPermissionGranted(permission)
42 val isRationaleRequired = shouldShowPermissionRationale(permission)
43 return PermissionState(permission, isGranted, isRationaleRequired)
44 }
45
46 /** Returns whether the permission should show rationale or not. */
47 private fun shouldShowPermissionRationale(permission: String): Boolean? {
48 val activity = currentActivity?.get() ?: return null
49 return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
50 }
51
52 /** Returns whether the permission is granted or not. */
53 private fun isPermissionGranted(permission: String): Boolean {
54 return ContextCompat.checkSelfPermission(
55 application,
56 permission,
57 ) == PackageManager.PERMISSION_GRANTED
58 }
59
60 /**
61 * A flow which gives callback whenever any activity is started withing application (without
62 * configuration change) or any activity is resumed after being in multi-window or
63 * picture-in-picture mode.
64 */
65 val activityForegroundEvents
66 get() = callbackFlow {
67 val callback =
68 object : Application.ActivityLifecycleCallbacks {
69 private var isActivityChangingConfigurations: Boolean? = null
70 private var wasInMultiWindowMode: Boolean? = null
71 private var wasInPictureInPictureMode: Boolean? = null
72
73 override fun onActivityPreCreated(
74 activity: Activity,
75 savedInstanceState: Bundle?,
76 ) {
77 currentActivity = WeakReference(activity)
78 }
79
80 override fun onActivityCreated(
81 activity: Activity,
82 savedInstanceState: Bundle?,
83 ) {
84 if (currentActivity?.get() != activity) {
85 currentActivity = WeakReference(activity)
86 }
87 }
88
89 /**
90 * Whenever activity receives onStart() lifecycle callback, emit foreground
91 * event only when activity hasn't changed configurations.
92 */
93 override fun onActivityStarted(activity: Activity) {
94 if (isActivityChangingConfigurations == false) {
95 trySend(Unit)
96 }
97 }
98
99 override fun onActivityStopped(activity: Activity) {
100 isActivityChangingConfigurations = activity.isChangingConfigurations
101 }
102
103 /**
104 * Whenever application is resized after being in in PiP or multi-window mode,
105 * or exits from these modes, onResumed() lifecycle callback is triggered.
106 *
107 * Here we assume that user has changed permission from app settings after being
108 * in PiP or multi-window mode. So whenever these modes are exited, emit
109 * foreground event.
110 */
111 override fun onActivityResumed(activity: Activity) {
112 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
113 if (isActivityResumedAfterMultiWindowOrPiPMode(activity)) {
114 trySend(Unit)
115 }
116 wasInMultiWindowMode = activity.isInMultiWindowMode
117 wasInPictureInPictureMode = activity.isInPictureInPictureMode
118 }
119 }
120
121 /**
122 * Whenever application is launched in PiP or multi-window mode, onPaused()
123 * lifecycle callback is triggered.
124 */
125 override fun onActivityPaused(activity: Activity) {
126 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
127 wasInMultiWindowMode = activity.isInMultiWindowMode
128 wasInPictureInPictureMode = activity.isInPictureInPictureMode
129 }
130 }
131
132 override fun onActivitySaveInstanceState(
133 activity: Activity,
134 outState: Bundle,
135 ) {}
136
137 override fun onActivityDestroyed(activity: Activity) {
138 if (activity == currentActivity?.get()) {
139 currentActivity?.clear()
140 }
141 }
142
143 /**
144 * Returns whether [activity] was previously in multi-window mode or PiP mode.
145 */
146 @RequiresApi(Build.VERSION_CODES.N)
147 private fun isActivityResumedAfterMultiWindowOrPiPMode(activity: Activity) =
148 (wasInMultiWindowMode == true && !activity.isInMultiWindowMode) ||
149 (wasInPictureInPictureMode == true &&
150 !activity.isInPictureInPictureMode)
151 }
152
153 application.registerActivityLifecycleCallbacks(callback)
154
155 awaitClose {
156 // Cleanup
157 application.unregisterActivityLifecycleCallbacks(callback)
158 }
159 }
160
161 @VisibleForTesting fun getCurrentActivityReference() = currentActivity
162 }