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       val globalStatus = currentActivity.checkSelfPermission(permission)
42       if (globalStatus == PackageManager.PERMISSION_DENIED) {
43         permissionsToRequestGlobally.add(permission)
44       } else if (!expoKernelServiceRegistry.permissionsKernelService.hasGrantedPermissions(
45           permission,
46           experienceKey
47         )
48       ) {
49         permissionsToRequestPerExperience.add(permission)
50       } else {
51         permissionsResult[permission] = PackageManager.PERMISSION_GRANTED
52       }
53     }
54 
55     if (permissionsToRequestPerExperience.isEmpty() && permissionsToRequestGlobally.isEmpty()) {
56       callPermissionsListener()
57       return
58     }
59 
60     permissionsAskedCount = permissionsToRequestPerExperience.size
61 
62     if (permissionsToRequestPerExperience.isNotEmpty()) {
63       requestExperienceAndGlobalPermissions(permissionsToRequestPerExperience[permissionsAskedCount - 1])
64     } else if (permissionsToRequestGlobally.isNotEmpty()) {
65       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
66         currentActivity.requestPermissions(
67           permissionsToRequestGlobally.toTypedArray(),
68           EXPONENT_PERMISSIONS_REQUEST
69         )
70       } else {
71         val result = IntArray(permissionsToRequestGlobally.size)
72         Arrays.fill(result, PackageManager.PERMISSION_DENIED)
73         onRequestPermissionsResult(permissionsToRequestGlobally.toTypedArray(), result)
74       }
75     }
76   }
77 
78   fun onRequestPermissionsResult(permissions: Array<String>, grantResults: IntArray): Boolean {
79     if (permissionListener == null) {
80       // sometimes onRequestPermissionsResult is called multiple times if the first permission
81       // is rejected...
82       return true
83     }
84 
85     if (grantResults.isNotEmpty()) {
86       for (i in grantResults.indices) {
87         if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
88           expoKernelServiceRegistry.permissionsKernelService.grantScopedPermissions(
89             permissions[i], experienceKey
90           )
91         }
92         permissionsResult[permissions[i]] = grantResults[i]
93       }
94     }
95 
96     return callPermissionsListener()
97   }
98 
99   private fun callPermissionsListener(): Boolean {
100     val permissions = permissionsResult.keys.toTypedArray()
101     val result = IntArray(permissions.size)
102     for (i in permissions.indices) {
103       result[i] = permissionsResult[permissions[i]]!!
104     }
105     return permissionListener!!.onRequestPermissionsResult(
106       EXPONENT_PERMISSIONS_REQUEST,
107       permissions,
108       result
109     )
110   }
111 
112   private fun requestExperienceAndGlobalPermissions(permission: String) {
113     val activity = Exponent.instance.currentActivity
114     val builder = AlertDialog.Builder(activity)
115     val onClickListener = PermissionsDialogOnClickListener(permission)
116     builder
117       .setMessage(
118         activity!!.getString(
119           R.string.experience_needs_permissions,
120           experienceName,
121           activity.getString(permissionToResId(permission))
122         )
123       )
124       .setPositiveButton(R.string.allow_experience_permissions, onClickListener)
125       .setNegativeButton(R.string.deny_experience_permissions, onClickListener)
126       .show()
127   }
128 
129   private fun permissionToResId(permission: String): Int {
130     return when (permission) {
131       Manifest.permission.CAMERA -> R.string.perm_camera
132       Manifest.permission.READ_CONTACTS -> R.string.perm_contacts_read
133       Manifest.permission.WRITE_CONTACTS -> R.string.perm_contacts_write
134       Manifest.permission.READ_EXTERNAL_STORAGE -> R.string.perm_camera_roll_read
135       Manifest.permission.WRITE_EXTERNAL_STORAGE -> R.string.perm_camera_roll_write
136       Manifest.permission.ACCESS_MEDIA_LOCATION -> R.string.perm_access_media_location
137       Manifest.permission.RECORD_AUDIO -> R.string.perm_audio_recording
138       Settings.ACTION_MANAGE_WRITE_SETTINGS -> R.string.perm_system_brightness
139       Manifest.permission.READ_CALENDAR -> R.string.perm_calendar_read
140       Manifest.permission.WRITE_CALENDAR -> R.string.perm_calendar_write
141       Manifest.permission.ACCESS_FINE_LOCATION -> R.string.perm_fine_location
142       Manifest.permission.ACCESS_COARSE_LOCATION -> R.string.perm_coarse_location
143       Manifest.permission.ACCESS_BACKGROUND_LOCATION -> R.string.perm_background_location
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() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
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