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