package org.unimodules.adapters.react.permissions import android.Manifest import android.annotation.TargetApi import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.facebook.react.modules.core.PermissionAwareActivity import com.facebook.react.modules.core.PermissionListener import expo.modules.interfaces.permissions.Permissions import expo.modules.interfaces.permissions.PermissionsResponse import expo.modules.interfaces.permissions.PermissionsResponseListener import expo.modules.interfaces.permissions.PermissionsStatus import org.unimodules.core.ModuleRegistry import org.unimodules.core.Promise import org.unimodules.core.interfaces.ActivityProvider import org.unimodules.core.interfaces.InternalModule import org.unimodules.core.interfaces.LifecycleEventListener import org.unimodules.core.interfaces.services.UIManager import java.util.* import kotlin.collections.HashMap private const val PERMISSIONS_REQUEST: Int = 13 private const val PREFERENCE_FILENAME = "expo.modules.permissions.asked" open class PermissionsService(val context: Context) : InternalModule, Permissions, LifecycleEventListener { private var mActivityProvider: ActivityProvider? = null // state holders for asking for writing permissions private var mWriteSettingsPermissionBeingAsked = false // change this directly before calling corresponding startActivity private var mAskAsyncListener: PermissionsResponseListener? = null private var mAskAsyncRequestedPermissions: Array? = null private val mPendingPermissionCalls: Queue, PermissionsResponseListener>> = LinkedList() private var mCurrentPermissionListener: PermissionsResponseListener? = null private lateinit var mAskedPermissionsCache: SharedPreferences private fun didAsk(permission: String): Boolean = mAskedPermissionsCache.getBoolean(permission, false) private fun addToAskedPermissionsCache(permissions: Array) { with(mAskedPermissionsCache.edit()) { permissions.forEach { putBoolean(it, true) } apply() } } override fun getExportedInterfaces(): List> = listOf(Permissions::class.java) @Throws(IllegalStateException::class) override fun onCreate(moduleRegistry: ModuleRegistry) { mActivityProvider = moduleRegistry.getModule(ActivityProvider::class.java) ?: throw IllegalStateException("Couldn't find implementation for ActivityProvider.") moduleRegistry.getModule(UIManager::class.java).registerLifecycleEventListener(this) mAskedPermissionsCache = context.applicationContext.getSharedPreferences(PREFERENCE_FILENAME, Context.MODE_PRIVATE) } override fun getPermissionsWithPromise(promise: Promise, vararg permissions: String) { getPermissions(PermissionsResponseListener { permissionsMap: MutableMap -> val areAllGranted = permissionsMap.all { (_, response) -> response.status == PermissionsStatus.GRANTED } val areAllDenied = permissionsMap.all { (_, response) -> response.status == PermissionsStatus.DENIED } val canAskAgain = permissionsMap.all { (_, response) -> response.canAskAgain } promise.resolve(Bundle().apply { putString(PermissionsResponse.EXPIRES_KEY, PermissionsResponse.PERMISSION_EXPIRES_NEVER) putString(PermissionsResponse.STATUS_KEY, when { areAllGranted -> PermissionsStatus.GRANTED.status areAllDenied -> PermissionsStatus.DENIED.status else -> PermissionsStatus.UNDETERMINED.status }) putBoolean(PermissionsResponse.CAN_ASK_AGAIN_KEY, canAskAgain) putBoolean(PermissionsResponse.GRANTED_KEY, areAllGranted) }) }, *permissions) } override fun askForPermissionsWithPromise(promise: Promise, vararg permissions: String) { askForPermissions(PermissionsResponseListener { getPermissionsWithPromise(promise, *permissions) }, *permissions) } override fun getPermissions(responseListener: PermissionsResponseListener, vararg permissions: String) { responseListener.onResult(parseNativeResult(permissions, permissions.map { if (isPermissionGranted(it)) { PackageManager.PERMISSION_GRANTED } else { PackageManager.PERMISSION_DENIED } }.toIntArray())) } @Throws(IllegalStateException::class) override fun askForPermissions(responseListener: PermissionsResponseListener, vararg permissions: String) { if (permissions.contains(Manifest.permission.WRITE_SETTINGS) && isRuntimePermissionsAvailable()) { val permissionsToAsk = permissions.toMutableList().apply { remove(Manifest.permission.WRITE_SETTINGS) }.toTypedArray() val newListener = PermissionsResponseListener { val status = if (hasWriteSettingsPermission()) { PackageManager.PERMISSION_GRANTED } else { PackageManager.PERMISSION_DENIED } it[Manifest.permission.WRITE_SETTINGS] = getPermissionResponseFromNativeResponse(Manifest.permission.WRITE_SETTINGS, status) responseListener.onResult(it) } if (!hasWriteSettingsPermission()) { if (mAskAsyncListener != null) { throw IllegalStateException("Another permissions request is in progress. Await the old request and then try again.") } mAskAsyncListener = newListener mAskAsyncRequestedPermissions = permissionsToAsk addToAskedPermissionsCache(arrayOf(Manifest.permission.WRITE_SETTINGS)) askForWriteSettingsPermissionFirst() } else { askForManifestPermissions(permissionsToAsk, newListener) } } else { askForManifestPermissions(permissions, responseListener) } } override fun hasGrantedPermissions(vararg permissions: String): Boolean { return permissions.all { isPermissionGranted(it) } } /** * Checks whether given permission is present in AndroidManifest or not. */ override fun isPermissionPresentInManifest(permission: String): Boolean { try { context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)?.run { return requestedPermissions.contains(permission) } return false } catch (e: PackageManager.NameNotFoundException) { return false } } /** * Checks status for Android built-in permission * * @param permission [android.Manifest.permission] */ private fun isPermissionGranted(permission: String): Boolean { return when (permission) { // we need to handle this permission in different way Manifest.permission.WRITE_SETTINGS -> hasWriteSettingsPermission() else -> getManifestPermission(permission) == PackageManager.PERMISSION_GRANTED } } /** * Gets status for Android built-in permission * * @param permission [android.Manifest.permission] */ private fun getManifestPermission(permission: String): Int { mActivityProvider?.currentActivity?.let { if (it is PermissionAwareActivity) { return ContextCompat.checkSelfPermission(it, permission) } } // We are in the headless mode. So, we ask current context. return getManifestPermissionFromContext(permission) } protected open fun getManifestPermissionFromContext(permission: String): Int { return ContextCompat.checkSelfPermission(context, permission) } private fun canAskAgain(permission: String): Boolean { return mActivityProvider?.currentActivity?.let { ActivityCompat.shouldShowRequestPermissionRationale(it, permission) } ?: false } private fun parseNativeResult(permissionsString: Array, grantResults: IntArray): Map { return HashMap().apply { grantResults.zip(permissionsString).forEach { (result, permission) -> this[permission] = getPermissionResponseFromNativeResponse(permission, result) } } } private fun getPermissionResponseFromNativeResponse(permission: String, result: Int): PermissionsResponse { val status = when { result == PackageManager.PERMISSION_GRANTED -> PermissionsStatus.GRANTED didAsk(permission) -> PermissionsStatus.DENIED else -> PermissionsStatus.UNDETERMINED } return PermissionsResponse( status, if (status == PermissionsStatus.DENIED) { canAskAgain(permission) } else { true } ) } protected open fun askForManifestPermissions(permissions: Array, listener: PermissionsResponseListener) { if (!isRuntimePermissionsAvailable()) { // It's not possible to ask for the permissions in the runtime. // We return to the user the permissions status, which was granted during installation. addToAskedPermissionsCache(permissions) val permissionsResult = permissions.map { getManifestPermission(it) }.toIntArray() listener.onResult(parseNativeResult(permissions, permissionsResult)) return } delegateRequestToActivity(permissions, listener) } /** * Asks for Android built-in permission * According to Android documentation [android.Manifest.permission.WRITE_SETTINGS] need to be handled in different way * * @param permissions [android.Manifest.permission] */ protected fun delegateRequestToActivity(permissions: Array, listener: PermissionsResponseListener) { addToAskedPermissionsCache(permissions) val currentActivity = mActivityProvider?.currentActivity if (currentActivity is PermissionAwareActivity) { synchronized(this@PermissionsService) { if (mCurrentPermissionListener != null) { mPendingPermissionCalls.add(permissions to listener) } else { mCurrentPermissionListener = listener currentActivity.requestPermissions(permissions, PERMISSIONS_REQUEST, createListenerWithPendingPermissionsRequest()) } } } else { listener.onResult(parseNativeResult(permissions, IntArray(permissions.size) { PackageManager.PERMISSION_DENIED })) } } private fun createListenerWithPendingPermissionsRequest(): PermissionListener { return PermissionListener { requestCode, receivePermissions, grantResults -> if (requestCode == PERMISSIONS_REQUEST) { synchronized(this@PermissionsService) { val currentListener = requireNotNull(mCurrentPermissionListener) currentListener.onResult(parseNativeResult(receivePermissions, grantResults)) mCurrentPermissionListener = null mPendingPermissionCalls.poll()?.let { pendingCall -> val activity = mActivityProvider?.currentActivity as? PermissionAwareActivity if (activity == null) { // clear all pending calls, because we don't have access to the activity instance pendingCall.second.onResult(parseNativeResult(pendingCall.first, IntArray(pendingCall.first.size) { PackageManager.PERMISSION_DENIED })) mPendingPermissionCalls.forEach { it.second.onResult(parseNativeResult(it.first, IntArray(it.first.size) { PackageManager.PERMISSION_DENIED })) } mPendingPermissionCalls.clear() return@let } mCurrentPermissionListener = pendingCall.second activity.requestPermissions(pendingCall.first, PERMISSIONS_REQUEST, createListenerWithPendingPermissionsRequest()) return@PermissionListener false } return@PermissionListener true } } return@PermissionListener false } } /** * Asking for [android.provider.Settings.ACTION_MANAGE_WRITE_SETTINGS] via separate activity * WARNING: has to be asked first among all permissions being asked in request * Scenario that forces this order: * 1. user asks for "systemBrightness" (actual [android.provider.Settings.ACTION_MANAGE_WRITE_SETTINGS]) and for some other permission (e.g. [android.Manifest.permission.CAMERA]) * 2. first goes ACTION_MANAGE_WRITE_SETTINGS that moves app into background and launches system-specific fullscreen activity * 3. upon user action system resumes app and [onHostResume] is being called for the first time and logic for other permission is invoked * 4. other permission invokes other system-specific activity that is visible as dialog what moves app again into background * 5. upon user action app is restored and [onHostResume] is being called again, but no further action is invoked and promise is resolved */ @TargetApi(Build.VERSION_CODES.M) private fun askForWriteSettingsPermissionFirst() { Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply { data = Uri.parse("package:${context.packageName}") addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }.let { mWriteSettingsPermissionBeingAsked = true context.startActivity(it) } } private fun hasWriteSettingsPermission(): Boolean { return if (isRuntimePermissionsAvailable()) { Settings.System.canWrite(context.applicationContext) } else { true } } private fun isRuntimePermissionsAvailable() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M override fun onHostResume() { if (!mWriteSettingsPermissionBeingAsked) { return } mWriteSettingsPermissionBeingAsked = false // cleanup val askAsyncListener = mAskAsyncListener!! val askAsyncRequestedPermissions = mAskAsyncRequestedPermissions!! mAskAsyncListener = null mAskAsyncRequestedPermissions = null if (askAsyncRequestedPermissions.isNotEmpty()) { // invoke actual asking for permissions askForManifestPermissions(askAsyncRequestedPermissions, askAsyncListener) } else { // user asked only for Manifest.permission.WRITE_SETTINGS askAsyncListener.onResult(mutableMapOf()) } } override fun onHostPause() = Unit override fun onHostDestroy() = Unit }