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