mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-05-06 21:33:09 +00:00
Started external file sharing
This commit is contained in:
parent
16bf6bfc2c
commit
97cae93bb5
8 changed files with 174 additions and 31 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>>>()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
4
app/src/main/res/xml/locales_config.xml
Normal file
4
app/src/main/res/xml/locales_config.xml
Normal 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>
|
||||
Loading…
Add table
Reference in a new issue