Added back take snapshot button in video call if enabled in CorePreferences + reworked/fixed PermissionHelper.hasWriteExternalStoragePermission() for Android >= 10
This commit is contained in:
parent
5bae5bc9de
commit
7b933ad76b
12 changed files with 86 additions and 73 deletions
|
@ -27,7 +27,6 @@ import kotlin.math.max
|
||||||
import org.linphone.activities.main.chat.data.EventLogData
|
import org.linphone.activities.main.chat.data.EventLogData
|
||||||
import org.linphone.core.*
|
import org.linphone.core.*
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.mediastream.Version
|
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
import org.linphone.utils.LinphoneUtils
|
import org.linphone.utils.LinphoneUtils
|
||||||
import org.linphone.utils.PermissionHelper
|
import org.linphone.utils.PermissionHelper
|
||||||
|
@ -71,7 +70,7 @@ class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Version.sdkStrictlyBelow(Version.API29_ANDROID_10) && !PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
||||||
for (content in chatMessage.contents) {
|
for (content in chatMessage.contents) {
|
||||||
if (content.isFileTransfer) {
|
if (content.isFileTransfer) {
|
||||||
Log.i("[Chat Messages] Android < 10 detected and WRITE_EXTERNAL_STORAGE permission isn't granted yet")
|
Log.i("[Chat Messages] Android < 10 detected and WRITE_EXTERNAL_STORAGE permission isn't granted yet")
|
||||||
|
|
|
@ -33,7 +33,6 @@ import org.linphone.compatibility.Compatibility
|
||||||
import org.linphone.core.Content
|
import org.linphone.core.Content
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.databinding.FileViewerTopBarFragmentBinding
|
import org.linphone.databinding.FileViewerTopBarFragmentBinding
|
||||||
import org.linphone.mediastream.Version
|
|
||||||
import org.linphone.utils.FileUtils
|
import org.linphone.utils.FileUtils
|
||||||
import org.linphone.utils.PermissionHelper
|
import org.linphone.utils.PermissionHelper
|
||||||
|
|
||||||
|
@ -90,7 +89,7 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
|
||||||
private fun exportContent(content: Content) {
|
private fun exportContent(content: Content) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
var mediaStoreFilePath = ""
|
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")
|
Log.i("[File Viewer] Exporting image through Media Store API")
|
||||||
when (content.type) {
|
when (content.type) {
|
||||||
"image" -> {
|
"image" -> {
|
||||||
|
|
|
@ -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(
|
controlsViewModel.askPermissionEvent.observe(
|
||||||
this
|
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)
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import java.util.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
|
import org.linphone.compatibility.Compatibility
|
||||||
import org.linphone.contact.GenericContactData
|
import org.linphone.contact.GenericContactData
|
||||||
import org.linphone.core.*
|
import org.linphone.core.*
|
||||||
import org.linphone.core.tools.Log
|
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")
|
Log.i("[Call] Remote recording changed: $recording")
|
||||||
isRemotelyRecorded.value = 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())
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
|
@ -149,69 +167,6 @@ open class CallData(val call: Call) : GenericContactData(call.remoteAddress) {
|
||||||
contextMenuClickListener?.onShowContextMenu(anchor, this)
|
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 {
|
private fun isCallPaused(): Boolean {
|
||||||
return when (call.state) {
|
return when (call.state) {
|
||||||
Call.State.Paused, Call.State.Pausing -> true
|
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")
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ import org.linphone.core.*
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.utils.AppUtils
|
import org.linphone.utils.AppUtils
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
|
import org.linphone.utils.FileUtils
|
||||||
|
import org.linphone.utils.PermissionHelper
|
||||||
|
|
||||||
class CallsViewModel : ViewModel() {
|
class CallsViewModel : ViewModel() {
|
||||||
val currentCallData = MutableLiveData<CallData>()
|
val currentCallData = MutableLiveData<CallData>()
|
||||||
|
@ -41,6 +43,10 @@ class CallsViewModel : ViewModel() {
|
||||||
|
|
||||||
val chatAndCallsCount = MediatorLiveData<Int>()
|
val chatAndCallsCount = MediatorLiveData<Int>()
|
||||||
|
|
||||||
|
val askWriteExternalStoragePermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
val callConnectedEvent: MutableLiveData<Event<Call>> by lazy {
|
val callConnectedEvent: MutableLiveData<Event<Call>> by lazy {
|
||||||
MutableLiveData<Event<Call>>()
|
MutableLiveData<Event<Call>>()
|
||||||
}
|
}
|
||||||
|
@ -158,6 +164,20 @@ class CallsViewModel : ViewModel() {
|
||||||
conference?.addParticipants(core.calls)
|
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() {
|
private fun initCallList() {
|
||||||
val calls = arrayListOf<CallData>()
|
val calls = arrayListOf<CallData>()
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,8 @@ class ControlsViewModel : ViewModel() {
|
||||||
|
|
||||||
val proximitySensorEnabled = MediatorLiveData<Boolean>()
|
val proximitySensorEnabled = MediatorLiveData<Boolean>()
|
||||||
|
|
||||||
|
val showTakeSnaptshotButton = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val goToConferenceParticipantsListEvent: MutableLiveData<Event<Boolean>> by lazy {
|
val goToConferenceParticipantsListEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
MutableLiveData<Event<Boolean>>()
|
MutableLiveData<Event<Boolean>>()
|
||||||
}
|
}
|
||||||
|
@ -453,6 +455,7 @@ class ControlsViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
isVideoEnabled.value = enabled
|
isVideoEnabled.value = enabled
|
||||||
|
showTakeSnaptshotButton.value = enabled && corePreferences.showScreenshotButton
|
||||||
isSwitchCameraAvailable.value = enabled && coreContext.showSwitchCameraButton()
|
isSwitchCameraAvailable.value = enabled && coreContext.showSwitchCameraButton()
|
||||||
if (coreContext.core.currentCall?.conference != null) {
|
if (coreContext.core.currentCall?.conference != null) {
|
||||||
isVideoSendReceive.value = coreContext.core.currentCall?.currentParams?.videoDirection == MediaDirection.SendRecv
|
isVideoSendReceive.value = coreContext.core.currentCall?.currentParams?.videoDirection == MediaDirection.SendRecv
|
||||||
|
|
|
@ -743,7 +743,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
if (PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
||||||
for (content in message.contents) {
|
for (content in message.contents) {
|
||||||
if (content.isFile && content.filePath != null && content.userData == null) {
|
if (content.isFile && content.filePath != null && content.userData == null) {
|
||||||
addContentToMediaStore(content)
|
addContentToMediaStore(content)
|
||||||
|
@ -764,7 +764,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
if (PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
when (content.type) {
|
when (content.type) {
|
||||||
"image" -> {
|
"image" -> {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.linphone.compatibility.Compatibility
|
import org.linphone.compatibility.Compatibility
|
||||||
import org.linphone.core.tools.Log
|
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
|
* 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 {
|
fun hasWriteExternalStoragePermission(): Boolean {
|
||||||
|
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) return true
|
||||||
return hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
return hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
app/src/main/res/drawable-xhdpi/voip_screenshot.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/voip_screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
11
app/src/main/res/drawable/icon_call_screenshot.xml
Normal file
11
app/src/main/res/drawable/icon_call_screenshot.xml
Normal 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>
|
|
@ -142,6 +142,7 @@
|
||||||
app:layout_constraintTop_toTopOf="@id/background" />
|
app:layout_constraintTop_toTopOf="@id/background" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
android:id="@+id/record_call"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:layout_margin="10dp"
|
android:layout_margin="10dp"
|
||||||
|
@ -155,6 +156,18 @@
|
||||||
app:layout_constraintStart_toStartOf="@id/background"
|
app:layout_constraintStart_toStartOf="@id/background"
|
||||||
app:layout_constraintTop_toTopOf="@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
|
<ImageView
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
|
|
|
@ -821,6 +821,7 @@
|
||||||
<string name="content_description_call_secured">Call is secured</string>
|
<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_call_security_pending">Call security is pending</string>
|
||||||
<string name="content_description_chat_message_video_attachment">Video attachment</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_close_bubble">Close notification bubble</string>
|
||||||
<string name="content_description_open_app">Open conversation in app instead of 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>
|
<string name="content_description_export">Open file in third-party app</string>
|
||||||
|
|
Loading…
Reference in a new issue