package expo.modules.medialibrary.next.permissions import android.Manifest.permission.ACCESS_MEDIA_LOCATION import android.Manifest.permission.READ_EXTERNAL_STORAGE import android.Manifest.permission.READ_MEDIA_AUDIO import android.Manifest.permission.READ_MEDIA_IMAGES import android.Manifest.permission.READ_MEDIA_VIDEO import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.content.Context import android.content.pm.PackageManager import android.os.Build import expo.modules.interfaces.permissions.Permissions.askForPermissionsWithPermissionsManager import expo.modules.interfaces.permissions.Permissions.getPermissionsWithPermissionsManager import expo.modules.kotlin.AppContext import expo.modules.kotlin.Promise import expo.modules.kotlin.exception.Exceptions import expo.modules.medialibrary.R import expo.modules.medialibrary.next.exceptions.PermissionException import expo.modules.medialibrary.next.permissions.enums.GranularPermission import java.lang.ref.WeakReference class SystemPermissionsDelegate(private val appContext: AppContext) { private val context: Context get() = appContext.reactContext ?: throw Exceptions.ReactContextLost() fun requestPermissions(writeOnly: Boolean, permissions: List?, promise: Promise) { val granularPermissions = permissions ?: allowedPermissionsList maybeThrowIfExpoGo(granularPermissions) askForPermissionsWithPermissionsManager( appContext.permissions, MediaLibraryPermissionPromiseWrapper(granularPermissions, promise, WeakReference(context)), *getManifestPermissions(writeOnly, granularPermissions) ) } fun getPermissions(writeOnly: Boolean, permissions: List?, promise: Promise) { val granularPermissions = permissions ?: allowedPermissionsList maybeThrowIfExpoGo(granularPermissions) getPermissionsWithPermissionsManager( appContext.permissions, MediaLibraryPermissionPromiseWrapper(granularPermissions, promise, WeakReference(context)), *getManifestPermissions(writeOnly, granularPermissions) ) } fun presentPermissionsPicker(permissions: List?, promise: Promise) { if (Build.VERSION.SDK_INT < 34) { throw PermissionException("presentPermissionsPicker is only available on Android 14+") } val pickerPermissions = permissions ?: listOf(GranularPermission.PHOTO, GranularPermission.VIDEO) requestPermissions(writeOnly = false, permissions = pickerPermissions, promise = promise) } fun requireReadPermissions() { val granted = appContext.permissions?.hasGrantedPermissions(READ_EXTERNAL_STORAGE) if (granted != true) { throw PermissionException("Missing READ_EXTERNAL_STORAGE permission") } } fun requireWritePermissions() { val granted = appContext.permissions?.hasGrantedPermissions(WRITE_EXTERNAL_STORAGE) if (granted != true) { throw PermissionException("Missing WRITE_EXTERNAL_STORAGE permission") } } private val isExpoGo by lazy { context.resources.getString(R.string.is_expo_go).toBoolean() } private fun maybeThrowIfExpoGo(permissions: List) { if (isExpoGo) { if (permissions.contains(GranularPermission.PHOTO) || permissions.contains(GranularPermission.VIDEO)) { throw PermissionException("Due to changes in Androids permission requirements, Expo Go can no longer provide full access to the media library. To test the full functionality of this module, you can create a development build") } } } private val allowedPermissionsList by lazy { if (isExpoGo) { listOf(GranularPermission.AUDIO) } else { getManifestDeclaredPermissions( context, listOf(GranularPermission.PHOTO, GranularPermission.VIDEO, GranularPermission.AUDIO) ) } } private fun getManifestDeclaredPermissions( context: Context, granularPermissions: List ): List { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return granularPermissions.filter { hasManifestPermission(context, it.toManifestPermission()) } } return granularPermissions } private fun hasManifestPermission(context: Context, permission: String): Boolean = getManifestPermissions(context).contains(permission) private fun getManifestPermissions(context: Context): Set { return try { val packageInfo = context.packageManager .getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) packageInfo.requestedPermissions?.toSet() ?: emptySet() } catch (_: PackageManager.NameNotFoundException) { emptySet() } } private fun getManifestPermissions( writeOnly: Boolean, granularPermissions: List ): Array { // ACCESS_MEDIA_LOCATION should not be requested if it's absent in android-manifest // If only audio permission is requested, we don't need to request media location permissions val shouldAddMediaLocationAccess = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && hasManifestPermission(context, ACCESS_MEDIA_LOCATION) && !( Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && granularPermissions.count() == 1 && granularPermissions.contains( GranularPermission.AUDIO ) ) val shouldAddWriteExternalStorage = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && hasManifestPermission(context, WRITE_EXTERNAL_STORAGE) val shouldAddGranularPermissions = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU val shouldIncludeGranular = shouldAddGranularPermissions && !writeOnly return listOfNotNull( WRITE_EXTERNAL_STORAGE.takeIf { shouldAddWriteExternalStorage }, READ_EXTERNAL_STORAGE.takeIf { !writeOnly && !shouldAddGranularPermissions }, ACCESS_MEDIA_LOCATION.takeIf { shouldAddMediaLocationAccess }, *getGranularPermissions(shouldIncludeGranular, granularPermissions) ).toTypedArray() } private fun getGranularPermissions( shouldIncludeGranular: Boolean, granularPermissions: List ): Array { if (shouldIncludeGranular) { assertGranularPermissionIntegrity(context, granularPermissions) return listOfNotNull( READ_MEDIA_IMAGES.takeIf { granularPermissions.contains(GranularPermission.PHOTO) }, READ_MEDIA_VIDEO.takeIf { granularPermissions.contains(GranularPermission.VIDEO) }, READ_MEDIA_AUDIO.takeIf { granularPermissions.contains(GranularPermission.AUDIO) } ).toTypedArray() } return arrayOf() } private fun assertGranularPermissionIntegrity(context: Context, granularPermissions: List) { for (permission in granularPermissions) { if (!hasManifestPermission(context, permission.toManifestPermission())) { throw PermissionException("You have requested the $permission permission, but it is not declared in AndroidManifest. Update expo-media-library config plugin to include the permission before requesting it.") } } } }