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 }