Added back take snapshot button in video call if enabled in CorePreferences + reworked/fixed PermissionHelper.hasWriteExternalStoragePermission() for Android >= 10

This commit is contained in:
Sylvain Berfini 2022-03-31 12:24:56 +02:00
parent 5bae5bc9de
commit 7b933ad76b
12 changed files with 86 additions and 73 deletions

View file

@ -27,7 +27,6 @@ import kotlin.math.max
import org.linphone.activities.main.chat.data.EventLogData
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.mediastream.Version
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.PermissionHelper
@ -71,7 +70,7 @@ class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
return
}
if (Version.sdkStrictlyBelow(Version.API29_ANDROID_10) && !PermissionHelper.get().hasWriteExternalStoragePermission()) {
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
for (content in chatMessage.contents) {
if (content.isFileTransfer) {
Log.i("[Chat Messages] Android < 10 detected and WRITE_EXTERNAL_STORAGE permission isn't granted yet")

View file

@ -33,7 +33,6 @@ import org.linphone.compatibility.Compatibility
import org.linphone.core.Content
import org.linphone.core.tools.Log
import org.linphone.databinding.FileViewerTopBarFragmentBinding
import org.linphone.mediastream.Version
import org.linphone.utils.FileUtils
import org.linphone.utils.PermissionHelper
@ -90,7 +89,7 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
private fun exportContent(content: Content) {
lifecycleScope.launch {
var mediaStoreFilePath = ""
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStoragePermission()) {
if (PermissionHelper.get().hasWriteExternalStoragePermission()) {
Log.i("[File Viewer] Exporting image through Media Store API")
when (content.type) {
"image" -> {

View file

@ -94,6 +94,15 @@ class CallActivity : ProximitySensorActivity() {
}
}
callsViewModel.askWriteExternalStoragePermissionEvent.observe(
this
) {
it.consume {
Log.i("[Call Activity] Asking for WRITE_EXTERNAL_STORAGE permission to take snapshot")
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
}
}
controlsViewModel.askPermissionEvent.observe(
this
) {
@ -283,6 +292,11 @@ class CallActivity : ProximitySensorActivity() {
}
}
}
} else if (requestCode == 1) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.i("[Call Activity] WRITE_EXTERNAL_STORAGE permission has been granted, taking snapshot")
callsViewModel.takeSnapshot()
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

View file

@ -25,6 +25,7 @@ import java.util.*
import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.compatibility.Compatibility
import org.linphone.contact.GenericContactData
import org.linphone.core.*
import org.linphone.core.tools.Log
@ -82,6 +83,23 @@ open class CallData(val call: Call) : GenericContactData(call.remoteAddress) {
Log.i("[Call] Remote recording changed: $recording")
isRemotelyRecorded.value = recording
}
override fun onSnapshotTaken(call: Call, filePath: String) {
Log.i("[Call] Snapshot taken: $filePath")
val content = Factory.instance().createContent()
content.filePath = filePath
content.type = "image"
content.subtype = "jpeg"
content.name = filePath.substring(filePath.indexOf("/") + 1)
scope.launch {
if (Compatibility.addImageToMediaStore(coreContext.context, content)) {
Log.i("[Call] Adding snapshot ${content.name} to Media Store terminated")
} else {
Log.e("[Call] Something went wrong while copying file to Media Store...")
}
}
}
}
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
@ -149,69 +167,6 @@ open class CallData(val call: Call) : GenericContactData(call.remoteAddress) {
contextMenuClickListener?.onShowContextMenu(anchor, this)
}
private fun initChatRoom() {
val core = coreContext.core
val localSipUri = core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
val remoteSipUri = call.remoteAddress.asStringUriOnly()
val conference = call.conference
if (localSipUri != null) {
val localAddress = Factory.instance().createAddress(localSipUri)
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
chatRoom = core.searchChatRoom(null, localAddress, remoteSipAddress, arrayOfNulls(0))
if (chatRoom == null) {
Log.w("[Call] Failed to find existing chat room for local address [$localSipUri] and remote address [$remoteSipUri]")
var chatRoomParams: ChatRoomParams? = null
if (conference != null) {
val params = core.createDefaultChatRoomParams()
params.subject = conference.subject
params.backend = ChatRoomBackend.FlexisipChat
params.isGroupEnabled = true
chatRoomParams = params
}
chatRoom = core.searchChatRoom(
chatRoomParams,
localAddress,
null,
arrayOf(remoteSipAddress)
)
}
if (chatRoom == null) {
val chatRoomParams = core.createDefaultChatRoomParams()
if (conference != null) {
Log.w("[Call] Failed to find existing chat room with same subject & participants, creating it")
chatRoomParams.backend = ChatRoomBackend.FlexisipChat
chatRoomParams.isGroupEnabled = true
chatRoomParams.subject = conference.subject
val participants = arrayOfNulls<Address>(conference.participantCount)
val addresses = arrayListOf<Address>()
for (participant in conference.participantList) {
addresses.add(participant.address)
}
addresses.toArray(participants)
Log.i("[Call] Creating chat room with same subject [${chatRoomParams.subject}] & participants as for conference")
chatRoom = core.createChatRoom(chatRoomParams, localAddress, participants)
} else {
Log.w("[Call] Failed to find existing chat room with same participants, creating it")
// TODO: configure chat room params
chatRoom = core.createChatRoom(chatRoomParams, localAddress, arrayOf(remoteSipAddress))
}
}
if (chatRoom == null) {
Log.e("[Call] Failed to create a chat room for local address [$localSipUri] and remote address [$remoteSipUri]!")
}
} else {
Log.e("[Call] Failed to get either local [$localSipUri] or remote [$remoteSipUri] SIP address!")
}
}
private fun isCallPaused(): Boolean {
return when (call.state) {
Call.State.Paused, Call.State.Pausing -> true
@ -298,8 +253,4 @@ open class CallData(val call: Call) : GenericContactData(call.remoteAddress) {
)
Log.i("[Call] Starting 30 seconds timer to automatically decline video request")
}
fun isActiveAndNotInConference(): Boolean {
return isPaused.value == false && isRemotelyPaused.value == false && isInRemoteConference.value == false
}
}

View file

@ -29,6 +29,8 @@ import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
import org.linphone.utils.PermissionHelper
class CallsViewModel : ViewModel() {
val currentCallData = MutableLiveData<CallData>()
@ -41,6 +43,10 @@ class CallsViewModel : ViewModel() {
val chatAndCallsCount = MediatorLiveData<Int>()
val askWriteExternalStoragePermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val callConnectedEvent: MutableLiveData<Event<Call>> by lazy {
MutableLiveData<Event<Call>>()
}
@ -158,6 +164,20 @@ class CallsViewModel : ViewModel() {
conference?.addParticipants(core.calls)
}
fun takeSnapshot() {
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
askWriteExternalStoragePermissionEvent.value = Event(true)
} else {
if (currentCallData.value?.call?.currentParams?.isVideoEnabled == true) {
val fileName = System.currentTimeMillis().toString() + ".jpeg"
Log.i("[Calls] Snapshot will be save under $fileName")
currentCallData.value?.call?.takeVideoSnapshot(FileUtils.getFileStoragePath(fileName).absolutePath)
} else {
Log.e("[Calls] Current call doesn't have video, can't take snapshot")
}
}
}
private fun initCallList() {
val calls = arrayListOf<CallData>()

View file

@ -77,6 +77,8 @@ class ControlsViewModel : ViewModel() {
val proximitySensorEnabled = MediatorLiveData<Boolean>()
val showTakeSnaptshotButton = MutableLiveData<Boolean>()
val goToConferenceParticipantsListEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
@ -453,6 +455,7 @@ class ControlsViewModel : ViewModel() {
}
isVideoEnabled.value = enabled
showTakeSnaptshotButton.value = enabled && corePreferences.showScreenshotButton
isSwitchCameraAvailable.value = enabled && coreContext.showSwitchCameraButton()
if (coreContext.core.currentCall?.conference != null) {
isVideoSendReceive.value = coreContext.core.currentCall?.currentParams?.videoDirection == MediaDirection.SendRecv

View file

@ -743,7 +743,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
return
}
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStoragePermission()) {
if (PermissionHelper.get().hasWriteExternalStoragePermission()) {
for (content in message.contents) {
if (content.isFile && content.filePath != null && content.userData == null) {
addContentToMediaStore(content)
@ -764,7 +764,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
return
}
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStoragePermission()) {
if (PermissionHelper.get().hasWriteExternalStoragePermission()) {
coroutineScope.launch {
when (content.type) {
"image" -> {

View file

@ -23,6 +23,7 @@ import android.Manifest
import android.content.Context
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.mediastream.Version
/**
* Helper methods to check whether a permission has been granted and log the result
@ -63,6 +64,7 @@ class PermissionHelper private constructor(private val context: Context) {
}
fun hasWriteExternalStoragePermission(): Boolean {
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) return true
return hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<bitmap android:src="@drawable/voip_screenshot"
android:tint="@color/primary_color"/>
</item>
<item>
<bitmap android:src="@drawable/voip_screenshot"
android:tint="@color/voip_gray_blue_color"/>
</item>
</selector>

View file

@ -142,6 +142,7 @@
app:layout_constraintTop_toTopOf="@id/background" />
<ImageView
android:id="@+id/record_call"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="10dp"
@ -155,6 +156,18 @@
app:layout_constraintStart_toStartOf="@id/background"
app:layout_constraintTop_toTopOf="@id/background" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/button_call_recording_background"
android:contentDescription="@string/content_description_take_screenshot"
android:onClick="@{() -> callsViewModel.takeSnapshot()}"
android:src="@drawable/icon_call_screenshot"
android:visibility="@{!controlsViewModel.showTakeSnaptshotButton || controlsViewModel.fullScreenMode || controlsViewModel.pipMode ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toStartOf="@id/background"
app:layout_constraintTop_toBottomOf="@id/record_call" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"

View file

@ -821,6 +821,7 @@
<string name="content_description_call_secured">Call is secured</string>
<string name="content_description_call_security_pending">Call security is pending</string>
<string name="content_description_chat_message_video_attachment">Video attachment</string>
<string name="content_description_take_screenshot">Take a screenshot of received video</string>
<string name="content_description_close_bubble">Close notification bubble</string>
<string name="content_description_open_app">Open conversation in app instead of bubble</string>
<string name="content_description_export">Open file in third-party app</string>