Skip to content

Commit 7e80979

Browse files
authored
Merge pull request #74 from apivideo/feat/android_permissions
feat(android): request Android permissions
2 parents e8dc4c3 + d872df0 commit 7e80979

12 files changed

+450
-30
lines changed

android/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ dependencies {
159159
implementation "com.facebook.react:react-native:+"
160160

161161
implementation("video.api:rtmpdroid:1.2.1-packed")
162-
implementation("video.api:android-live-stream:1.3.1") {
162+
implementation("video.api:android-live-stream:1.4.0") {
163163
exclude group: 'video.api', module: 'rtmpdroid'
164164
// exclude the transitive dependency to use packed version to avoid conflict with libssl.so and libcrypto.so
165165
}

android/src/main/java/video/api/reactnative/livestream/LiveStreamView.kt

+107-28
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ package video.api.reactnative.livestream
33
import android.Manifest
44
import android.annotation.SuppressLint
55
import android.content.Context
6-
import android.content.pm.PackageManager
76
import android.util.AttributeSet
87
import android.util.Log
98
import android.view.ScaleGestureDetector
109
import androidx.constraintlayout.widget.ConstraintLayout
11-
import androidx.core.app.ActivityCompat
10+
import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
11+
import com.facebook.react.uimanager.ThemedReactContext
1212
import video.api.livestream.ApiVideoLiveStream
1313
import video.api.livestream.enums.CameraFacingDirection
14-
import video.api.livestream.interfaces.IConnectionChecker
14+
import video.api.livestream.interfaces.IConnectionListener
1515
import video.api.livestream.models.AudioConfig
1616
import video.api.livestream.models.VideoConfig
17+
import video.api.reactnative.livestream.utils.permissions.PermissionsManager
18+
import video.api.reactnative.livestream.utils.permissions.SerialPermissionsManager
19+
import video.api.reactnative.livestream.utils.showDialog
1720
import java.io.Closeable
1821

1922

@@ -25,12 +28,20 @@ class LiveStreamView @JvmOverloads constructor(
2528
) : ConstraintLayout(context, attrs, defStyle),
2629
Closeable {
2730
private val liveStream: ApiVideoLiveStream
31+
private val permissionsManager = SerialPermissionsManager(
32+
PermissionsManager((context as ThemedReactContext).reactApplicationContext)
33+
)
2834

35+
// Connection listeners
2936
var onConnectionSuccess: (() -> Unit)? = null
3037
var onConnectionFailed: ((reason: String?) -> Unit)? = null
3138
var onDisconnected: (() -> Unit)? = null
3239

33-
private val connectionListener = object : IConnectionChecker {
40+
// Permission listeners
41+
var onPermissionsDenied: ((List<String>) -> Unit)? = null
42+
var onPermissionsRationale: ((List<String>) -> Unit)? = null
43+
44+
private val connectionListener = object : IConnectionListener {
3445
override fun onConnectionSuccess() {
3546
onConnectionSuccess?.let { it() }
3647
}
@@ -48,8 +59,55 @@ class LiveStreamView @JvmOverloads constructor(
4859
inflate(context, R.layout.react_native_livestream, this)
4960
liveStream = ApiVideoLiveStream(
5061
context = context,
51-
connectionChecker = connectionListener,
52-
apiVideoView = findViewById(R.id.apivideo_view)
62+
connectionListener = connectionListener,
63+
apiVideoView = findViewById(R.id.apivideo_view),
64+
permissionRequester = { permissions, onGranted ->
65+
permissionsManager.requestPermissions(
66+
permissions,
67+
onAllGranted = {
68+
onGranted()
69+
},
70+
onShowPermissionRationale = { missingPermissions, onRequiredPermissionLastTime ->
71+
runOnUiThread {
72+
when {
73+
missingPermissions.size > 1 -> {
74+
context.showDialog(
75+
R.string.permission_required,
76+
R.string.camera_and_record_audio_permission_required_message,
77+
android.R.string.ok,
78+
onPositiveButtonClick = { onRequiredPermissionLastTime() }
79+
)
80+
}
81+
82+
missingPermissions.contains(Manifest.permission.CAMERA) -> {
83+
context.showDialog(
84+
R.string.permission_required,
85+
R.string.camera_permission_required_message,
86+
android.R.string.ok,
87+
onPositiveButtonClick = { onRequiredPermissionLastTime() }
88+
)
89+
}
90+
91+
missingPermissions.contains(Manifest.permission.RECORD_AUDIO) -> {
92+
context.showDialog(
93+
R.string.permission_required,
94+
R.string.record_audio_permission_required_message,
95+
android.R.string.ok,
96+
onPositiveButtonClick = { onRequiredPermissionLastTime() }
97+
)
98+
}
99+
}
100+
}
101+
val permissionsStrings = missingPermissions.joinToString(", ")
102+
Log.e(TAG, "Asking rationale for missing permissions: $permissionsStrings")
103+
onPermissionsRationale?.let { it(missingPermissions) }
104+
},
105+
onAtLeastOnePermissionDenied = { missingPermissions ->
106+
val permissionsStrings = missingPermissions.joinToString(", ")
107+
Log.e(TAG, "Missing permissions: $permissionsStrings")
108+
onPermissionsDenied?.let { it(missingPermissions) }
109+
})
110+
}
53111
)
54112
}
55113

@@ -62,41 +120,59 @@ class LiveStreamView @JvmOverloads constructor(
62120
var videoConfig: VideoConfig?
63121
get() = liveStream.videoConfig
64122
set(value) {
65-
if (ActivityCompat.checkSelfPermission(
66-
context,
67-
Manifest.permission.CAMERA
68-
) != PackageManager.PERMISSION_GRANTED
69-
) {
70-
Log.e(TAG, "Missing permissions Manifest.permission.CAMERA")
71-
throw UnsupportedOperationException("Missing permissions Manifest.permission.CAMERA")
72-
}
73-
74-
liveStream.videoConfig = value
123+
permissionsManager.requestPermission(
124+
Manifest.permission.CAMERA,
125+
onGranted = {
126+
liveStream.videoConfig = value
127+
},
128+
onShowPermissionRationale = { onRequiredPermissionLastTime ->
129+
runOnUiThread {
130+
context.showDialog(
131+
R.string.permission_required,
132+
R.string.camera_permission_required_message,
133+
android.R.string.ok,
134+
onPositiveButtonClick = { onRequiredPermissionLastTime() }
135+
)
136+
}
137+
},
138+
onDenied = {
139+
Log.e(TAG, "Missing permissions Manifest.permission.CAMERA")
140+
onPermissionsDenied?.let { it(listOf(Manifest.permission.CAMERA)) }
141+
})
75142
}
76143

77144

78145
var audioConfig: AudioConfig?
79146
get() = liveStream.audioConfig
80147
set(value) {
81-
if (ActivityCompat.checkSelfPermission(
82-
context,
83-
Manifest.permission.RECORD_AUDIO
84-
) != PackageManager.PERMISSION_GRANTED
85-
) {
86-
Log.e(TAG, "Missing permissions Manifest.permission.RECORD_AUDIO")
87-
throw UnsupportedOperationException("Missing permissions Manifest.permission.RECORD_AUDIO")
88-
}
89-
90-
liveStream.audioConfig = value
148+
permissionsManager.requestPermission(
149+
Manifest.permission.RECORD_AUDIO,
150+
onGranted = {
151+
liveStream.audioConfig = value
152+
},
153+
onShowPermissionRationale = { onRequiredPermissionLastTime ->
154+
runOnUiThread {
155+
context.showDialog(
156+
R.string.permission_required,
157+
R.string.record_audio_permission_required_message,
158+
android.R.string.ok,
159+
onPositiveButtonClick = { onRequiredPermissionLastTime() }
160+
)
161+
}
162+
},
163+
onDenied = {
164+
Log.e(TAG, "Missing permissions Manifest.permission.RECORD_AUDIO")
165+
onPermissionsDenied?.let { it(listOf(Manifest.permission.RECORD_AUDIO)) }
166+
})
91167
}
92168

93169
val isStreaming: Boolean
94170
get() = liveStream.isStreaming
95171

96172
var camera: CameraFacingDirection = CameraFacingDirection.BACK
97-
get() = liveStream.camera
173+
get() = liveStream.cameraPosition
98174
set(value) {
99-
liveStream.camera = value
175+
liveStream.cameraPosition = value
100176
field = value
101177
}
102178

@@ -148,6 +224,9 @@ class LiveStreamView @JvmOverloads constructor(
148224
}
149225

150226
fun startStreaming(streamKey: String, url: String?) {
227+
require(permissionsManager.hasPermission(Manifest.permission.CAMERA)) { "Missing permissions Manifest.permission.CAMERA" }
228+
require(permissionsManager.hasPermission(Manifest.permission.RECORD_AUDIO)) { "Missing permissions Manifest.permission.RECORD_AUDIO" }
229+
151230
url?.let { liveStream.startStreaming(streamKey, it) }
152231
?: liveStream.startStreaming(streamKey)
153232
}

android/src/main/java/video/api/reactnative/livestream/LiveStreamViewManager.kt

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.facebook.react.uimanager.annotations.ReactProp
99
import video.api.reactnative.livestream.events.OnConnectionFailedEvent
1010
import video.api.reactnative.livestream.events.OnConnectionSuccessEvent
1111
import video.api.reactnative.livestream.events.OnDisconnectEvent
12+
import video.api.reactnative.livestream.events.OnPermissionsDeniedEvent
1213
import video.api.reactnative.livestream.utils.getCameraFacing
1314
import video.api.reactnative.livestream.utils.toAudioConfig
1415
import video.api.reactnative.livestream.utils.toVideoConfig
@@ -34,6 +35,11 @@ class LiveStreamViewManager : LiveStreamViewManagerSpec<LiveStreamView>() {
3435
OnDisconnectEvent(view.id)
3536
) ?: Log.e(NAME, "No event dispatcher for react tag ${view.id}")
3637
}
38+
view.onPermissionsDenied = { permissions ->
39+
UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)?.dispatchEvent(
40+
OnPermissionsDeniedEvent(view.id, permissions)
41+
) ?: Log.e(NAME, "No event dispatcher for react tag ${view.id}")
42+
}
3743
return view
3844
}
3945

android/src/main/java/video/api/reactnative/livestream/ViewProps.kt

+6-1
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ object ViewProps {
2020
const val IS_STEREO = "isStereo"
2121

2222
enum class Events(val eventName: String) {
23+
// Connection events
2324
CONNECTION_SUCCESS("onConnectionSuccess"),
2425
CONNECTION_FAILED("onConnectionFailed"),
25-
DISCONNECTED("onDisconnect");
26+
DISCONNECTED("onDisconnect"),
27+
28+
// Permission events
29+
PERMISSIONS_DENIED("onPermissionsDenied"),
30+
PERMISSIONS_RATIONALE("onPermissionsRationale");
2631

2732
companion object {
2833
fun toEventsMap(): Map<String, *> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package video.api.reactnative.livestream.events
2+
3+
import com.facebook.react.bridge.Arguments
4+
import com.facebook.react.uimanager.events.Event
5+
import com.facebook.react.uimanager.events.RCTEventEmitter
6+
import video.api.reactnative.livestream.ViewProps
7+
8+
class OnPermissionsDeniedEvent(private val viewTag: Int, private val permissions: List<String>) :
9+
Event<OnPermissionsDeniedEvent>(viewTag) {
10+
private val params = Arguments.createMap().apply {
11+
putArray("permissions", Arguments.fromList(permissions))
12+
}
13+
14+
override fun getEventName() = ViewProps.Events.PERMISSIONS_DENIED.eventName
15+
16+
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
17+
rctEventEmitter.receiveEvent(viewTag, eventName, params)
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package video.api.reactnative.livestream.utils
2+
3+
import android.content.Context
4+
import android.content.DialogInterface
5+
import androidx.annotation.StringRes
6+
import androidx.appcompat.app.AlertDialog
7+
8+
/**
9+
* Show a dialog with the given title and message.
10+
*/
11+
fun Context.showDialog(
12+
@StringRes title: Int,
13+
@StringRes message: Int = 0,
14+
@StringRes
15+
positiveButtonText: Int = android.R.string.ok,
16+
@StringRes
17+
negativeButtonText: Int = 0,
18+
onPositiveButtonClick: () -> Unit = {},
19+
onNegativeButtonClick: () -> Unit = {}
20+
) {
21+
AlertDialog.Builder(this)
22+
.setTitle(title)
23+
.setMessage(message)
24+
.apply {
25+
if (positiveButtonText != 0) {
26+
setPositiveButton(positiveButtonText) { dialogInterface: DialogInterface, _: Int ->
27+
dialogInterface.dismiss()
28+
onPositiveButtonClick()
29+
}
30+
}
31+
if (negativeButtonText != 0) {
32+
setNegativeButton(negativeButtonText) { dialogInterface: DialogInterface, _: Int ->
33+
dialogInterface.dismiss()
34+
onNegativeButtonClick()
35+
}
36+
}
37+
}
38+
.show()
39+
}

0 commit comments

Comments
 (0)