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