Started external file sharing

This commit is contained in:
Sylvain Berfini 2023-11-09 14:21:41 +01:00
parent 16bf6bfc2c
commit 97cae93bb5
8 changed files with 174 additions and 31 deletions

View file

@ -41,6 +41,7 @@
android:label="@string/app_name"
android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.Linphone"
android:localeConfig="@xml/locales_config"
tools:targetApi="34">
<meta-data
@ -66,6 +67,24 @@
<action android:name="android.intent.action.VIEW_LOCUS" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="video/*" />
<data android:mimeType="application/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="video/*" />
<data android:mimeType="application/*" />
</intent-filter>
</activity>
<activity

View file

@ -23,7 +23,9 @@ import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.view.Gravity
import android.view.ViewGroup
import androidx.annotation.DrawableRes
@ -36,7 +38,10 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.findNavController
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -48,6 +53,8 @@ import org.linphone.databinding.MainActivityBinding
import org.linphone.ui.main.viewmodel.MainViewModel
import org.linphone.ui.main.viewmodel.SharedMainViewModel
import org.linphone.utils.AppUtils
import org.linphone.utils.FileUtils
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.slideInToastFromTop
import org.linphone.utils.slideInToastFromTopForDuration
@ -292,9 +299,25 @@ class MainActivity : AppCompatActivity() {
}
private fun handleIntent(intent: Intent, defaultDestination: Int, isNewIntent: Boolean) {
Log.i("$TAG Handling intent [$intent]")
val navGraph = findNavController().navInflater.inflate(R.navigation.main_nav_graph)
Log.i(
"$TAG Handling intent action [${intent.action}], type [${intent.type}] and data [${intent.data}]"
)
when (intent.action) {
Intent.ACTION_MAIN -> {
handleMainIntent(intent, defaultDestination, isNewIntent)
}
Intent.ACTION_SEND -> {
handleSendIntent(intent, false)
}
Intent.ACTION_SEND_MULTIPLE -> {
handleSendIntent(intent, true)
}
}
}
private fun handleMainIntent(intent: Intent, defaultDestination: Int, isNewIntent: Boolean) {
val navGraph = findNavController().navInflater.inflate(R.navigation.main_nav_graph)
if (intent.hasExtra("Chat")) {
Log.i("$TAG New intent with [Chat] extra")
coreContext.postOnMainThread {
@ -319,6 +342,69 @@ class MainActivity : AppCompatActivity() {
}
}
private fun handleSendIntent(intent: Intent, multiple: Boolean) {
val parcelablesUri = arrayListOf<Uri>()
if (multiple) {
val parcelables = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM)
for (parcelable in parcelables.orEmpty()) {
val uri = parcelable as? Uri
if (uri != null) {
Log.i("$TAG Found URI [$uri] in parcelable extra list")
parcelablesUri.add(uri)
}
}
} else {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) {
Log.i("$TAG Found URI [$uri] in parcelable extra")
parcelablesUri.add(uri)
}
}
val list = arrayListOf<String>()
lifecycleScope.launch() {
val deferred = arrayListOf<Deferred<String?>>()
for (uri in parcelablesUri) {
deferred.add(async { FileUtils.getFilePath(this@MainActivity, uri, false) })
}
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
if (shortcutId != null) {
Log.i("$TAG Found shortcut ID [$shortcutId]")
val pair = LinphoneUtils.getLocalAndPeerSipUrisFromChatRoomId(shortcutId)
if (pair != null) {
val localSipUri = pair.first
val remoteSipUri = pair.second
Log.i(
"$TAG Navigating to conversation with local [$localSipUri] and peer [$remoteSipUri] addresses, computed from shortcut ID"
)
intent.putExtra("LocalSipUri", localSipUri)
intent.putExtra("RemoteSipUri", remoteSipUri)
} else {
Log.e("$TAG Failed to parse shortcut ID, going to conversations list")
}
} else {
Log.i("$TAG Going into conversations list as no shortcut ID as found")
}
val navGraph = findNavController().navInflater.inflate(R.navigation.main_nav_graph)
navGraph.setStartDestination(R.id.conversationsFragment)
val paths = deferred.awaitAll()
for (path in paths) {
Log.i("$TAG Found file to share [$path] in intent")
if (path != null) list.add(path)
}
if (list.isNotEmpty()) {
sharedViewModel.filesToShareFromIntent.value = list
} else {
Log.w("$TAG Failed to find at least one file to share!")
}
findNavController().setGraph(navGraph, intent.extras)
}
}
private fun loadContacts() {
coreContext.contactsManager.loadContacts(this)

View file

@ -385,6 +385,17 @@ class ConversationFragment : GenericFragment() {
}
}
sharedViewModel.filesToShareFromIntent.observe(viewLifecycleOwner) { files ->
if (files.isNotEmpty()) {
Log.i("$TAG Found [${files.size}] files to share from intent")
for (path in files) {
viewModel.addAttachment(path)
}
sharedViewModel.filesToShareFromIntent.value = arrayListOf()
}
}
binding.sendArea.messageToSend.setControlEnterListener(object :
RichEditText.RichEditTextSendListener {
override fun onControlEnterPressedAndReleased() {

View file

@ -454,7 +454,9 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
list.add(model)
attachments.value = list
isFileAttachmentsListOpen.value = true
if (list.isNotEmpty()) {
isFileAttachmentsListOpen.value = true
}
}
@UiThread

View file

@ -107,6 +107,8 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
MutableLiveData<Event<Boolean>>()
}
val filesToShareFromIntent = MutableLiveData<ArrayList<String>>()
var displayedChatRoom: ChatRoom? = null // Prevents the need to go look for the chat room
val showConversationEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
MutableLiveData<Event<Pair<String, String>>>()

View file

@ -143,35 +143,37 @@ class FileUtils {
}
suspend fun getFilePath(context: Context, uri: Uri, overrideExisting: Boolean): String? {
val name: String = getNameFromUri(uri, context)
try {
if (Os.fstat(
ParcelFileDescriptor.open(
File(uri.path),
ParcelFileDescriptor.MODE_READ_ONLY
).fileDescriptor
).st_uid != Process.myUid()
) {
Log.e("$TAG File descriptor UID different from our, denying copy!")
return null
return withContext(Dispatchers.IO) {
val name: String = getNameFromUri(uri, context)
try {
if (Os.fstat(
ParcelFileDescriptor.open(
File(uri.path),
ParcelFileDescriptor.MODE_READ_ONLY
).fileDescriptor
).st_uid != Process.myUid()
) {
Log.e("$TAG File descriptor UID different from our, denying copy!")
return@withContext null
}
} catch (e: Exception) {
Log.e("$TAG Can't check file ownership: ", e)
}
} catch (e: Exception) {
Log.e("$TAG Can't check file ownership: ", e)
val extension = getExtensionFromFileName(name)
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
val isImage = getMimeType(type) == MimeType.Image
try {
val localFile: File = getFileStoragePath(name, isImage, overrideExisting)
copyFile(uri, localFile)
return@withContext localFile.absolutePath
} catch (e: Exception) {
Log.e("$TAG Can't copy file in local storage: ", e)
}
return@withContext null
}
val extension = getExtensionFromFileName(name)
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
val isImage = getMimeType(type) == MimeType.Image
try {
val localFile: File = getFileStoragePath(name, isImage, overrideExisting)
copyFile(uri, localFile)
return localFile.absolutePath
} catch (e: Exception) {
Log.e("$TAG Can't copy file in local storage: ", e)
}
return null
}
@AnyThread

View file

@ -43,6 +43,7 @@ class LinphoneUtils {
private const val TAG = "[Linphone Utils]"
private const val RECORDING_DATE_PATTERN = "dd-MM-yyyy-HH-mm-ss"
private const val CHAT_ROOM_ID_SEPARATOR = "~"
@WorkerThread
fun getDefaultAccount(): Account? {
@ -226,7 +227,23 @@ class LinphoneUtils {
@AnyThread
fun getChatRoomId(localSipUri: String, remoteSipUri: String): String {
return "$localSipUri~$remoteSipUri"
return "$localSipUri$CHAT_ROOM_ID_SEPARATOR$remoteSipUri"
}
@AnyThread
fun getLocalAndPeerSipUrisFromChatRoomId(id: String): Pair<String, String>? {
val split = id.split(CHAT_ROOM_ID_SEPARATOR)
if (split.size == 2) {
val localAddress = split[0]
val peerAddress = split[1]
Log.i(
"$TAG Got local [$localAddress] and peer [$peerAddress] SIP URIs from chat room id [$id]"
)
return Pair(localAddress, peerAddress)
} else {
Log.e("$TAG Failed to parse chat room id [$id]")
}
return null
}
@WorkerThread

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en"/>
</locale-config>