1 package host.exp.exponent.utils 2 3 import android.Manifest 4 import host.exp.exponent.kernel.ExperienceKey 5 import javax.inject.Inject 6 import host.exp.exponent.kernel.services.ExpoKernelServiceRegistry 7 import host.exp.exponent.experience.ReactNativeActivity 8 import android.content.pm.PackageManager 9 import android.app.AlertDialog 10 import android.content.DialogInterface 11 import android.provider.Settings 12 import com.facebook.react.modules.core.PermissionListener 13 import host.exp.exponent.di.NativeModuleDepsProvider 14 import host.exp.expoview.Exponent 15 import host.exp.expoview.R 16 import java.util.* 17 18 class ScopedPermissionsRequester(private val experienceKey: ExperienceKey) { 19 @Inject 20 lateinit var expoKernelServiceRegistry: ExpoKernelServiceRegistry 21 22 private var permissionListener: PermissionListener? = null 23 private var experienceName: String? = null 24 private var permissionsResult = mutableMapOf<String, Int>() 25 private val permissionsToRequestPerExperience = mutableListOf<String>() 26 private val permissionsToRequestGlobally = mutableListOf<String>() 27 private var permissionsAskedCount = 0 28 29 fun requestPermissions( 30 currentActivity: ReactNativeActivity, 31 experienceName: String?, 32 permissions: Array<String>, 33 listener: PermissionListener 34 ) { 35 permissionListener = listener 36 this.experienceName = experienceName 37 permissionsResult = mutableMapOf() 38 39 for (permission in permissions) { 40 if (permission == null) { 41 continue 42 } 43 val globalStatus = currentActivity.checkSelfPermission(permission) 44 if (globalStatus == PackageManager.PERMISSION_DENIED) { 45 permissionsToRequestGlobally.add(permission) 46 } else if (!expoKernelServiceRegistry.permissionsKernelService.hasGrantedPermissions( 47 permission, 48 experienceKey 49 ) 50 ) { 51 permissionsToRequestPerExperience.add(permission) 52 } else { 53 permissionsResult[permission] = PackageManager.PERMISSION_GRANTED 54 } 55 } 56 57 if (permissionsToRequestPerExperience.isEmpty() && permissionsToRequestGlobally.isEmpty()) { 58 callPermissionsListener() 59 return 60 } 61 62 permissionsAskedCount = permissionsToRequestPerExperience.size 63 64 if (permissionsToRequestPerExperience.isNotEmpty()) { 65 requestExperienceAndGlobalPermissions(permissionsToRequestPerExperience[permissionsAskedCount - 1]) 66 } else if (permissionsToRequestGlobally.isNotEmpty()) { 67 currentActivity.requestPermissions( 68 permissionsToRequestGlobally.toTypedArray(), 69 EXPONENT_PERMISSIONS_REQUEST 70 ) 71 } 72 } 73 74 fun onRequestPermissionsResult(permissions: Array<String>, grantResults: IntArray): Boolean { 75 if (permissionListener == null) { 76 // sometimes onRequestPermissionsResult is called multiple times if the first permission 77 // is rejected... 78 return true 79 } 80 81 if (grantResults.isNotEmpty()) { 82 for (i in grantResults.indices) { 83 if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { 84 expoKernelServiceRegistry.permissionsKernelService.grantScopedPermissions( 85 permissions[i], experienceKey 86 ) 87 } 88 permissionsResult[permissions[i]] = grantResults[i] 89 } 90 } 91 92 return callPermissionsListener() 93 } 94 95 private fun callPermissionsListener(): Boolean { 96 val permissions = permissionsResult.keys.toTypedArray() 97 val result = IntArray(permissions.size) 98 for (i in permissions.indices) { 99 result[i] = permissionsResult[permissions[i]]!! 100 } 101 return permissionListener!!.onRequestPermissionsResult( 102 EXPONENT_PERMISSIONS_REQUEST, 103 permissions, 104 result 105 ) 106 } 107 108 private fun requestExperienceAndGlobalPermissions(permission: String) { 109 val activity = Exponent.instance.currentActivity 110 val builder = AlertDialog.Builder(activity) 111 val onClickListener = PermissionsDialogOnClickListener(permission) 112 builder 113 .setMessage( 114 activity!!.getString( 115 R.string.experience_needs_permissions, 116 experienceName, 117 activity.getString(permissionToResId(permission)) 118 ) 119 ) 120 .setPositiveButton(R.string.allow_experience_permissions, onClickListener) 121 .setNegativeButton(R.string.deny_experience_permissions, onClickListener) 122 .show() 123 } 124 125 private fun permissionToResId(permission: String): Int { 126 return when (permission) { 127 Manifest.permission.CAMERA -> R.string.perm_camera 128 Manifest.permission.READ_CONTACTS -> R.string.perm_contacts_read 129 Manifest.permission.WRITE_CONTACTS -> R.string.perm_contacts_write 130 Manifest.permission.READ_EXTERNAL_STORAGE -> R.string.perm_camera_roll_read 131 Manifest.permission.WRITE_EXTERNAL_STORAGE -> R.string.perm_camera_roll_write 132 Manifest.permission.ACCESS_MEDIA_LOCATION -> R.string.perm_access_media_location 133 Manifest.permission.RECORD_AUDIO -> R.string.perm_audio_recording 134 Settings.ACTION_MANAGE_WRITE_SETTINGS -> R.string.perm_system_brightness 135 Manifest.permission.READ_CALENDAR -> R.string.perm_calendar_read 136 Manifest.permission.WRITE_CALENDAR -> R.string.perm_calendar_write 137 Manifest.permission.ACCESS_FINE_LOCATION -> R.string.perm_fine_location 138 Manifest.permission.ACCESS_COARSE_LOCATION -> R.string.perm_coarse_location 139 Manifest.permission.ACCESS_BACKGROUND_LOCATION -> R.string.perm_background_location 140 Manifest.permission.READ_PHONE_STATE -> R.string.perm_read_phone_state 141 Manifest.permission.READ_MEDIA_IMAGES -> R.string.perm_read_media_images 142 Manifest.permission.READ_MEDIA_VIDEO -> R.string.perm_read_media_videos 143 Manifest.permission.READ_MEDIA_AUDIO -> R.string.perm_read_media_audio 144 else -> -1 145 } 146 } 147 148 private inner class PermissionsDialogOnClickListener(private val permission: String) : DialogInterface.OnClickListener { 149 override fun onClick(dialog: DialogInterface, which: Int) { 150 permissionsAskedCount -= 1 151 when (which) { 152 DialogInterface.BUTTON_POSITIVE -> { 153 expoKernelServiceRegistry.permissionsKernelService.grantScopedPermissions( 154 permission, 155 this@ScopedPermissionsRequester.experienceKey 156 ) 157 permissionsResult[permission] = PackageManager.PERMISSION_GRANTED 158 } 159 DialogInterface.BUTTON_NEGATIVE -> { 160 expoKernelServiceRegistry.permissionsKernelService.revokeScopedPermissions( 161 permission, 162 experienceKey 163 ) 164 permissionsResult[permission] = PackageManager.PERMISSION_DENIED 165 } 166 } 167 168 if (permissionsAskedCount > 0) { 169 requestExperienceAndGlobalPermissions(permissionsToRequestPerExperience[permissionsAskedCount - 1]) 170 } else if (permissionsToRequestGlobally.isNotEmpty()) { 171 Exponent.instance.currentActivity!!.requestPermissions( 172 permissionsToRequestGlobally.toTypedArray(), 173 EXPONENT_PERMISSIONS_REQUEST 174 ) 175 } else { 176 callPermissionsListener() 177 } 178 } 179 } 180 181 companion object { 182 const val EXPONENT_PERMISSIONS_REQUEST = 13 183 } 184 185 init { 186 NativeModuleDepsProvider.instance.inject(ScopedPermissionsRequester::class.java, this) 187 } 188 } 189