mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 03:18:06 +00:00
Merge branch 'release/6.0'
This commit is contained in:
commit
8363d41441
219 changed files with 4923 additions and 2013 deletions
|
|
@ -24,13 +24,12 @@ job-android:
|
|||
|
||||
artifacts:
|
||||
paths:
|
||||
- ./app/build/outputs/apk/debug/linphone-android-debug-*.apk
|
||||
- ./app/build/outputs/apk/release/linphone-android-release-*.apk
|
||||
when: always
|
||||
expire_in: 1 week
|
||||
expire_in: 1 day
|
||||
|
||||
|
||||
.scheduled-job-android:
|
||||
extends: job-android
|
||||
only:
|
||||
- schedules
|
||||
- schedules
|
||||
|
|
|
|||
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -11,7 +11,7 @@ Group changes to describe their impact on the project, as follows:
|
|||
Security to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
|
||||
## [6.0.0] - 2024-??-??
|
||||
## [6.0.0] - 2025-03-11
|
||||
|
||||
6.0.0 release is a complete rework of Linphone Android, with a fully redesigned UI, so it is impossible to list everything here.
|
||||
|
||||
|
|
@ -20,30 +20,38 @@ Group changes to describe their impact on the project, as follows:
|
|||
- Asymmetrical video : you no longer need to send your own camera feed to receive the one from the remote end of the call, and vice versa.
|
||||
- Improved multi account: you'll only see history, conversations, meetings etc... related to currently selected account, and you can switch the default account in two clicks.
|
||||
- Call transfer: Blind & Attended call transfer have been merged into one: during a call, if you initiate a transfer action, either pick another call to do the attended transfer or select a contact from the list (you can input a SIP URI not already in the suggestions list) to start a blind transfer.
|
||||
- User can only send up to 12 files in a single chat message.
|
||||
- IMDNs are now only sent to the message sender, preventing huge traffic in large groups, and thus the delivery status icon for received messages is now hidden in groups (as it was in 1-1 conversations).
|
||||
- Settings: a lot of them are gone, the one that are still there have been reworked to increase user friendliness.
|
||||
- Default screen (between contacts, call history, conversations & meetings list) will change depending on where you were when the app was paused or killed, and you will return to that last visited screen on the next startup.
|
||||
- Gradle files have been migrated from Groovy to Kotlin DSL, and dependencies are now in a separated file (libs.versions.toml).
|
||||
- Account creation no longer allows you to use your phone number as username, but it is still required to provide it to receive activation code by SMS.
|
||||
- Minimum supported Android OS version is now 9 (API level 28).
|
||||
- Telecom Manager support is now based on androidx.core.core-telecom package.
|
||||
- Some settings have changed name and/or section in linphonerc file.
|
||||
|
||||
### Added
|
||||
- Contacts trust: contacts for which all devices have been validated through a ZRTP call with SAS exchange are now highlighted with a blue circle (and with a red one in case of mistrust). That trust is now handled at contact level (instead of conversation level in previous versions).
|
||||
- Contacts trust: contacts for which all devices have been validated through a ZRTP call with SAS exchange are now highlighted with a blue circle (and with a red one in case of mistrust). That trust is now handled at contact level (instead of conversation level in previous versions).
|
||||
- Media & documents exchanged in a conversation can be easily found through a dedicated screen.
|
||||
- A brand new chat message search feature has been added to conversations.
|
||||
- You can now react to a chat message using any emoji.
|
||||
- If next message is also a voice recording, playback will automatically start after the currently playing one ends.
|
||||
- Chat while in call: a shortcut to a conversation screen with the remote.
|
||||
- Chat while in a conference: if the conference has a text stream enabled, you can chat with the other participants of the conference while it lasts. At the end, you'll find the messages history in the call history (and not in the list of conversations).
|
||||
- Auto export of media to native gallery even when auto download is enabled (but still not if VFS is enabled nor for ephemeral messages).
|
||||
- Save / export document & media from ephemeral messages will be disabled, and secure policy that prevents screenshots will be enforced in file viewer even if the setting is disabled.
|
||||
- Notification showing upload/download of files shared through chat will let user know the progress and keep the app alive during that process.
|
||||
- Screen sharing in conference: only desktop app starting with 6.0 version is able to start it, but on mobiles you'll be able to see it.
|
||||
- You can choose whatever ringtone you'd like for incoming calls (in Android notification channel settings).
|
||||
- Security focus: security & trust is more visible than ever, and unsecure conversations & calls are even more visible than before.
|
||||
- CardDAV: you can configure as many CardDAV servers you want to synchronize you contacts in Linphone (in addition or in replacement of native addressbook import).
|
||||
- OpenID: when used with a SSO compliant SIP server (such as Flexisip), we support single-sign-on login.
|
||||
- MWI support: display and allow to call your voicemail when you have new messages (if supported by your VoIP provider and properly configured in your account params).
|
||||
- CCMP support: if you configure a CCMP server URL in your accounts params, it will be used when scheduling meetings & to fetch list of meetings you've organized/been invited to.
|
||||
- Devices list: check on which device your sip.linphone.org account is connected and the last connection date & time (like on subscribe.linphone.org).
|
||||
- Protobuf dependency to allow logging native crashes stack traces at next app startup.
|
||||
- Android 15 startup listener, allowing us to log type of start (cold, warm, etc...) and some other useful info.
|
||||
- Dialer & in-call numpad show letters under the digit.
|
||||
|
||||
### Removed
|
||||
- Dialer: the previous home screen (dialer) has been removed, you'll find it as an input option in the new start call screen.
|
||||
|
|
|
|||
|
|
@ -134,6 +134,8 @@ adb logcat -d | ndk-stack -sym ./libs-debug/`adb shell getprop ro.product.cpu.ab
|
|||
```
|
||||
Warning: This command won't print anything until you reproduce the crash!
|
||||
|
||||
Starting [NDK r29](https://github.com/android/ndk/wiki/Changelog-r29) you will be able to directly use the ```libs-debug.zip``` file in ```ndk-stack -sym``` argument.
|
||||
|
||||
## Create an APK with a different package name
|
||||
|
||||
Simply edit the ```app/build.gradle.kts``` file and change the value of the ```packageName``` variable.
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ android {
|
|||
versionCode = 600000 // 6.00.000
|
||||
versionName = "6.0.0"
|
||||
|
||||
manifestPlaceholders["appAuthRedirectScheme"] = "org.linphone"
|
||||
manifestPlaceholders["appAuthRedirectScheme"] = packageName
|
||||
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
|
|
@ -155,6 +155,7 @@ android {
|
|||
}
|
||||
resValue("string", "linphone_app_version", gitVersion.trim())
|
||||
resValue("string", "linphone_app_branch", gitBranch.toString().trim())
|
||||
resValue("string", "linphone_openid_callback_scheme", packageName)
|
||||
|
||||
if (crashlyticsAvailable) {
|
||||
val path = File("$sdkPath/libs-debug/").toString()
|
||||
|
|
@ -177,6 +178,7 @@ android {
|
|||
resValue("string", "file_provider", "$packageName.fileprovider")
|
||||
resValue("string", "linphone_app_version", gitVersion.trim())
|
||||
resValue("string", "linphone_app_branch", gitBranch.toString().trim())
|
||||
resValue("string", "linphone_openid_callback_scheme", packageName)
|
||||
|
||||
if (crashlyticsAvailable) {
|
||||
val path = File("$sdkPath/libs-debug/").toString()
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc"/>
|
||||
|
||||
<!--<meta-data
|
||||
android:name="androidx.car.app.minCarApiLevel"
|
||||
android:value="1"/>-->
|
||||
|
|
@ -122,18 +123,6 @@
|
|||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.fileviewer.MediaViewerActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.fileviewer.FileViewerActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.welcome.WelcomeActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
|
|
@ -146,6 +135,18 @@
|
|||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.fileviewer.MediaViewerActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.fileviewer.FileViewerActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.call.CallActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
|
|
@ -199,7 +200,7 @@
|
|||
</service>
|
||||
|
||||
<!--<service
|
||||
android:name=".telecom.auto.AAService"
|
||||
android:name=".telecom.auto.AndroidAutoService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.car.app.CarAppService"/>
|
||||
|
|
|
|||
|
|
@ -22,13 +22,14 @@
|
|||
<entry name="rtp_bundle" overwrite="true">1</entry>
|
||||
<entry name="lime_server_url" overwrite="true">https://lime.linphone.org/lime-server/lime-server.php</entry>
|
||||
<entry name="lime_algo" overwrite="true">c25519</entry>
|
||||
<entry name="supported" overwrite="true"></entry>
|
||||
</section>
|
||||
<section name="nat_policy_default_values">
|
||||
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
|
||||
<entry name="protocols" overwrite="true">stun,ice</entry>
|
||||
</section>
|
||||
<section name="sip">
|
||||
<entry name="media_encryption" overwrite="true">zrtp</entry>
|
||||
<entry name="media_encryption" overwrite="true">srtp</entry>
|
||||
<entry name="media_encryption_mandatory">1</entry>
|
||||
</section>
|
||||
</config>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
<entry name="rtp_bundle" overwrite="true">0</entry>
|
||||
<entry name="lime_server_url" overwrite="true"></entry>
|
||||
<entry name="lime_algo" overwrite="true"></entry>
|
||||
<entry name="supported" overwrite="true">outbound</entry>
|
||||
</section>
|
||||
<section name="nat_policy_default_values">
|
||||
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ media_encryption=none
|
|||
update_presence_model_timestamp_before_publish_expires_refresh=1
|
||||
use_rfc2833=1
|
||||
use_info=1
|
||||
rls_uri=sips:rls@sip.linphone.org
|
||||
|
||||
[net]
|
||||
#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_MLK512,MS_ZRTP_KEY_AGREEME
|
|||
chat_messages_aggregation_delay=1000
|
||||
chat_messages_aggregation=1
|
||||
update_presence_model_timestamp_before_publish_expires_refresh=1
|
||||
rls_uri=sips:rls@sip.linphone.org
|
||||
|
||||
[sound]
|
||||
#remove this property for any application that is not Linphone public version itself
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ class LinphoneApplication : Application(), SingletonImageLoader.Factory {
|
|||
.crossfade(false)
|
||||
.components {
|
||||
add(VideoFrameDecoder.Factory())
|
||||
// add(GifDecoder.Factory) // Do not add it, GIFs are properly rendered without it and adding it breaks resizing...
|
||||
add(SvgDecoder.Factory())
|
||||
}
|
||||
.memoryCache {
|
||||
|
|
|
|||
|
|
@ -27,13 +27,10 @@ import android.view.View
|
|||
import android.view.contentcapture.ContentCaptureContext
|
||||
import android.view.contentcapture.ContentCaptureSession
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
class Api29Compatibility {
|
||||
companion object {
|
||||
private const val TAG = "[API 29 Compatibility]"
|
||||
|
||||
fun getMediaCollectionUri(isImage: Boolean, isVideo: Boolean, isAudio: Boolean): Uri {
|
||||
return when {
|
||||
isImage -> {
|
||||
|
|
@ -59,11 +56,10 @@ class Api29Compatibility {
|
|||
return intent.getStringExtra(Intent.EXTRA_LOCUS_ID)
|
||||
}
|
||||
|
||||
fun setLocusIdInContentCaptureSession(root: View, localSipUri: String, remoteSipUri: String) {
|
||||
fun setLocusIdInContentCaptureSession(root: View, conversationId: String) {
|
||||
val session: ContentCaptureSession? = root.contentCaptureSession
|
||||
if (session != null) {
|
||||
val id = LinphoneUtils.getChatRoomId(localSipUri, remoteSipUri)
|
||||
session.contentCaptureContext = ContentCaptureContext.forLocusId(id)
|
||||
session.contentCaptureContext = ContentCaptureContext.forLocusId(conversationId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ import androidx.annotation.RequiresApi
|
|||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
class Api33Compatibility {
|
||||
companion object {
|
||||
private const val TAG = "[API 33 Compatibility]"
|
||||
|
||||
fun getAllRequiredPermissionsArray(): Array<String> {
|
||||
return arrayOf(
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
|
|
|
|||
|
|
@ -24,12 +24,11 @@ import android.app.NotificationManager
|
|||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.linphone.core.tools.Log
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
class Api34Compatibility {
|
||||
|
|
@ -69,10 +68,10 @@ class Api34Compatibility {
|
|||
val intent = Intent()
|
||||
// See https://developer.android.com/reference/android/provider/Settings#ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT
|
||||
intent.action = Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT
|
||||
intent.data = Uri.parse("package:${context.packageName}")
|
||||
intent.data = "package:${context.packageName}".toUri()
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
Log.i("$TAG Starting ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT")
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
context.startActivity(intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ class Compatibility {
|
|||
companion object {
|
||||
private const val TAG = "[Compatibility]"
|
||||
|
||||
const val FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1 // ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
const val FOREGROUND_SERVICE_TYPE_PHONE_CALL = 4 // ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
||||
const val FOREGROUND_SERVICE_TYPE_CAMERA = 64 // ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
||||
const val FOREGROUND_SERVICE_TYPE_MICROPHONE = 128 // ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
|
|
@ -159,12 +158,11 @@ class Compatibility {
|
|||
return null
|
||||
}
|
||||
|
||||
fun setLocusIdInContentCaptureSession(root: View, localSipUri: String, remoteSipUri: String) {
|
||||
fun setLocusIdInContentCaptureSession(root: View, conversationId: String) {
|
||||
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) {
|
||||
return Api29Compatibility.setLocusIdInContentCaptureSession(
|
||||
root,
|
||||
localSipUri,
|
||||
remoteSipUri
|
||||
conversationId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import androidx.core.content.res.ResourcesCompat
|
|||
import androidx.core.graphics.drawable.IconCompat
|
||||
import org.linphone.R
|
||||
import org.linphone.utils.AppUtils
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
|
||||
class AvatarGenerator(private val context: Context) {
|
||||
private var textSize: Float = AppUtils.getDimension(R.dimen.avatar_initials_text_size)
|
||||
|
|
@ -74,7 +76,7 @@ class AvatarGenerator(private val context: Context) {
|
|||
val textPainter = getTextPainter()
|
||||
val painter = if (useTransparentBackground) getTransparentPainter() else getBackgroundPainter()
|
||||
|
||||
val bitmap = Bitmap.createBitmap(avatarSize, avatarSize, Bitmap.Config.ARGB_8888)
|
||||
val bitmap = createBitmap(avatarSize, avatarSize)
|
||||
val canvas = Canvas(bitmap)
|
||||
val areaRect = Rect(0, 0, avatarSize, avatarSize)
|
||||
val bounds = RectF(areaRect)
|
||||
|
|
@ -91,7 +93,7 @@ class AvatarGenerator(private val context: Context) {
|
|||
}
|
||||
|
||||
fun buildDrawable(): BitmapDrawable {
|
||||
return BitmapDrawable(context.resources, buildBitmap(true))
|
||||
return buildBitmap(true).toDrawable(context.resources)
|
||||
}
|
||||
|
||||
fun buildIcon(): IconCompat {
|
||||
|
|
|
|||
|
|
@ -143,6 +143,14 @@ class ContactsManager
|
|||
}
|
||||
|
||||
private val friendListListener: FriendListListenerStub = object : FriendListListenerStub() {
|
||||
@WorkerThread
|
||||
override fun onPresenceReceived(friendList: FriendList, friends: Array<out Friend?>) {
|
||||
if (friendList.isSubscriptionBodyless) {
|
||||
Log.i("$TAG Bodyless friendlist [${friendList.displayName}] presence received")
|
||||
notifyContactsListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun onNewSipAddressDiscovered(
|
||||
friendList: FriendList,
|
||||
|
|
@ -575,10 +583,12 @@ class ContactsManager
|
|||
|
||||
@WorkerThread
|
||||
fun onCoreStarted(core: Core) {
|
||||
Log.i("$TAG Core has been started")
|
||||
loadContactsOnlyFromDefaultDirectory = corePreferences.fetchContactsFromDefaultDirectory
|
||||
|
||||
core.addListener(coreListener)
|
||||
for (list in core.friendsLists) {
|
||||
Log.i("$TAG Found existing friend list [${list.displayName}]")
|
||||
list.addListener(friendListListener)
|
||||
}
|
||||
|
||||
|
|
@ -604,6 +614,7 @@ class ContactsManager
|
|||
|
||||
@WorkerThread
|
||||
fun onCoreStopped(core: Core) {
|
||||
Log.w("$TAG Core has been stopped")
|
||||
coroutineScope.cancel()
|
||||
|
||||
core.removeListener(coreListener)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import org.linphone.ui.call.CallActivity
|
|||
import org.linphone.utils.ActivityMonitor
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class CoreContext
|
||||
|
|
@ -92,6 +93,10 @@ class CoreContext
|
|||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val clearAuthenticationRequestDialogEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val refreshMicrophoneMuteStateEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
|
@ -112,6 +117,11 @@ class CoreContext
|
|||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private var filesToExportToNativeMediaGallery = arrayListOf<String>()
|
||||
val filesToExportToNativeMediaGalleryEvent: MutableLiveData<Event<List<String>>> by lazy {
|
||||
MutableLiveData<Event<List<String>>>()
|
||||
}
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
private lateinit var coreThread: Handler
|
||||
|
||||
|
|
@ -155,6 +165,42 @@ class CoreContext
|
|||
private var previousCallState = Call.State.Idle
|
||||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
@WorkerThread
|
||||
override fun onMessagesReceived(
|
||||
core: Core,
|
||||
chatRoom: ChatRoom,
|
||||
messages: Array<out ChatMessage?>
|
||||
) {
|
||||
if (corePreferences.makePublicMediaFilesDownloaded && core.maxSizeForAutoDownloadIncomingFiles >= 0) {
|
||||
for (message in messages) {
|
||||
// Never do auto media export for ephemeral messages!
|
||||
if (message?.isEphemeral == true) continue
|
||||
|
||||
for (content in message?.contents.orEmpty()) {
|
||||
if (content.isFile) {
|
||||
val path = content.filePath
|
||||
if (path.isNullOrEmpty()) continue
|
||||
|
||||
val mime = "${content.type}/${content.subtype}"
|
||||
val mimeType = FileUtils.getMimeType(mime)
|
||||
when (mimeType) {
|
||||
FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> {
|
||||
Log.i("$TAG Added file path [$path] to the list of media to export to native media gallery")
|
||||
filesToExportToNativeMediaGallery.add(path)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToExportToNativeMediaGallery.isNotEmpty()) {
|
||||
Log.i("$TAG Creating event with [${filesToExportToNativeMediaGallery.size}] files to export to native media gallery")
|
||||
filesToExportToNativeMediaGalleryEvent.postValue(Event(filesToExportToNativeMediaGallery))
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun onGlobalStateChanged(core: Core, state: GlobalState, message: String) {
|
||||
Log.i("$TAG Global state changed [$state]")
|
||||
|
|
@ -163,6 +209,8 @@ class CoreContext
|
|||
// Wait for GlobalState.ON as some settings modification won't be saved
|
||||
// in RC file if Core isn't ON
|
||||
onCoreStarted()
|
||||
} else if (state == GlobalState.Shutdown) {
|
||||
onCoreStopped()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -347,6 +395,7 @@ class CoreContext
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun onAccountAdded(core: Core, account: Account) {
|
||||
// Prevent this trigger when core is stopped/start in remote prov
|
||||
if (core.globalState == GlobalState.Off) return
|
||||
|
|
@ -368,6 +417,15 @@ class CoreContext
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun onAccountRemoved(core: Core, account: Account) {
|
||||
Log.i("$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] removed, clearing auth request dialog if needed")
|
||||
if (account.findAuthInfo() == digestAuthInfoPendingPasswordUpdate) {
|
||||
Log.i("$TAG Removed account matches auth info pending password update, removing dialog")
|
||||
clearAuthenticationRequestDialogEvent.postValue(Event(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var logcatEnabled: Boolean = corePreferences.printLogsInLogcat
|
||||
|
|
@ -415,7 +473,9 @@ class CoreContext
|
|||
}
|
||||
Log.i("=========================================")
|
||||
Log.i("==== Linphone-android information dump ====")
|
||||
Log.i("VERSION=${BuildConfig.VERSION_NAME} / ${BuildConfig.VERSION_CODE}")
|
||||
val gitVersion = AppUtils.getString(org.linphone.R.string.linphone_app_version)
|
||||
val gitBranch = AppUtils.getString(org.linphone.R.string.linphone_app_branch)
|
||||
Log.i("VERSION=${BuildConfig.VERSION_NAME} / ${BuildConfig.VERSION_CODE} ($gitVersion from $gitBranch branch)")
|
||||
Log.i("PACKAGE=${BuildConfig.APPLICATION_ID}")
|
||||
Log.i("BUILD TYPE=${BuildConfig.BUILD_TYPE}")
|
||||
Log.i("=========================================")
|
||||
|
|
@ -498,6 +558,14 @@ class CoreContext
|
|||
Log.i("$TAG Started contacts, telecom & notifications managers")
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun onCoreStopped() {
|
||||
Log.w("$TAG Core is being shut down, notifying managers so they can remove their listeners and do some cleanup if needed")
|
||||
contactsManager.onCoreStopped(core)
|
||||
telecomManager.onCoreStopped(core)
|
||||
notificationsManager.onCoreStopped(core)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun destroyCore() {
|
||||
if (!::core.isInitialized) {
|
||||
|
|
@ -520,10 +588,6 @@ class CoreContext
|
|||
|
||||
core.stop()
|
||||
|
||||
contactsManager.onCoreStopped(core)
|
||||
telecomManager.onCoreStopped(core)
|
||||
notificationsManager.onCoreStopped(core)
|
||||
|
||||
// It's very unlikely the process will survive until the Core reaches GlobalStateOff sadly
|
||||
Log.w("$TAG Core has been shut down")
|
||||
exitProcess(0)
|
||||
|
|
@ -620,6 +684,11 @@ class CoreContext
|
|||
return found != null
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun clearFilesToExportToNativeGallery() {
|
||||
filesToExportToNativeMediaGallery.clear()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun startAudioCall(
|
||||
address: Address,
|
||||
|
|
@ -728,7 +797,9 @@ class CoreContext
|
|||
|
||||
@WorkerThread
|
||||
fun answerCall(call: Call) {
|
||||
Log.i("$TAG Answering call $call")
|
||||
Log.i(
|
||||
"$TAG Answering call with remote address [${call.remoteAddress.asStringUriOnly()}] and to address [${call.toAddress.asStringUriOnly()}]"
|
||||
)
|
||||
val params = core.createCallParams(call)
|
||||
if (params == null) {
|
||||
Log.w("$TAG Answering call without params!")
|
||||
|
|
|
|||
|
|
@ -142,12 +142,29 @@ class CorePreferences
|
|||
|
||||
// Conversation related
|
||||
|
||||
@get:WorkerThread @set:WorkerThread
|
||||
var markConversationAsReadWhenDismissingMessageNotification: Boolean
|
||||
get() = config.getBool("app", "mark_as_read_notif_dismissal", false)
|
||||
set(value) {
|
||||
config.setBool("app", "mark_as_read_notif_dismissal", value)
|
||||
}
|
||||
|
||||
var makePublicMediaFilesDownloaded: Boolean
|
||||
// Keep old name for backward compatibility
|
||||
get() = config.getBool("app", "make_downloaded_images_public_in_gallery", false)
|
||||
set(value) {
|
||||
config.setBool("app", "make_downloaded_images_public_in_gallery", value)
|
||||
}
|
||||
|
||||
// Conference related
|
||||
|
||||
@get:WorkerThread @set:WorkerThread
|
||||
var createEndToEndEncryptedMeetingsAndGroupCalls: Boolean
|
||||
get() = config.getBool("app", "create_e2e_encrypted_conferences", false)
|
||||
set(value) {
|
||||
config.setBool("app", "create_e2e_encrypted_conferences", value)
|
||||
}
|
||||
|
||||
// Contacts related
|
||||
|
||||
@get:WorkerThread @set:WorkerThread
|
||||
|
|
@ -266,6 +283,10 @@ class CorePreferences
|
|||
val hideAssistantThirdPartySipAccount: Boolean
|
||||
get() = config.getBool("ui", "assistant_hide_third_party_account", false)
|
||||
|
||||
@get:WorkerThread
|
||||
val singleSignOnClientId: String
|
||||
get() = config.getString("app", "oidc_client_id", "linphone")!!
|
||||
|
||||
@get:WorkerThread
|
||||
val useUsernameAsSingleSignOnLoginHint: Boolean
|
||||
get() = config.getBool("ui", "use_username_as_sso_login_hint", false)
|
||||
|
|
@ -328,6 +349,10 @@ class CorePreferences
|
|||
val ssoCacheFile: String
|
||||
get() = context.filesDir.absolutePath + "/auth_state.json"
|
||||
|
||||
@get:AnyThread
|
||||
val messageReceivedInVisibleConversationNotificationSound: String
|
||||
get() = context.filesDir.absolutePath + "/share/sounds/linphone/incoming_chat.wav"
|
||||
|
||||
@UiThread
|
||||
fun copyAssetsFromPackage() {
|
||||
copy("linphonerc_default", configPath)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import android.util.Base64
|
|||
import android.util.Pair
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import java.math.BigInteger
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.KeyStore
|
||||
|
|
@ -38,6 +39,7 @@ import javax.crypto.KeyGenerator
|
|||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import org.linphone.core.tools.Log
|
||||
import androidx.core.content.edit
|
||||
|
||||
class VFS {
|
||||
companion object {
|
||||
|
|
@ -72,7 +74,13 @@ class VFS {
|
|||
return false
|
||||
}
|
||||
|
||||
preferences.edit().putBoolean("vfs_enabled", true).apply()
|
||||
preferences.edit { putBoolean("vfs_enabled", true) }
|
||||
|
||||
if (corePreferences.makePublicMediaFilesDownloaded) {
|
||||
Log.w("$TAG VFS is now enabled, disabling auto export of media files to native gallery")
|
||||
corePreferences.makePublicMediaFilesDownloaded = false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -91,10 +99,10 @@ class VFS {
|
|||
generateSecretKey()
|
||||
encryptToken(generateToken()).let { data ->
|
||||
preferences
|
||||
.edit()
|
||||
.putString(VFS_IV, data.first)
|
||||
.putString(VFS_KEY, data.second)
|
||||
.commit()
|
||||
.edit(commit = true) {
|
||||
putString(VFS_IV, data.first)
|
||||
.putString(VFS_KEY, data.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import android.content.pm.PackageManager
|
|||
import android.graphics.Bitmap
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaPlayer
|
||||
import android.media.RingtoneManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
|
|
@ -71,6 +72,8 @@ import org.linphone.core.MediaDirection
|
|||
import org.linphone.core.tools.Log
|
||||
import org.linphone.ui.call.CallActivity
|
||||
import org.linphone.ui.main.MainActivity
|
||||
import org.linphone.ui.main.MainActivity.Companion.ARGUMENTS_CHAT
|
||||
import org.linphone.ui.main.MainActivity.Companion.ARGUMENTS_CONVERSATION_ID
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
|
@ -122,6 +125,8 @@ class NotificationsManager
|
|||
private var currentlyDisplayedChatRoomId: String = ""
|
||||
private var currentlyDisplayedIncomingCallFragment: Boolean = false
|
||||
|
||||
private lateinit var mediaPlayer: MediaPlayer
|
||||
|
||||
private val contactsListener = object : ContactsListener {
|
||||
@WorkerThread
|
||||
override fun onContactsLoaded() { }
|
||||
|
|
@ -262,11 +267,22 @@ class NotificationsManager
|
|||
Log.i("$TAG Received ${messages.size} aggregated messages")
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
if (currentlyDisplayedChatRoomId.isNotEmpty() && id == currentlyDisplayedChatRoomId) {
|
||||
Log.i(
|
||||
"$TAG Do not notify received messages for currently displayed conversation [$id]"
|
||||
"$TAG Do not notify received messages for currently displayed conversation [$id], but play sound if at least one message is incoming and not read"
|
||||
)
|
||||
|
||||
var playSound = false
|
||||
for (message in messages) {
|
||||
if (!message.isOutgoing && !message.isRead) {
|
||||
playSound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (playSound) {
|
||||
playMessageReceivedSound()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -301,7 +317,7 @@ class NotificationsManager
|
|||
"$TAG Reaction received [${reaction.body}] from [${address.asStringUriOnly()}] for message [$message]"
|
||||
)
|
||||
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
/*if (id == currentlyDisplayedChatRoomId) {
|
||||
Log.i(
|
||||
"$TAG Do not notify received reaction for currently displayed conversation [$id]"
|
||||
|
|
@ -340,7 +356,7 @@ class NotificationsManager
|
|||
if (corePreferences.disableChat) return
|
||||
|
||||
if (chatRoom.muted) {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Conversation $id has been muted")
|
||||
return
|
||||
}
|
||||
|
|
@ -370,7 +386,7 @@ class NotificationsManager
|
|||
val notification = createMessageNotification(
|
||||
notifiable,
|
||||
pendingIntent,
|
||||
LinphoneUtils.getChatRoomId(chatRoom),
|
||||
LinphoneUtils.getConversationId(chatRoom),
|
||||
me
|
||||
)
|
||||
notify(notifiable.notificationId, notification, CHAT_TAG)
|
||||
|
|
@ -389,7 +405,7 @@ class NotificationsManager
|
|||
@WorkerThread
|
||||
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
|
||||
Log.i(
|
||||
"$TAG Conversation [${LinphoneUtils.getChatRoomId(chatRoom)}] has been marked as read, removing notification if any"
|
||||
"$TAG Conversation [${LinphoneUtils.getConversationId(chatRoom)}] has been marked as read, removing notification if any"
|
||||
)
|
||||
dismissChatNotification(chatRoom)
|
||||
}
|
||||
|
|
@ -533,6 +549,21 @@ class NotificationsManager
|
|||
core.addListener(coreListener)
|
||||
|
||||
coreContext.contactsManager.addListener(contactsListener)
|
||||
|
||||
val soundPath = corePreferences.messageReceivedInVisibleConversationNotificationSound
|
||||
mediaPlayer = MediaPlayer().apply {
|
||||
try {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.build()
|
||||
)
|
||||
setDataSource(soundPath)
|
||||
prepare()
|
||||
} catch (e: Exception) {
|
||||
Log.e("$TAG Failed to prepare message received sound file [$soundPath]: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
@ -786,7 +817,7 @@ class NotificationsManager
|
|||
val address = chatRoom.peerAddress.asStringUriOnly()
|
||||
var notifiable: Notifiable? = chatNotificationsMap[address]
|
||||
if (notifiable == null) {
|
||||
notifiable = Notifiable(LinphoneUtils.getChatRoomId(chatRoom).hashCode())
|
||||
notifiable = Notifiable(LinphoneUtils.getConversationId(chatRoom).hashCode())
|
||||
notifiable.myself = LinphoneUtils.getDisplayName(chatRoom.localAddress)
|
||||
notifiable.localIdentity = chatRoom.localAddress.asStringUriOnly()
|
||||
notifiable.remoteAddress = chatRoom.peerAddress.asStringUriOnly()
|
||||
|
|
@ -834,7 +865,7 @@ class NotificationsManager
|
|||
val notification = createMessageNotification(
|
||||
notifiable,
|
||||
pendingIntent,
|
||||
LinphoneUtils.getChatRoomId(chatRoom),
|
||||
LinphoneUtils.getConversationId(chatRoom),
|
||||
me
|
||||
)
|
||||
notify(notifiable.notificationId, notification, CHAT_TAG)
|
||||
|
|
@ -899,7 +930,7 @@ class NotificationsManager
|
|||
val notification = createMessageNotification(
|
||||
notifiable,
|
||||
pendingIntent,
|
||||
LinphoneUtils.getChatRoomId(chatRoom),
|
||||
LinphoneUtils.getConversationId(chatRoom),
|
||||
me
|
||||
)
|
||||
notify(notifiable.notificationId, notification, CHAT_TAG)
|
||||
|
|
@ -928,7 +959,7 @@ class NotificationsManager
|
|||
val notification = createMessageNotification(
|
||||
notifiable,
|
||||
pendingIntent,
|
||||
LinphoneUtils.getChatRoomId(chatRoom),
|
||||
LinphoneUtils.getConversationId(chatRoom),
|
||||
me
|
||||
)
|
||||
Log.i(
|
||||
|
|
@ -1103,7 +1134,7 @@ class NotificationsManager
|
|||
}
|
||||
|
||||
Log.i(
|
||||
"Creating notification for [${if (isIncoming) "incoming" else "outgoing"}] [${if (isConference) "conference" else "call"}] with video [${if (isVideo) "enabled" else "disabled"}] on channel [$channel]"
|
||||
"Creating notification for ${if (isIncoming) "[incoming] " else ""}[${if (isConference) "conference" else "call"}] with video [${if (isVideo) "enabled" else "disabled"}] on channel [$channel]"
|
||||
)
|
||||
|
||||
val builder = NotificationCompat.Builder(
|
||||
|
|
@ -1295,7 +1326,7 @@ class NotificationsManager
|
|||
return true
|
||||
} else {
|
||||
val previousNotificationId = previousChatNotifications.find { id ->
|
||||
id == LinphoneUtils.getChatRoomId(chatRoom).hashCode()
|
||||
id == LinphoneUtils.getConversationId(chatRoom).hashCode()
|
||||
}
|
||||
if (previousNotificationId != null) {
|
||||
Log.i(
|
||||
|
|
@ -1362,7 +1393,7 @@ class NotificationsManager
|
|||
val notification = createMessageNotification(
|
||||
notifiable,
|
||||
pendingIntent,
|
||||
LinphoneUtils.getChatRoomId(chatRoom),
|
||||
LinphoneUtils.getConversationId(chatRoom),
|
||||
me
|
||||
)
|
||||
notify(notifiable.notificationId, notification, CHAT_TAG)
|
||||
|
|
@ -1551,7 +1582,7 @@ class NotificationsManager
|
|||
val id = context.getString(R.string.notification_channel_call_id)
|
||||
val name = context.getString(R.string.notification_channel_call_name)
|
||||
|
||||
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = name
|
||||
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
}
|
||||
|
|
@ -1585,9 +1616,8 @@ class NotificationsManager
|
|||
@WorkerThread
|
||||
private fun getChatRoomPendingIntent(chatRoom: ChatRoom, notificationId: Int): PendingIntent {
|
||||
val args = Bundle()
|
||||
args.putBoolean("Chat", true)
|
||||
args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
|
||||
args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
|
||||
args.putBoolean(ARGUMENTS_CHAT, true)
|
||||
args.putString(ARGUMENTS_CONVERSATION_ID, LinphoneUtils.getConversationId(chatRoom))
|
||||
|
||||
// Not using NavDeepLinkBuilder to prevent stacking a ConversationsListFragment above another one
|
||||
return TaskStackBuilder.create(context).run {
|
||||
|
|
@ -1605,6 +1635,17 @@ class NotificationsManager
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun playMessageReceivedSound() {
|
||||
if (::mediaPlayer.isInitialized) {
|
||||
try {
|
||||
mediaPlayer.start()
|
||||
} catch (e: Exception) {
|
||||
Log.e("$TAG Failed to play message received sound file: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Notifiable(val notificationId: Int) {
|
||||
var myself: String? = null
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,11 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.core.AudioDevice
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.CallListenerStub
|
||||
import org.linphone.core.Reason
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AudioUtils
|
||||
import org.linphone.utils.Event
|
||||
|
|
@ -50,6 +52,7 @@ class TelecomCallControlCallback(
|
|||
|
||||
private var availableEndpoints: List<CallEndpointCompat> = arrayListOf()
|
||||
private var currentEndpoint = CallEndpointCompat.TYPE_UNKNOWN
|
||||
private var endpointUpdateRequestFromLinphone: Boolean = false
|
||||
|
||||
private val callListener = object : CallListenerStub() {
|
||||
@WorkerThread
|
||||
|
|
@ -64,9 +67,14 @@ class TelecomCallControlCallback(
|
|||
CallAttributesCompat.Companion.CALL_TYPE_AUDIO_CALL
|
||||
}
|
||||
scope.launch {
|
||||
Log.i("$TAG Answering ${if (isVideo) "video" else "audio"} call")
|
||||
Log.i("$TAG Answering [${if (isVideo) "video" else "audio"}] call")
|
||||
callControl.answer(type)
|
||||
}
|
||||
|
||||
if (isVideo && corePreferences.routeAudioToSpeakerWhenVideoIsEnabled) {
|
||||
Log.i("$TAG Answering video call, routing audio to speaker")
|
||||
AudioUtils.routeAudioToSpeaker(call)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Log.i("$TAG Setting call active")
|
||||
|
|
@ -74,18 +82,39 @@ class TelecomCallControlCallback(
|
|||
}
|
||||
}
|
||||
} else if (state == Call.State.End) {
|
||||
val reason = call.reason
|
||||
val direction = call.dir
|
||||
scope.launch {
|
||||
Log.i("$TAG Disconnecting call because it has ended")
|
||||
callControl.disconnect(DisconnectCause(DisconnectCause.LOCAL))
|
||||
val disconnectCause = when (reason) {
|
||||
Reason.NotAnswered -> DisconnectCause.REMOTE
|
||||
Reason.Declined -> DisconnectCause.REJECTED
|
||||
Reason.Busy -> {
|
||||
if (direction == Call.Dir.Incoming) {
|
||||
DisconnectCause.MISSED
|
||||
} else {
|
||||
DisconnectCause.BUSY
|
||||
}
|
||||
}
|
||||
else -> DisconnectCause.LOCAL
|
||||
}
|
||||
Log.i("$TAG Disconnecting [${if (direction == Call.Dir.Incoming)"incoming" else "outgoing"}] call with cause [${disconnectCauseToString(disconnectCause)}] because it has ended with reason [$reason]")
|
||||
try {
|
||||
callControl.disconnect(DisconnectCause(disconnectCause))
|
||||
} catch (ise: IllegalArgumentException) {
|
||||
Log.e("$TAG Couldn't disconnect call control with cause [${disconnectCauseToString(disconnectCause)}]: $ise")
|
||||
}
|
||||
}
|
||||
} else if (state == Call.State.Error) {
|
||||
val reason = call.reason
|
||||
scope.launch {
|
||||
Log.w("$TAG Disconnecting call due to error [$message]")
|
||||
// For some reason DisconnectCause.ERROR or DisconnectCause.BUSY triggers an IllegalArgumentException with following message
|
||||
// Valid DisconnectCause codes are limited to [DisconnectCause.LOCAL, DisconnectCause.REMOTE, DisconnectCause.MISSED, or DisconnectCause.REJECTED]
|
||||
val disconnectCause = DisconnectCause.REJECTED
|
||||
Log.w("$TAG Disconnecting call with cause [${disconnectCauseToString(disconnectCause)}] due to error [$message] and reason [$reason]")
|
||||
try {
|
||||
// For some reason DisconnectCause.ERROR triggers an IllegalArgumentException
|
||||
callControl.disconnect(DisconnectCause(DisconnectCause.REJECTED))
|
||||
callControl.disconnect(DisconnectCause(disconnectCause))
|
||||
} catch (ise: IllegalArgumentException) {
|
||||
Log.e("$TAG Couldn't terminate call control with REJECTED cause: $ise")
|
||||
Log.e("$TAG Couldn't disconnect call control with cause [${disconnectCauseToString(disconnectCause)}]: $ise")
|
||||
}
|
||||
}
|
||||
} else if (state == Call.State.Pausing) {
|
||||
|
|
@ -134,11 +163,23 @@ class TelecomCallControlCallback(
|
|||
}.launchIn(scope)
|
||||
|
||||
callControl.currentCallEndpoint.onEach { endpoint ->
|
||||
Log.i("$TAG We're asked to use [${endpoint.name}] audio endpoint")
|
||||
val type = endpoint.type
|
||||
currentEndpoint = type
|
||||
if (endpointUpdateRequestFromLinphone) {
|
||||
Log.i("$TAG Linphone requests to use [${endpoint.name}] audio endpoint with type [$type]")
|
||||
} else {
|
||||
Log.i("$TAG Android requests us to use [${endpoint.name}] audio endpoint with type [$type]")
|
||||
}
|
||||
|
||||
if (!endpointUpdateRequestFromLinphone && !coreContext.isConnectedToAndroidAuto && (type == CallEndpointCompat.Companion.TYPE_EARPIECE || type == CallEndpointCompat.Companion.TYPE_SPEAKER)) {
|
||||
Log.w("$TAG Device isn't connected to Android Auto, do not follow system request to change audio endpoint to either earpiece or speaker")
|
||||
return@onEach
|
||||
}
|
||||
|
||||
// Change audio route in SDK, this way the usual listener will trigger
|
||||
// and we'll be able to update the UI accordingly
|
||||
val route = arrayListOf<AudioDevice.Type>()
|
||||
when (endpoint.type) {
|
||||
when (type) {
|
||||
CallEndpointCompat.Companion.TYPE_EARPIECE -> {
|
||||
route.add(AudioDevice.Type.Earpiece)
|
||||
}
|
||||
|
|
@ -154,7 +195,6 @@ class TelecomCallControlCallback(
|
|||
}
|
||||
}
|
||||
if (route.isNotEmpty()) {
|
||||
currentEndpoint = endpoint.type
|
||||
coreContext.postOnCoreThread {
|
||||
if (!AudioUtils.applyAudioRouteChangeInLinphone(call, route)) {
|
||||
Log.w("$TAG Failed to apply audio route change, trying again in 200ms")
|
||||
|
|
@ -164,6 +204,7 @@ class TelecomCallControlCallback(
|
|||
}
|
||||
}
|
||||
}
|
||||
endpointUpdateRequestFromLinphone = false
|
||||
}.launchIn(scope)
|
||||
|
||||
callControl.isMuted.onEach { muted ->
|
||||
|
|
@ -193,6 +234,7 @@ class TelecomCallControlCallback(
|
|||
}
|
||||
|
||||
fun applyAudioRouteToCallWithId(routes: List<AudioDevice.Type>): Boolean {
|
||||
endpointUpdateRequestFromLinphone = true
|
||||
Log.i("$TAG Looking for audio endpoint with type [${routes.first()}]")
|
||||
|
||||
var wiredHeadsetFound = false
|
||||
|
|
@ -260,4 +302,23 @@ class TelecomCallControlCallback(
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun disconnectCauseToString(cause: Int): String {
|
||||
return when (cause) {
|
||||
DisconnectCause.UNKNOWN -> "UNKNOWN"
|
||||
DisconnectCause.ERROR -> "ERROR"
|
||||
DisconnectCause.LOCAL -> "LOCAL"
|
||||
DisconnectCause.REMOTE -> "REMOTE"
|
||||
DisconnectCause.CANCELED -> "CANCELED"
|
||||
DisconnectCause.MISSED -> "MISSED"
|
||||
DisconnectCause.REJECTED -> "REJECTED"
|
||||
DisconnectCause.BUSY -> "BUSY"
|
||||
DisconnectCause.RESTRICTED -> "RESTRICTED"
|
||||
DisconnectCause.OTHER -> "OTHER"
|
||||
DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED -> "CONNECTION_MANAGER_NOT_SUPPORTED"
|
||||
DisconnectCause.ANSWERED_ELSEWHERE -> "ANSWERED_ELSEWHERE"
|
||||
DisconnectCause.CALL_PULLED -> "CALL_PULLED"
|
||||
else -> "UNEXPECTED: $cause"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@
|
|||
package org.linphone.telecom
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.telecom.CallAttributesCompat
|
||||
import androidx.core.telecom.CallException
|
||||
import androidx.core.telecom.CallsManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -35,6 +35,7 @@ import org.linphone.core.Core
|
|||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class TelecomManager
|
||||
@WorkerThread
|
||||
|
|
@ -51,8 +52,15 @@ class TelecomManager
|
|||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
@WorkerThread
|
||||
override fun onCallCreated(core: Core, call: Call) {
|
||||
onCallCreated(call)
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State?,
|
||||
message: String
|
||||
) {
|
||||
if (state == Call.State.IncomingReceived || state == Call.State.OutgoingProgress) {
|
||||
onCallCreated(call)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
@ -88,10 +96,10 @@ class TelecomManager
|
|||
|
||||
@WorkerThread
|
||||
fun onCallCreated(call: Call) {
|
||||
Log.i("$TAG Call to [${call.remoteAddress.asStringUriOnly()}] created")
|
||||
Log.i("$TAG Call to [${call.remoteAddress.asStringUriOnly()}] created in state [${call.state}]")
|
||||
|
||||
val address = call.callLog.remoteAddress
|
||||
val uri = Uri.parse(address.asStringUriOnly())
|
||||
val uri = address.asStringUriOnly().toUri()
|
||||
|
||||
val direction = if (call.dir == Call.Dir.Outgoing) {
|
||||
CallAttributesCompat.DIRECTION_OUTGOING
|
||||
|
|
@ -99,9 +107,9 @@ class TelecomManager
|
|||
CallAttributesCompat.DIRECTION_INCOMING
|
||||
}
|
||||
|
||||
val conferenceInfo = LinphoneUtils.getConferenceInfoIfAny(call)
|
||||
val capabilities = CallAttributesCompat.SUPPORTS_SET_INACTIVE or CallAttributesCompat.SUPPORTS_TRANSFER
|
||||
|
||||
val conferenceInfo = LinphoneUtils.getConferenceInfoIfAny(call)
|
||||
val displayName = if (call.conference != null || conferenceInfo != null) {
|
||||
conferenceInfo?.subject ?: call.conference?.subject ?: LinphoneUtils.getDisplayName(address)
|
||||
} else {
|
||||
|
|
@ -109,20 +117,24 @@ class TelecomManager
|
|||
friend?.name ?: LinphoneUtils.getDisplayName(address)
|
||||
}
|
||||
|
||||
// When call is created, it is ringing (incoming or outgoing, do not set video)
|
||||
val type = CallAttributesCompat.Companion.CALL_TYPE_AUDIO_CALL
|
||||
|
||||
val callAttributes = CallAttributesCompat(
|
||||
displayName,
|
||||
uri,
|
||||
direction,
|
||||
type,
|
||||
capabilities
|
||||
)
|
||||
Log.i("$TAG Adding call to Telecom's CallsManager with attributes [$callAttributes]")
|
||||
val isVideo = LinphoneUtils.isVideoEnabled(call)
|
||||
val type = if (isVideo) {
|
||||
CallAttributesCompat.Companion.CALL_TYPE_VIDEO_CALL
|
||||
} else {
|
||||
CallAttributesCompat.Companion.CALL_TYPE_AUDIO_CALL
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
val callAttributes = CallAttributesCompat(
|
||||
displayName,
|
||||
uri,
|
||||
direction,
|
||||
type,
|
||||
capabilities
|
||||
)
|
||||
Log.i("$TAG Adding call to Telecom's CallsManager with attributes [$callAttributes]")
|
||||
|
||||
callsManager.addCall(
|
||||
callAttributes,
|
||||
{ callType -> // onAnswer
|
||||
|
|
@ -160,6 +172,11 @@ class TelecomManager
|
|||
) {
|
||||
val callbacks = TelecomCallControlCallback(call, this, scope)
|
||||
|
||||
// We must first call setCallback on callControlScope before using it
|
||||
callbacks.onCallControlCallbackSet()
|
||||
currentlyFollowedCalls += 1
|
||||
Log.i("$TAG Call added to Telecom's CallsManager")
|
||||
|
||||
coreContext.postOnCoreThread {
|
||||
val callId = call.callLog.callId.orEmpty()
|
||||
if (callId.isNotEmpty()) {
|
||||
|
|
@ -167,13 +184,8 @@ class TelecomManager
|
|||
map[callId] = callbacks
|
||||
}
|
||||
}
|
||||
|
||||
// We must first call setCallback on callControlScope before using it
|
||||
callbacks.onCallControlCallbackSet()
|
||||
currentlyFollowedCalls += 1
|
||||
Log.i("$TAG Call added to Telecom's CallsManager")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (e: CallException) {
|
||||
Log.e("$TAG Failed to add call to Telecom's CallsManager: $e")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import org.linphone.core.tools.Log
|
|||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class AAScreen(context: CarContext) : Screen(context) {
|
||||
class AndroidAutoScreen(context: CarContext) : Screen(context) {
|
||||
companion object {
|
||||
private const val TAG = "[Android Auto Screen]"
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ import androidx.car.app.validation.HostValidator
|
|||
import org.linphone.R
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class AAService : CarAppService() {
|
||||
class AndroidAutoService : CarAppService() {
|
||||
companion object {
|
||||
private const val TAG = "[Android Auto Service]"
|
||||
}
|
||||
|
|
@ -55,6 +55,6 @@ class AAService : CarAppService() {
|
|||
|
||||
override fun onCreateSession(): Session {
|
||||
Log.i("$TAG Creating Session object")
|
||||
return AASession()
|
||||
return AndroidAutoSession()
|
||||
}
|
||||
}
|
||||
|
|
@ -24,13 +24,13 @@ import androidx.car.app.Screen
|
|||
import androidx.car.app.Session
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class AASession : Session() {
|
||||
class AndroidAutoSession : Session() {
|
||||
companion object {
|
||||
private const val TAG = "[Android Auto Session]"
|
||||
}
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
Log.i("$TAG Creating Screen object for host with API level [${carContext.carAppApiLevel}]")
|
||||
return AAScreen(carContext)
|
||||
return AndroidAutoScreen(carContext)
|
||||
}
|
||||
}
|
||||
|
|
@ -235,7 +235,7 @@ open class GenericActivity : AppCompatActivity() {
|
|||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun enableWindowSecureMode(enable: Boolean) {
|
||||
protected fun enableWindowSecureMode(enable: Boolean) {
|
||||
val flags: Int = window.attributes.flags
|
||||
if ((enable && flags and WindowManager.LayoutParams.FLAG_SECURE != 0) ||
|
||||
(!enable && flags and WindowManager.LayoutParams.FLAG_SECURE == 0)
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@ import androidx.fragment.app.Fragment
|
|||
|
||||
@UiThread
|
||||
abstract class GenericFragment : Fragment() {
|
||||
companion object {
|
||||
private const val TAG = "[Generic Fragment]"
|
||||
}
|
||||
|
||||
protected fun observeToastEvents(viewModel: GenericViewModel) {
|
||||
viewModel.showRedToastEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
*/
|
||||
package org.linphone.ui
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.utils.Event
|
||||
|
|
@ -41,4 +43,20 @@ open class GenericViewModel : ViewModel() {
|
|||
val showFormattedRedToastEvent: MutableLiveData<Event<Pair<String, Int>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, Int>>>()
|
||||
}
|
||||
|
||||
fun showGreenToast(@StringRes message: Int, @DrawableRes icon: Int) {
|
||||
showGreenToastEvent.postValue(Event(Pair(message, icon)))
|
||||
}
|
||||
|
||||
fun showFormattedGreenToast(message: String, @DrawableRes icon: Int) {
|
||||
showFormattedGreenToastEvent.postValue(Event(Pair(message, icon)))
|
||||
}
|
||||
|
||||
fun showRedToast(@StringRes message: Int, @DrawableRes icon: Int) {
|
||||
showRedToastEvent.postValue(Event(Pair(message, icon)))
|
||||
}
|
||||
|
||||
fun showFormattedRedToast(message: String, @DrawableRes icon: Int) {
|
||||
showFormattedRedToastEvent.postValue(Event(Pair(message, icon)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ import androidx.annotation.FontRes
|
|||
import org.linphone.R
|
||||
|
||||
enum class NotoSansFont(@FontRes val fontRes: Int) {
|
||||
NotoSansLight(R.font.noto_sans_light), // 300
|
||||
// NotoSansLight(R.font.noto_sans_light), // 300
|
||||
NotoSansRegular(R.font.noto_sans_regular), // 400
|
||||
NotoSansMedium(R.font.noto_sans_medium), // 500
|
||||
NotoSansSemiBold(R.font.noto_sans_semi_bold), // 600
|
||||
// NotoSansSemiBold(R.font.noto_sans_semi_bold), // 600
|
||||
NotoSansBold(R.font.noto_sans_bold), // 700
|
||||
NotoSansExtraBold(R.font.noto_sans_extra_bold) // 800
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ package org.linphone.ui.assistant.fragment
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.telephony.TelephonyManager
|
||||
import android.view.LayoutInflater
|
||||
|
|
@ -44,6 +43,7 @@ import org.linphone.ui.assistant.model.AcceptConditionsAndPolicyDialogModel
|
|||
import org.linphone.ui.assistant.viewmodel.AccountLoginViewModel
|
||||
import org.linphone.utils.DialogUtils
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@UiThread
|
||||
class LandingFragment : GenericFragment() {
|
||||
|
|
@ -104,7 +104,7 @@ class LandingFragment : GenericFragment() {
|
|||
binding.setForgottenPasswordClickListener {
|
||||
val url = getString(R.string.web_platform_forgotten_password_url)
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
|
|
@ -207,7 +207,7 @@ class LandingFragment : GenericFragment() {
|
|||
it.consume {
|
||||
val url = getString(R.string.website_privacy_policy_url)
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
|
|
@ -221,7 +221,7 @@ class LandingFragment : GenericFragment() {
|
|||
it.consume {
|
||||
val url = getString(R.string.website_terms_and_conditions_url)
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import org.linphone.databinding.AssistantQrCodeScannerFragmentBinding
|
|||
import org.linphone.ui.GenericActivity
|
||||
import org.linphone.ui.GenericFragment
|
||||
import org.linphone.ui.assistant.viewmodel.QrCodeViewModel
|
||||
import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections
|
||||
|
||||
@UiThread
|
||||
class QrCodeScannerFragment : GenericFragment() {
|
||||
|
|
@ -85,17 +86,37 @@ class QrCodeScannerFragment : GenericFragment() {
|
|||
|
||||
viewModel.qrCodeFoundEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { isValid ->
|
||||
if (!isValid) {
|
||||
(requireActivity() as GenericActivity).showRedToast(
|
||||
getString(R.string.assistant_qr_code_invalid_toast),
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
} else {
|
||||
if (isValid) {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
// Core has restarted but something went wrong, restart video capture
|
||||
enableQrCodeVideoScanner()
|
||||
}
|
||||
}
|
||||
|
||||
coreContext.bearerAuthenticationRequestedEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
val serverUrl = pair.first
|
||||
val username = pair.second
|
||||
|
||||
Log.i(
|
||||
"$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]"
|
||||
)
|
||||
if (findNavController().currentDestination?.id == R.id.qrCodeScannerFragment) {
|
||||
val action = SingleSignOnFragmentDirections.actionGlobalSingleSignOnFragment(
|
||||
serverUrl,
|
||||
username
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCameraPermissionGranted()) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), Manifest.permission.CAMERA)) {
|
||||
Log.w("$TAG CAMERA permission wasn't granted yet, asking for it now")
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ package org.linphone.ui.assistant.fragment
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.telephony.TelephonyManager
|
||||
import android.text.Editable
|
||||
|
|
@ -49,6 +48,7 @@ import org.linphone.utils.ConfirmationDialogModel
|
|||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.DialogUtils
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@UiThread
|
||||
class RegisterFragment : GenericFragment() {
|
||||
|
|
@ -102,7 +102,7 @@ class RegisterFragment : GenericFragment() {
|
|||
binding.setOpenSubscribeWebPageClickListener {
|
||||
val url = getString(R.string.web_platform_register_email_url)
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
package org.linphone.ui.assistant.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
|
@ -31,6 +30,7 @@ import org.linphone.R
|
|||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantThirdPartySipAccountWarningFragmentBinding
|
||||
import org.linphone.ui.GenericFragment
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@UiThread
|
||||
class ThirdPartySipAccountWarningFragment : GenericFragment() {
|
||||
|
|
@ -61,7 +61,7 @@ class ThirdPartySipAccountWarningFragment : GenericFragment() {
|
|||
binding.setContactClickListener {
|
||||
val url = getString(R.string.website_contact_url)
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import kotlinx.coroutines.withContext
|
|||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.core.Account
|
||||
import org.linphone.core.AccountManagerServices
|
||||
|
|
@ -82,6 +83,8 @@ class AccountCreationViewModel
|
|||
|
||||
val showPassword = MutableLiveData<Boolean>()
|
||||
|
||||
val lockUsernameAndPassword = MutableLiveData<Boolean>()
|
||||
|
||||
val createEnabled = MediatorLiveData<Boolean>()
|
||||
|
||||
val pushNotificationsAvailable = MutableLiveData<Boolean>()
|
||||
|
|
@ -165,14 +168,7 @@ class AccountCreationViewModel
|
|||
operationInProgress.postValue(false)
|
||||
|
||||
if (!errorMessage.isNullOrEmpty()) {
|
||||
showFormattedRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
errorMessage,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showFormattedRedToast(errorMessage, R.drawable.warning_circle)
|
||||
}
|
||||
|
||||
for (parameter in parameterErrors?.keys.orEmpty()) {
|
||||
|
|
@ -199,12 +195,11 @@ class AccountCreationViewModel
|
|||
if (account != null) {
|
||||
coreContext.core.removeAccount(account)
|
||||
}
|
||||
createEnabled.postValue(true)
|
||||
}
|
||||
else -> {
|
||||
createEnabled.postValue(true)
|
||||
}
|
||||
}
|
||||
createEnabled.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,6 +253,7 @@ class AccountCreationViewModel
|
|||
|
||||
init {
|
||||
operationInProgress.value = false
|
||||
lockUsernameAndPassword.value = false
|
||||
|
||||
coreContext.postOnCoreThread { core ->
|
||||
pushNotificationsAvailable.postValue(LinphoneUtils.arePushNotificationsAvailable(core))
|
||||
|
|
@ -446,9 +442,9 @@ class AccountCreationViewModel
|
|||
operationInProgress.postValue(true)
|
||||
createEnabled.postValue(false)
|
||||
|
||||
val usernameValue = username.value
|
||||
val passwordValue = password.value
|
||||
if (usernameValue.isNullOrEmpty() || passwordValue.isNullOrEmpty()) {
|
||||
val usernameValue = username.value.orEmpty().trim()
|
||||
val passwordValue = password.value.orEmpty().trim()
|
||||
if (usernameValue.isEmpty() || passwordValue.isEmpty()) {
|
||||
Log.e("$TAG Either username [$usernameValue] or password is null or empty!")
|
||||
return
|
||||
}
|
||||
|
|
@ -469,15 +465,16 @@ class AccountCreationViewModel
|
|||
|
||||
@WorkerThread
|
||||
private fun storeAccountInCore(identity: String) {
|
||||
val passwordValue = password.value
|
||||
|
||||
val core = coreContext.core
|
||||
core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||
|
||||
val sipIdentity = Factory.instance().createAddress(identity)
|
||||
if (sipIdentity == null) {
|
||||
Log.e("$TAG Failed to create address from SIP Identity [$identity]!")
|
||||
return
|
||||
}
|
||||
|
||||
val passwordValue = password.value
|
||||
// We need to have an AuthInfo for newly created account to authorize phone number linking request
|
||||
val authInfo = Factory.instance().createAuthInfo(
|
||||
sipIdentity.username.orEmpty(),
|
||||
|
|
@ -506,6 +503,8 @@ class AccountCreationViewModel
|
|||
|
||||
accountCreatedAuthInfo = authInfo
|
||||
accountCreated = account
|
||||
|
||||
lockUsernameAndPassword.postValue(true)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
|
|||
|
|
@ -172,17 +172,12 @@ open class AccountLoginViewModel
|
|||
"sip:$userInput@$defaultDomain"
|
||||
}
|
||||
}
|
||||
Log.i("$TAG Computed identity is [$identity] from user input [$userInput]")
|
||||
|
||||
val identityAddress = Factory.instance().createAddress(identity)
|
||||
if (identityAddress == null) {
|
||||
Log.e("$TAG Can't parse [$identity] as Address!")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.assistant_login_cant_parse_address_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.assistant_login_cant_parse_address_toast, R.drawable.warning_circle)
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
|
|
@ -191,14 +186,7 @@ open class AccountLoginViewModel
|
|||
Log.e(
|
||||
"$TAG Address [${identityAddress.asStringUriOnly()}] doesn't contains an username!"
|
||||
)
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.assistant_login_address_without_username_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.assistant_login_address_without_username_toast, R.drawable.warning_circle)
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import org.linphone.core.CoreListenerStub
|
|||
import org.linphone.core.tools.Log
|
||||
import org.linphone.ui.GenericViewModel
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.R
|
||||
|
||||
class QrCodeViewModel
|
||||
@UiThread
|
||||
|
|
@ -40,12 +41,18 @@ class QrCodeViewModel
|
|||
|
||||
val qrCodeFoundEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
@WorkerThread
|
||||
override fun onConfiguringStatus(core: Core, status: ConfiguringState, message: String?) {
|
||||
Log.i("$TAG Configuring state is [$status]")
|
||||
if (status == ConfiguringState.Successful) {
|
||||
qrCodeFoundEvent.postValue(Event(true))
|
||||
} else if (status == ConfiguringState.Failed) {
|
||||
Log.e("$TAG Failure applying remote provisioning: $message")
|
||||
showRedToast(R.string.remote_provisioning_config_failed_toast, R.drawable.warning_circle)
|
||||
onErrorEvent.postValue(Event(true))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,16 +60,20 @@ class QrCodeViewModel
|
|||
override fun onQrcodeFound(core: Core, result: String?) {
|
||||
Log.i("$TAG QR Code found: [$result]")
|
||||
if (result == null) {
|
||||
qrCodeFoundEvent.postValue(Event(false))
|
||||
showRedToast(R.string.assistant_qr_code_invalid_toast, R.drawable.warning_circle)
|
||||
} else {
|
||||
val isValidUrl = Patterns.WEB_URL.matcher(result).matches()
|
||||
if (!isValidUrl) {
|
||||
Log.e("$TAG The content of the QR Code doesn't seem to be a valid web URL")
|
||||
qrCodeFoundEvent.postValue(Event(false))
|
||||
showRedToast(R.string.assistant_qr_code_invalid_toast, R.drawable.warning_circle)
|
||||
} else {
|
||||
Log.i(
|
||||
"$TAG QR code URL set, restarting the Core to apply configuration changes"
|
||||
)
|
||||
core.nativePreviewWindowId = null
|
||||
core.isVideoPreviewEnabled = false
|
||||
core.isQrcodeVideoPreviewEnabled = false
|
||||
|
||||
core.provisioningUri = result
|
||||
coreContext.core.stop()
|
||||
Log.i("$TAG Core has been stopped, restarting it")
|
||||
|
|
|
|||
|
|
@ -181,32 +181,25 @@ class ThirdPartySipAccountLoginViewModel
|
|||
|
||||
// Allow to enter SIP identity instead of simply username
|
||||
// in case identity domain doesn't match proxy domain
|
||||
val user = username.value.orEmpty().trim()
|
||||
val userId = authId.value.orEmpty().trim()
|
||||
val identity = if (user.startsWith("sip:")) {
|
||||
if (user.contains("@")) {
|
||||
user
|
||||
} else {
|
||||
"$user@$domain"
|
||||
}
|
||||
} else {
|
||||
if (user.contains("@")) {
|
||||
"sip:$user"
|
||||
} else {
|
||||
"sip:$user@$domain"
|
||||
}
|
||||
var user = username.value.orEmpty().trim()
|
||||
if (user.startsWith("sip:")) {
|
||||
user = user.substring("sip:".length)
|
||||
} else if (user.startsWith("sips:")) {
|
||||
user = user.substring("sips:".length)
|
||||
}
|
||||
if (user.contains("@")) {
|
||||
user = user.split("@")[0]
|
||||
}
|
||||
|
||||
val userId = authId.value.orEmpty().trim()
|
||||
|
||||
Log.i("$TAG Parsed username is [$user], user ID [$userId] and domain [$domain]")
|
||||
|
||||
val identity = "sip:$user@$domain"
|
||||
val identityAddress = Factory.instance().createAddress(identity)
|
||||
if (identityAddress == null) {
|
||||
Log.e("$TAG Can't parse [$identity] as Address!")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.assistant_login_cant_parse_address_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.assistant_login_cant_parse_address_toast, R.drawable.warning_circle)
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +209,7 @@ class ThirdPartySipAccountLoginViewModel
|
|||
password.value.orEmpty().trim(),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
domainValue
|
||||
)
|
||||
core.addAuthInfo(newlyCreatedAuthInfo)
|
||||
|
||||
|
|
|
|||
|
|
@ -125,10 +125,13 @@ class ActiveConferenceCallFragment : GenericCallFragment() {
|
|||
callViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CurrentCallViewModel::class.java]
|
||||
}
|
||||
observeToastEvents(callViewModel)
|
||||
observeToastEvents(callViewModel.conferenceModel)
|
||||
|
||||
callsViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CallsViewModel::class.java]
|
||||
}
|
||||
observeToastEvents(callsViewModel)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = callViewModel
|
||||
|
|
@ -224,17 +227,12 @@ class ActiveConferenceCallFragment : GenericCallFragment() {
|
|||
}
|
||||
|
||||
callViewModel.conferenceModel.goToConversationEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
it.consume { conversationId ->
|
||||
if (findNavController().currentDestination?.id == R.id.activeConferenceCallFragment) {
|
||||
val localSipUri = pair.first
|
||||
val remoteSipUri = pair.second
|
||||
Log.i(
|
||||
"$TAG Display conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
|
||||
)
|
||||
Log.i("$TAG Display conversation with conversation ID [$conversationId]")
|
||||
val action =
|
||||
ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToInCallConversationFragment(
|
||||
localSipUri,
|
||||
remoteSipUri
|
||||
conversationId
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,12 @@ import android.view.ViewGroup
|
|||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.PopupWindow
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.Participant
|
||||
import org.linphone.core.tools.Log
|
||||
|
|
@ -129,6 +131,32 @@ class ConferenceParticipantsListFragment : GenericCallFragment() {
|
|||
showKickParticipantDialog(displayName, participant)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isSendingVideo.observe(viewLifecycleOwner) { sending ->
|
||||
coreContext.postOnCoreThread { core ->
|
||||
core.nativePreviewWindowId = if (sending) {
|
||||
Log.i("$TAG We are sending video, setting capture preview surface")
|
||||
binding.localPreviewVideoSurface
|
||||
} else {
|
||||
Log.i("$TAG We are not sending video, clearing capture preview surface")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
(binding.root as? ViewGroup)?.doOnLayout {
|
||||
setupVideoPreview(binding.localPreviewVideoSurface)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
cleanVideoPreview(binding.localPreviewVideoSurface)
|
||||
}
|
||||
|
||||
private fun showKickParticipantDialog(displayName: String, participant: Participant) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import android.widget.GridLayout
|
|||
import androidx.annotation.UiThread
|
||||
import androidx.core.view.children
|
||||
import org.linphone.core.tools.Log
|
||||
import androidx.core.view.isEmpty
|
||||
|
||||
@UiThread
|
||||
class GridBoxLayout : GridLayout {
|
||||
|
|
@ -58,7 +59,7 @@ class GridBoxLayout : GridLayout {
|
|||
|
||||
@SuppressLint("DrawAllocation")
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
if (childCount == 0 || (!changed && previousChildCount == childCount)) {
|
||||
if (isEmpty() || (!changed && previousChildCount == childCount)) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
// To prevent display issue the first time conference is locally paused
|
||||
children.forEach { child ->
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import org.linphone.ui.call.conference.model.ConferenceParticipantModel
|
|||
import org.linphone.ui.call.conference.view.GridBoxLayout
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ConferenceViewModel
|
||||
@UiThread
|
||||
|
|
@ -91,8 +92,8 @@ class ConferenceViewModel
|
|||
MutableLiveData<Event<Pair<String, Participant>>>()
|
||||
}
|
||||
|
||||
val goToConversationEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String>>>()
|
||||
val goToConversationEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private lateinit var conference: Conference
|
||||
|
|
@ -134,22 +135,36 @@ class ConferenceViewModel
|
|||
@WorkerThread
|
||||
override fun onActiveSpeakerParticipantDevice(
|
||||
conference: Conference,
|
||||
participantDevice: ParticipantDevice
|
||||
participantDevice: ParticipantDevice?
|
||||
) {
|
||||
activeSpeaker.value?.isActiveSpeaker?.postValue(false)
|
||||
|
||||
val found = participantDevices.value.orEmpty().find {
|
||||
it.device.address.equal(participantDevice.address)
|
||||
}
|
||||
if (found != null) {
|
||||
Log.i("$TAG Newly active speaker participant is [${found.name}]")
|
||||
found.isActiveSpeaker.postValue(true)
|
||||
activeSpeaker.postValue(found!!)
|
||||
if (participantDevice != null) {
|
||||
val found = participantDevices.value.orEmpty().find {
|
||||
it.device.address.equal(participantDevice.address)
|
||||
}
|
||||
if (found != null) {
|
||||
Log.i("$TAG Newly active speaker participant is [${found.name}]")
|
||||
found.isActiveSpeaker.postValue(true)
|
||||
activeSpeaker.postValue(found!!)
|
||||
} else {
|
||||
Log.i("$TAG Failed to find actively speaking participant...")
|
||||
val model = ConferenceParticipantDeviceModel(participantDevice)
|
||||
model.isActiveSpeaker.postValue(true)
|
||||
activeSpeaker.postValue(model)
|
||||
}
|
||||
} else {
|
||||
Log.i("$TAG Failed to find actively speaking participant...")
|
||||
val model = ConferenceParticipantDeviceModel(participantDevice)
|
||||
model.isActiveSpeaker.postValue(true)
|
||||
activeSpeaker.postValue(model)
|
||||
Log.w("$TAG Notified active speaker participant device is null, using first one that's not us")
|
||||
val firstNotUs = participantDevices.value.orEmpty().find {
|
||||
it.isMe == false
|
||||
}
|
||||
if (firstNotUs != null) {
|
||||
Log.i("$TAG Newly active speaker participant is [${firstNotUs.name}]")
|
||||
firstNotUs.isActiveSpeaker.postValue(true)
|
||||
activeSpeaker.postValue(firstNotUs!!)
|
||||
} else {
|
||||
Log.i("$TAG No participant device that's not us found, expected if we're alone")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -355,14 +370,7 @@ class ConferenceViewModel
|
|||
Log.i("$TAG Navigating to conference's conversation")
|
||||
val chatRoom = conference.chatRoom
|
||||
if (chatRoom != null) {
|
||||
goToConversationEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
} else {
|
||||
Log.e(
|
||||
"$TAG No chat room available for current conference [${conference.conferenceAddress?.asStringUriOnly()}]"
|
||||
|
|
@ -396,14 +404,7 @@ class ConferenceViewModel
|
|||
Log.e(
|
||||
"$TAG Failed to parse SIP URI [$uri] into address, can't add it to the conference!"
|
||||
)
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conference_failed_to_add_participant_invalid_address_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conference_failed_to_add_participant_invalid_address_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
val addressesArray = arrayOfNulls<Address>(addresses.size)
|
||||
|
|
@ -796,14 +797,7 @@ class ConferenceViewModel
|
|||
"$TAG Too many participant devices for grid layout, switching to active speaker layout"
|
||||
)
|
||||
setNewLayout(ACTIVE_SPEAKER_LAYOUT)
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conference_too_many_participants_for_mosaic_layout_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conference_too_many_participants_for_mosaic_layout_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,10 +154,12 @@ class ActiveCallFragment : GenericCallFragment() {
|
|||
callViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CurrentCallViewModel::class.java]
|
||||
}
|
||||
observeToastEvents(callViewModel)
|
||||
|
||||
callsViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CallsViewModel::class.java]
|
||||
}
|
||||
observeToastEvents(callsViewModel)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = callViewModel
|
||||
|
|
@ -369,17 +371,14 @@ class ActiveCallFragment : GenericCallFragment() {
|
|||
}
|
||||
|
||||
callViewModel.goToConversationEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
it.consume { conversationId ->
|
||||
if (findNavController().currentDestination?.id == R.id.activeCallFragment) {
|
||||
val localSipUri = pair.first
|
||||
val remoteSipUri = pair.second
|
||||
Log.i(
|
||||
"$TAG Display conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
|
||||
"$TAG Display conversation with conversation ID [$conversationId]"
|
||||
)
|
||||
val action =
|
||||
ActiveCallFragmentDirections.actionActiveCallFragmentToInCallConversationFragment(
|
||||
localSipUri,
|
||||
remoteSipUri
|
||||
conversationId
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,17 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallsListFragmentBinding
|
||||
import org.linphone.ui.call.adapter.CallsListAdapter
|
||||
import org.linphone.ui.call.viewmodel.CallsViewModel
|
||||
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
|
||||
import org.linphone.utils.ConfirmationDialogModel
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
|
|
@ -43,6 +46,8 @@ class CallsListFragment : GenericCallFragment() {
|
|||
|
||||
private lateinit var viewModel: CallsViewModel
|
||||
|
||||
private lateinit var callViewModel: CurrentCallViewModel
|
||||
|
||||
private lateinit var adapter: CallsListAdapter
|
||||
|
||||
private var bottomSheetDialog: BottomSheetDialogFragment? = null
|
||||
|
|
@ -73,6 +78,11 @@ class CallsListFragment : GenericCallFragment() {
|
|||
binding.viewModel = viewModel
|
||||
observeToastEvents(viewModel)
|
||||
|
||||
callViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CurrentCallViewModel::class.java]
|
||||
}
|
||||
observeToastEvents(callViewModel)
|
||||
|
||||
binding.callsList.setHasFixedSize(true)
|
||||
binding.callsList.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
|
|
@ -101,6 +111,18 @@ class CallsListFragment : GenericCallFragment() {
|
|||
showMergeCallsIntoConferenceConfirmationDialog()
|
||||
}
|
||||
|
||||
callViewModel.isSendingVideo.observe(viewLifecycleOwner) { sending ->
|
||||
coreContext.postOnCoreThread { core ->
|
||||
core.nativePreviewWindowId = if (sending) {
|
||||
Log.i("$TAG We are sending video, setting capture preview surface")
|
||||
binding.localPreviewVideoSurface
|
||||
} else {
|
||||
Log.i("$TAG We are not sending video, clearing capture preview surface")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.calls.observe(viewLifecycleOwner) {
|
||||
Log.i("$TAG Calls list updated with [${it.size}] items")
|
||||
adapter.submitList(it)
|
||||
|
|
@ -113,11 +135,21 @@ class CallsListFragment : GenericCallFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
(binding.root as? ViewGroup)?.doOnLayout {
|
||||
setupVideoPreview(binding.localPreviewVideoSurface)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
bottomSheetDialog?.dismiss()
|
||||
bottomSheetDialog = null
|
||||
|
||||
cleanVideoPreview(binding.localPreviewVideoSurface)
|
||||
}
|
||||
|
||||
private fun showMergeCallsIntoConferenceConfirmationDialog() {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
package org.linphone.ui.call.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
|
@ -67,16 +66,12 @@ class EndedCallFragment : GenericCallFragment() {
|
|||
callViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CurrentCallViewModel::class.java]
|
||||
}
|
||||
observeToastEvents(callViewModel)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = callViewModel
|
||||
|
||||
Log.i("$TAG Showing ended call fragment")
|
||||
|
||||
callViewModel.callDuration.observe(viewLifecycleOwner) { duration ->
|
||||
binding.chronometer.base = SystemClock.elapsedRealtime() - (1000 * duration)
|
||||
binding.chronometer.stop() // Do not start it and make sure it is stopped
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
|
@ -84,7 +79,7 @@ class EndedCallFragment : GenericCallFragment() {
|
|||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (callViewModel.terminatedByUsed) {
|
||||
if (callViewModel.terminatedByUser) {
|
||||
Log.i(
|
||||
"$TAG Call terminated by user, waiting 1 second before finishing activity"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ class IncomingCallFragment : GenericCallFragment() {
|
|||
callViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CurrentCallViewModel::class.java]
|
||||
}
|
||||
observeToastEvents(callViewModel)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = callViewModel
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class OutgoingCallFragment : GenericCallFragment() {
|
|||
callViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CurrentCallViewModel::class.java]
|
||||
}
|
||||
observeToastEvents(callViewModel)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = callViewModel
|
||||
|
|
|
|||
|
|
@ -41,8 +41,6 @@ import org.linphone.ui.call.model.CallModel
|
|||
import org.linphone.ui.call.viewmodel.CallsViewModel
|
||||
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
|
||||
import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter
|
||||
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
|
||||
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
|
||||
import org.linphone.ui.main.history.viewmodel.StartCallViewModel
|
||||
import org.linphone.utils.ConfirmationDialogModel
|
||||
import org.linphone.ui.main.model.ConversationContactOrSuggestionModel
|
||||
|
|
@ -75,22 +73,6 @@ class TransferCallFragment : GenericCallFragment() {
|
|||
|
||||
private var numberOrAddressPickerDialog: Dialog? = null
|
||||
|
||||
private val listener = object : ContactNumberOrAddressClickListener {
|
||||
@UiThread
|
||||
override fun onClicked(model: ContactNumberOrAddressModel) {
|
||||
val address = model.address
|
||||
if (address != null) {
|
||||
coreContext.postOnCoreThread {
|
||||
// TODO FIXME: transfer call (blind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
override fun onLongPress(model: ContactNumberOrAddressModel) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
|
@ -115,10 +97,12 @@ class TransferCallFragment : GenericCallFragment() {
|
|||
callViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CurrentCallViewModel::class.java]
|
||||
}
|
||||
observeToastEvents(callViewModel)
|
||||
|
||||
callsViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CallsViewModel::class.java]
|
||||
}
|
||||
observeToastEvents(callsViewModel)
|
||||
|
||||
binding.viewModel = viewModel
|
||||
binding.callsViewModel = callsViewModel
|
||||
|
|
@ -258,7 +242,7 @@ class TransferCallFragment : GenericCallFragment() {
|
|||
|
||||
private fun showConfirmAttendedTransferDialog(callModel: CallModel) {
|
||||
val label = AppUtils.getFormattedString(
|
||||
org.linphone.R.string.call_transfer_confirm_dialog_message,
|
||||
R.string.call_transfer_confirm_dialog_message,
|
||||
callViewModel.displayedName.value.orEmpty(),
|
||||
callModel.displayName.value.orEmpty()
|
||||
)
|
||||
|
|
@ -294,7 +278,7 @@ class TransferCallFragment : GenericCallFragment() {
|
|||
|
||||
private fun showConfirmBlindTransferDialog(contactModel: ConversationContactOrSuggestionModel) {
|
||||
val label = AppUtils.getFormattedString(
|
||||
org.linphone.R.string.call_transfer_confirm_dialog_message,
|
||||
R.string.call_transfer_confirm_dialog_message,
|
||||
callViewModel.displayedName.value.orEmpty(),
|
||||
contactModel.name
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,10 +27,6 @@ import org.linphone.utils.Event
|
|||
class ZrtpAlertDialogModel
|
||||
@UiThread
|
||||
constructor(val allowTryAgain: Boolean) : GenericViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "[ZRTP Alert Dialog]"
|
||||
}
|
||||
|
||||
val tryAgainEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val hangUpEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
|
|
|||
|
|
@ -231,14 +231,7 @@ class CallsViewModel
|
|||
val conference = LinphoneUtils.createGroupCall(defaultAccount, subject)
|
||||
if (conference == null) {
|
||||
Log.e("$TAG Failed to create conference!")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conference_failed_to_merge_calls_into_conference_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conference_failed_to_merge_calls_into_conference_toast, R.drawable.warning_circle)
|
||||
} else {
|
||||
conference.addParticipants(core.calls)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ class CurrentCallViewModel
|
|||
|
||||
val qualityIcon = MutableLiveData<Int>()
|
||||
|
||||
var terminatedByUsed = false
|
||||
var terminatedByUser = false
|
||||
|
||||
val isRemoteRecordingEvent: MutableLiveData<Event<Pair<Boolean, String>>> by lazy {
|
||||
MutableLiveData<Event<Pair<Boolean, String>>>()
|
||||
|
|
@ -199,8 +199,8 @@ class CurrentCallViewModel
|
|||
|
||||
val operationInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val goToConversationEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String>>>()
|
||||
val goToConversationEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val chatRoomCreationErrorEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
|
|
@ -358,7 +358,11 @@ class CurrentCallViewModel
|
|||
videoUpdateInProgress.postValue(false)
|
||||
updateCallDuration()
|
||||
if (corePreferences.automaticallyStartCallRecording) {
|
||||
isRecording.postValue(call.params.isRecording)
|
||||
val recording = call.params.isRecording
|
||||
isRecording.postValue(recording)
|
||||
if (recording) {
|
||||
showRecordingToast()
|
||||
}
|
||||
}
|
||||
|
||||
// MediaEncryption None & SRTP won't be notified through onEncryptionChanged callback,
|
||||
|
|
@ -390,21 +394,14 @@ class CurrentCallViewModel
|
|||
val state = chatRoom.state
|
||||
if (state == ChatRoom.State.Instantiated) return
|
||||
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]")
|
||||
|
||||
if (state == ChatRoom.State.Created) {
|
||||
Log.i("$TAG Conversation [$id] successfully created")
|
||||
chatRoom.removeListener(this)
|
||||
operationInProgress.postValue(false)
|
||||
goToConversationEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("$TAG Conversation [$id] creation has failed!")
|
||||
chatRoom.removeListener(this)
|
||||
|
|
@ -612,7 +609,7 @@ class CurrentCallViewModel
|
|||
coreContext.postOnCoreThread {
|
||||
if (::currentCall.isInitialized) {
|
||||
Log.i("$TAG Terminating call [${currentCall.remoteAddress.asStringUriOnly()}]")
|
||||
terminatedByUsed = true
|
||||
terminatedByUser = true
|
||||
coreContext.terminateCall(currentCall)
|
||||
}
|
||||
}
|
||||
|
|
@ -706,7 +703,12 @@ class CurrentCallViewModel
|
|||
val routeAudioToSpeaker = isSpeakerEnabled.value != true
|
||||
|
||||
coreContext.postOnCoreThread { core ->
|
||||
var earpieceFound = false
|
||||
var speakerFound = false
|
||||
val audioDevices = core.audioDevices
|
||||
val currentDevice = currentCall.outputAudioDevice
|
||||
Log.i("$TAG Currently used output audio device is [${currentDevice?.deviceName} (${currentDevice?.type}])")
|
||||
|
||||
val list = arrayListOf<AudioDeviceModel>()
|
||||
for (device in audioDevices) {
|
||||
// Only list output audio devices
|
||||
|
|
@ -714,9 +716,11 @@ class CurrentCallViewModel
|
|||
|
||||
val name = when (device.type) {
|
||||
AudioDevice.Type.Earpiece -> {
|
||||
earpieceFound = true
|
||||
AppUtils.getString(R.string.call_audio_device_type_earpiece)
|
||||
}
|
||||
AudioDevice.Type.Speaker -> {
|
||||
speakerFound = true
|
||||
AppUtils.getString(R.string.call_audio_device_type_speaker)
|
||||
}
|
||||
AudioDevice.Type.Headset -> {
|
||||
|
|
@ -739,7 +743,6 @@ class CurrentCallViewModel
|
|||
}
|
||||
else -> device.deviceName
|
||||
}
|
||||
val currentDevice = currentCall.outputAudioDevice
|
||||
val isCurrentlyInUse = device.type == currentDevice?.type && device.deviceName == currentDevice.deviceName
|
||||
val model = AudioDeviceModel(device, name, device.type, isCurrentlyInUse, true) {
|
||||
// onSelected
|
||||
|
|
@ -765,8 +768,8 @@ class CurrentCallViewModel
|
|||
Log.i("$TAG Found audio device [${device.id}]")
|
||||
}
|
||||
|
||||
if (list.size > 2) {
|
||||
Log.i("$TAG Found more than two devices, showing list to let user choose")
|
||||
if (list.size > 2 || (list.size > 1 && (!earpieceFound || !speakerFound))) {
|
||||
Log.i("$TAG Found more than two devices (or more than 1 but no earpiece or speaker), showing list to let user choose")
|
||||
showAudioDevicesListEvent.postValue(Event(list))
|
||||
} else {
|
||||
Log.i(
|
||||
|
|
@ -855,8 +858,12 @@ class CurrentCallViewModel
|
|||
Log.i("$TAG Starting call recording")
|
||||
currentCall.startRecording()
|
||||
}
|
||||
|
||||
val recording = currentCall.params.isRecording
|
||||
isRecording.postValue(recording)
|
||||
if (recording) {
|
||||
showRecordingToast()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -921,17 +928,10 @@ class CurrentCallViewModel
|
|||
if (existingConversation != null) {
|
||||
Log.i(
|
||||
"$TAG Found existing conversation [${
|
||||
LinphoneUtils.getChatRoomId(existingConversation)
|
||||
LinphoneUtils.getConversationId(existingConversation)
|
||||
}], going to it"
|
||||
)
|
||||
goToConversationEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
existingConversation.localAddress.asStringUriOnly(),
|
||||
existingConversation.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(existingConversation)))
|
||||
} else {
|
||||
Log.i("$TAG No existing conversation was found, let's create it")
|
||||
createCurrentCallConversation(currentCall)
|
||||
|
|
@ -1052,7 +1052,7 @@ class CurrentCallViewModel
|
|||
)
|
||||
contact.value?.destroy()
|
||||
|
||||
terminatedByUsed = false
|
||||
terminatedByUser = false
|
||||
currentCall = call
|
||||
callStatsModel.update(call, call.audioStats)
|
||||
callMediaEncryptionModel.update(call)
|
||||
|
|
@ -1172,7 +1172,11 @@ class CurrentCallViewModel
|
|||
contact.postValue(model)
|
||||
displayedName.postValue(model.friend.name)
|
||||
|
||||
isRecording.postValue(call.params.isRecording)
|
||||
val recording = call.params.isRecording
|
||||
isRecording.postValue(recording)
|
||||
if (recording) {
|
||||
showRecordingToast()
|
||||
}
|
||||
|
||||
val isRemoteRecording = call.remoteParams?.isRecording == true
|
||||
if (isRemoteRecording) {
|
||||
|
|
@ -1204,6 +1208,7 @@ class CurrentCallViewModel
|
|||
|
||||
@WorkerThread
|
||||
private fun updateOutputAudioDevice(audioDevice: AudioDevice?) {
|
||||
Log.i("$TAG Output audio device updated to [${audioDevice?.deviceName} (${audioDevice?.type})]")
|
||||
isSpeakerEnabled.postValue(audioDevice?.type == AudioDevice.Type.Speaker)
|
||||
isHeadsetEnabled.postValue(
|
||||
audioDevice?.type == AudioDevice.Type.Headphones || audioDevice?.type == AudioDevice.Type.Headset
|
||||
|
|
@ -1346,44 +1351,29 @@ class CurrentCallViewModel
|
|||
|
||||
@WorkerThread
|
||||
private fun createCurrentCallConversation(call: Call) {
|
||||
val localAddress = call.callLog.localAddress
|
||||
val remoteAddress = call.remoteAddress
|
||||
val participants = arrayOf(remoteAddress)
|
||||
val core = call.core
|
||||
operationInProgress.postValue(true)
|
||||
|
||||
val params = getChatRoomParams(call) ?: return // TODO: show error to user
|
||||
val conversation = core.createChatRoom(params, localAddress, participants)
|
||||
if (conversation != null) {
|
||||
val chatRoom = core.createChatRoom(params, participants)
|
||||
if (chatRoom != null) {
|
||||
if (params.chatParams?.backend == ChatRoom.Backend.FlexisipChat) {
|
||||
if (conversation.state == ChatRoom.State.Created) {
|
||||
val id = LinphoneUtils.getChatRoomId(conversation)
|
||||
if (chatRoom.state == ChatRoom.State.Created) {
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG 1-1 conversation [$id] has been created")
|
||||
operationInProgress.postValue(false)
|
||||
goToConversationEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
conversation.localAddress.asStringUriOnly(),
|
||||
conversation.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
} else {
|
||||
Log.i("$TAG Conversation isn't in Created state yet, wait for it")
|
||||
conversation.addListener(chatRoomListener)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
}
|
||||
} else {
|
||||
val id = LinphoneUtils.getChatRoomId(conversation)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Conversation successfully created [$id]")
|
||||
operationInProgress.postValue(false)
|
||||
goToConversationEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
conversation.localAddress.asStringUriOnly(),
|
||||
conversation.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
}
|
||||
} else {
|
||||
Log.e(
|
||||
|
|
@ -1407,6 +1397,8 @@ class CurrentCallViewModel
|
|||
params.isChatEnabled = true
|
||||
params.isGroupEnabled = false
|
||||
params.subject = AppUtils.getString(R.string.conversation_one_to_one_hidden_subject)
|
||||
params.account = account
|
||||
|
||||
val chatParams = params.chatParams ?: return null
|
||||
chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
|
||||
|
|
@ -1471,4 +1463,9 @@ class CurrentCallViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
private fun showRecordingToast() {
|
||||
showGreenToast(R.string.call_is_being_recorded, R.drawable.record_fill)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package org.linphone.ui.fileviewer
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
|
|
@ -23,6 +22,7 @@ import org.linphone.ui.GenericActivity
|
|||
import org.linphone.ui.fileviewer.adapter.PdfPagesListAdapter
|
||||
import org.linphone.ui.fileviewer.viewmodel.FileViewModel
|
||||
import org.linphone.utils.FileUtils
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@UiThread
|
||||
class FileViewerActivity : GenericActivity() {
|
||||
|
|
@ -73,11 +73,19 @@ class FileViewerActivity : GenericActivity() {
|
|||
return
|
||||
}
|
||||
|
||||
val isFromEphemeralMessage = args.getBoolean("isFromEphemeralMessage", false)
|
||||
if (isFromEphemeralMessage) {
|
||||
Log.i("$TAG Displayed content is from an ephemeral chat message, force secure mode to prevent screenshots")
|
||||
// Force preventing screenshots for ephemeral messages contents
|
||||
enableWindowSecureMode(true)
|
||||
}
|
||||
|
||||
val timestamp = args.getLong("timestamp", -1)
|
||||
val preLoadedContent = args.getString("content")
|
||||
Log.i(
|
||||
"$TAG Path argument is [$path], pre loaded text content is ${if (preLoadedContent.isNullOrEmpty()) "not available" else "available, using it"}"
|
||||
)
|
||||
viewModel.isFromEphemeralMessage.value = isFromEphemeralMessage
|
||||
viewModel.loadFile(path, timestamp, preLoadedContent)
|
||||
|
||||
binding.setBackClickListener {
|
||||
|
|
@ -178,7 +186,7 @@ class FileViewerActivity : GenericActivity() {
|
|||
val filePath = FileUtils.getProperFilePath(viewModel.getFilePath())
|
||||
val copy = FileUtils.getFilePath(
|
||||
baseContext,
|
||||
Uri.parse(filePath),
|
||||
filePath.toUri(),
|
||||
overrideExisting = false,
|
||||
copyToCache = true
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package org.linphone.ui.fileviewer
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.UiThread
|
||||
|
|
@ -17,6 +16,7 @@ import java.io.File
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.FileMediaViewerActivityBinding
|
||||
|
|
@ -27,6 +27,7 @@ import org.linphone.ui.main.chat.model.FileModel
|
|||
import org.linphone.ui.main.viewmodel.SharedMainViewModel
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.FileUtils
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@UiThread
|
||||
class MediaViewerActivity : GenericActivity() {
|
||||
|
|
@ -51,6 +52,13 @@ class MediaViewerActivity : GenericActivity() {
|
|||
val model = list[position]
|
||||
viewModel.currentlyDisplayedFileName.value = model.fileName
|
||||
viewModel.currentlyDisplayedFileDateTime.value = model.dateTime
|
||||
|
||||
val isFromEphemeral = model.isFromEphemeralMessage
|
||||
viewModel.isCurrentlyDisplayedFileFromEphemeralMessage.value = isFromEphemeral
|
||||
if (!corePreferences.enableSecureMode) {
|
||||
// Force preventing screenshots for ephemeral messages contents, but allow it for others
|
||||
enableWindowSecureMode(isFromEphemeral)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -96,17 +104,24 @@ class MediaViewerActivity : GenericActivity() {
|
|||
return
|
||||
}
|
||||
|
||||
val isFromEphemeralMessage = args.getBoolean("isFromEphemeralMessage", false)
|
||||
if (isFromEphemeralMessage) {
|
||||
Log.i("$TAG Displayed content is from an ephemeral chat message, force secure mode to prevent screenshots")
|
||||
// Force preventing screenshots for ephemeral messages contents
|
||||
enableWindowSecureMode(true)
|
||||
}
|
||||
|
||||
val timestamp = args.getLong("timestamp", -1)
|
||||
val isEncrypted = args.getBoolean("isEncrypted", false)
|
||||
val originalPath = args.getString("originalPath", "")
|
||||
viewModel.initTempModel(path, timestamp, isEncrypted, originalPath)
|
||||
Log.i("$TAG Path argument is [$path], timestamp [$timestamp], encrypted [$isEncrypted] and original path [$originalPath]")
|
||||
viewModel.initTempModel(path, timestamp, isEncrypted, originalPath, isFromEphemeralMessage)
|
||||
|
||||
val localSipUri = args.getString("localSipUri").orEmpty()
|
||||
val remoteSipUri = args.getString("remoteSipUri").orEmpty()
|
||||
val conversationId = args.getString("conversationId").orEmpty()
|
||||
Log.i(
|
||||
"$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri] trying to display file [$path]"
|
||||
"$TAG Looking up for conversation with conversation ID [$conversationId] trying to display file [$path]"
|
||||
)
|
||||
viewModel.findChatRoom(null, localSipUri, remoteSipUri)
|
||||
viewModel.findChatRoom(null, conversationId)
|
||||
|
||||
viewModel.mediaList.observe(this) {
|
||||
updateMediaList(path, it)
|
||||
|
|
@ -232,7 +247,7 @@ class MediaViewerActivity : GenericActivity() {
|
|||
val filePath = FileUtils.getProperFilePath(model.path)
|
||||
val copy = FileUtils.getFilePath(
|
||||
baseContext,
|
||||
Uri.parse(filePath),
|
||||
filePath.toUri(),
|
||||
overrideExisting = true,
|
||||
copyToCache = true
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@ import android.util.AttributeSet
|
|||
import android.view.TextureView
|
||||
|
||||
class RatioTextureView : TextureView {
|
||||
companion object {
|
||||
private const val TAG = "[Ratio TextureView]"
|
||||
}
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
|
@ -38,13 +34,6 @@ class RatioTextureView : TextureView {
|
|||
defStyleAttr
|
||||
)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr,
|
||||
defStyleRes
|
||||
)
|
||||
|
||||
private var ratioWidth = 0
|
||||
private var rationHeight = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
*/
|
||||
package org.linphone.ui.fileviewer.viewmodel
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.pdf.PdfRenderer
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
|
|
@ -31,7 +30,6 @@ import java.io.BufferedReader
|
|||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import java.lang.IllegalStateException
|
||||
import java.lang.StringBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -41,6 +39,8 @@ import org.linphone.ui.GenericViewModel
|
|||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
class FileViewModel
|
||||
@UiThread
|
||||
|
|
@ -69,6 +69,8 @@ class FileViewModel
|
|||
|
||||
val dateTime = MutableLiveData<String>()
|
||||
|
||||
val isFromEphemeralMessage = MutableLiveData<Boolean>()
|
||||
|
||||
val exportPlainTextFileEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
|
@ -178,11 +180,7 @@ class FileViewModel
|
|||
Log.d(
|
||||
"$TAG Page size is ${page.width}/${page.height}, screen size is $screenWidth/$screenHeight"
|
||||
)
|
||||
val bm = Bitmap.createBitmap(
|
||||
page.width,
|
||||
page.height,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val bm = createBitmap(page.width, page.height)
|
||||
page.render(bm, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
||||
page.close()
|
||||
|
||||
|
|
@ -230,7 +228,7 @@ class FileViewModel
|
|||
|
||||
@UiThread
|
||||
fun copyFileToUri(dest: Uri) {
|
||||
val source = Uri.parse(FileUtils.getProperFilePath(getFilePath()))
|
||||
val source = FileUtils.getProperFilePath(getFilePath()).toUri()
|
||||
Log.i("$TAG Copying file URI [$source] to [$dest]")
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
|
@ -239,24 +237,10 @@ class FileViewModel
|
|||
Log.i(
|
||||
"$TAG File [$filePath] has been successfully exported to documents"
|
||||
)
|
||||
showGreenToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.file_successfully_exported_to_documents_toast,
|
||||
R.drawable.check
|
||||
)
|
||||
)
|
||||
)
|
||||
showGreenToast(R.string.file_successfully_exported_to_documents_toast, R.drawable.check)
|
||||
} else {
|
||||
Log.e("$TAG Failed to export file [$filePath] to documents!")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.export_file_to_documents_error_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.export_file_to_documents_error_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -272,24 +256,13 @@ class FileViewModel
|
|||
Log.i(
|
||||
"$TAG Text has been successfully exported to documents"
|
||||
)
|
||||
showGreenToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
showGreenToast(
|
||||
R.string.file_successfully_exported_to_documents_toast,
|
||||
R.drawable.check
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Log.e("$TAG Failed to save text to documents!")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.export_file_to_documents_error_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.export_file_to_documents_error_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -337,14 +310,7 @@ class FileViewModel
|
|||
// TODO FIXME : improve performances !
|
||||
} catch (e: Exception) {
|
||||
Log.e("$TAG Exception trying to read file [$filePath] as text: $e")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conversation_file_cant_be_opened_error_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conversation_file_cant_be_opened_error_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ class MediaListViewModel
|
|||
|
||||
val currentlyDisplayedFileDateTime = MutableLiveData<String>()
|
||||
|
||||
val isCurrentlyDisplayedFileFromEphemeralMessage = MutableLiveData<Boolean>()
|
||||
|
||||
private lateinit var temporaryModel: FileModel
|
||||
|
||||
override fun beforeNotifyingChatRoomFound(sameOne: Boolean) {
|
||||
|
|
@ -57,9 +59,9 @@ class MediaListViewModel
|
|||
}
|
||||
|
||||
@UiThread
|
||||
fun initTempModel(path: String, timestamp: Long, isEncrypted: Boolean, originalPath: String) {
|
||||
fun initTempModel(path: String, timestamp: Long, isEncrypted: Boolean, originalPath: String, isFromEphemeralMessage: Boolean) {
|
||||
val name = FileUtils.getNameFromFilePath(path)
|
||||
val model = FileModel(path, name, 0, timestamp, isEncrypted, originalPath)
|
||||
val model = FileModel(path, name, 0, timestamp, isEncrypted, originalPath, isFromEphemeralMessage)
|
||||
temporaryModel = model
|
||||
Log.i("$TAG Temporary model for file [$name] created, use it while other media for conversation are being loaded")
|
||||
mediaList.postValue(arrayListOf(model))
|
||||
|
|
@ -68,7 +70,7 @@ class MediaListViewModel
|
|||
@WorkerThread
|
||||
private fun loadMediaList() {
|
||||
val list = arrayListOf<FileModel>()
|
||||
val chatRoomId = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val chatRoomId = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Loading media contents for conversation [$chatRoomId]")
|
||||
|
||||
val media = chatRoom.mediaContents
|
||||
|
|
@ -90,20 +92,37 @@ class MediaListViewModel
|
|||
Log.d(
|
||||
"$TAG [VFS] Content is encrypted, requesting plain file path for file [${mediaContent.filePath}]"
|
||||
)
|
||||
mediaContent.exportPlainFile()
|
||||
val exportedPath = mediaContent.exportPlainFile()
|
||||
Log.i("$TAG Media original path is [$originalPath], newly exported plain file path is [$exportedPath]")
|
||||
exportedPath
|
||||
} else {
|
||||
originalPath
|
||||
}
|
||||
|
||||
val name = mediaContent.name.orEmpty()
|
||||
val size = mediaContent.size.toLong()
|
||||
val timestamp = mediaContent.creationTimestamp
|
||||
if (path.isNotEmpty() && name.isNotEmpty()) {
|
||||
val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath)
|
||||
val messageId = mediaContent.relatedChatMessageId
|
||||
val ephemeral = if (messageId != null) {
|
||||
val chatMessage = chatRoom.findMessage(messageId)
|
||||
if (chatMessage == null) {
|
||||
Log.w("$TAG Failed to find message using ID [$messageId] related to this content, can't get real info about being related to ephemeral message")
|
||||
}
|
||||
chatMessage?.isEphemeral ?: chatRoom.isEphemeralEnabled
|
||||
} else {
|
||||
Log.e("$TAG No chat message ID related to this content, can't get real info about being related to ephemeral message")
|
||||
chatRoom.isEphemeralEnabled
|
||||
}
|
||||
|
||||
val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath, ephemeral)
|
||||
list.add(model)
|
||||
} else {
|
||||
Log.w("$TAG Skipping content because either name [$name] or path [$path] is empty")
|
||||
}
|
||||
|
||||
if (tempFilePath.isNotEmpty() && !tempFileModelFound) {
|
||||
if (path == tempFilePath) {
|
||||
if (path == tempFilePath || (isEncrypted && originalPath == temporaryModel.originalPath)) {
|
||||
tempFileModelFound = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import org.linphone.ui.GenericViewModel
|
|||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
import org.linphone.R
|
||||
|
||||
class MediaViewModel
|
||||
@UiThread
|
||||
|
|
@ -163,33 +164,40 @@ class MediaViewModel
|
|||
private fun initMediaPlayer() {
|
||||
isMediaPlaying.value = false
|
||||
|
||||
mediaPlayer = MediaPlayer().apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).setUsage(
|
||||
AudioAttributes.USAGE_MEDIA
|
||||
).build()
|
||||
)
|
||||
setDataSource(filePath)
|
||||
setOnCompletionListener {
|
||||
Log.i("$TAG Media player reached the end of file")
|
||||
isMediaPlaying.postValue(false)
|
||||
position.postValue(0)
|
||||
stopUpdatePlaybackPosition()
|
||||
try {
|
||||
mediaPlayer = MediaPlayer().apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(
|
||||
AudioAttributes.USAGE_MEDIA
|
||||
).build()
|
||||
)
|
||||
setDataSource(filePath)
|
||||
setOnCompletionListener {
|
||||
Log.i("$TAG Media player reached the end of file")
|
||||
isMediaPlaying.postValue(false)
|
||||
position.postValue(0)
|
||||
stopUpdatePlaybackPosition()
|
||||
|
||||
// Leave full screen when playback is done
|
||||
fullScreenMode.postValue(false)
|
||||
changeFullScreenModeEvent.postValue(Event(false))
|
||||
}
|
||||
setOnVideoSizeChangedListener { mediaPlayer, width, height ->
|
||||
videoSizeChangedEvent.postValue(Event(Pair(width, height)))
|
||||
}
|
||||
try {
|
||||
prepare()
|
||||
} catch (e: Exception) {
|
||||
fullScreenMode.postValue(false)
|
||||
changeFullScreenModeEvent.postValue(Event(false))
|
||||
Log.e("$TAG Failed to prepare video file: $e")
|
||||
// Leave full screen when playback is done
|
||||
fullScreenMode.postValue(false)
|
||||
changeFullScreenModeEvent.postValue(Event(false))
|
||||
}
|
||||
setOnVideoSizeChangedListener { mediaPlayer, width, height ->
|
||||
videoSizeChangedEvent.postValue(Event(Pair(width, height)))
|
||||
}
|
||||
try {
|
||||
prepare()
|
||||
} catch (e: Exception) {
|
||||
fullScreenMode.postValue(false)
|
||||
changeFullScreenModeEvent.postValue(Event(false))
|
||||
Log.e("$TAG Failed to prepare video file: $e")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("$TAG Failed to initialize media player for file [$filePath]: $e")
|
||||
showRedToast(R.string.media_player_generic_error_toast, R.drawable.warning_circle)
|
||||
return
|
||||
}
|
||||
|
||||
val durationInMillis = mediaPlayer.duration
|
||||
|
|
|
|||
|
|
@ -52,9 +52,11 @@ import androidx.navigation.NavOptions
|
|||
import androidx.navigation.findNavController
|
||||
import kotlin.math.max
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
|
|
@ -86,6 +88,9 @@ class MainActivity : GenericActivity() {
|
|||
private const val HISTORY_FRAGMENT_ID = 2
|
||||
private const val CHAT_FRAGMENT_ID = 3
|
||||
private const val MEETINGS_FRAGMENT_ID = 4
|
||||
|
||||
const val ARGUMENTS_CHAT = "Chat"
|
||||
const val ARGUMENTS_CONVERSATION_ID = "ConversationId"
|
||||
}
|
||||
|
||||
private lateinit var binding: MainActivityBinding
|
||||
|
|
@ -279,6 +284,12 @@ class MainActivity : GenericActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
coreContext.clearAuthenticationRequestDialogEvent.observe(this) {
|
||||
it.consume {
|
||||
currentlyDisplayedAuthDialog?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
coreContext.showGreenToastEvent.observe(this) {
|
||||
it.consume { pair ->
|
||||
val message = getString(pair.first)
|
||||
|
|
@ -310,6 +321,19 @@ class MainActivity : GenericActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
coreContext.filesToExportToNativeMediaGalleryEvent.observe(this) {
|
||||
it.consume { files ->
|
||||
Log.i("$TAG Found [${files.size}] files to export to native media gallery")
|
||||
for (file in files) {
|
||||
exportFileToNativeMediaGallery(file)
|
||||
}
|
||||
|
||||
coreContext.postOnCoreThread {
|
||||
coreContext.clearFilesToExportToNativeGallery()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CarConnection(this).type.observe(this) {
|
||||
val asString = when (it) {
|
||||
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "NOT CONNECTED"
|
||||
|
|
@ -514,14 +538,9 @@ class MainActivity : GenericActivity() {
|
|||
|
||||
private fun handleLocusOrShortcut(id: String) {
|
||||
Log.i("$TAG Found locus ID [$id]")
|
||||
val pair = LinphoneUtils.getLocalAndPeerSipUrisFromChatRoomId(id)
|
||||
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"
|
||||
)
|
||||
sharedViewModel.showConversationEvent.value = Event(pair)
|
||||
if (id.isNotEmpty()) {
|
||||
Log.i("$TAG Navigating to conversation with ID [$id], computed from shortcut ID")
|
||||
sharedViewModel.showConversationEvent.value = Event(id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -547,22 +566,18 @@ class MainActivity : GenericActivity() {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if (intent.hasExtra("Chat")) {
|
||||
if (intent.hasExtra(ARGUMENTS_CHAT)) {
|
||||
Log.i("$TAG Intent has [Chat] extra")
|
||||
coreContext.postOnMainThread {
|
||||
try {
|
||||
Log.i("$TAG Trying to go to Conversations fragment")
|
||||
val args = intent.extras
|
||||
val localSipUri = args?.getString("LocalSipUri", "")
|
||||
val remoteSipUri = args?.getString("RemoteSipUri", "")
|
||||
if (remoteSipUri.isNullOrEmpty() || localSipUri.isNullOrEmpty()) {
|
||||
Log.w("$TAG Found [Chat] extra but no local and/or remote SIP URI!")
|
||||
val conversationId = args?.getString(ARGUMENTS_CONVERSATION_ID, "")
|
||||
if (conversationId.isNullOrEmpty()) {
|
||||
Log.w("$TAG Found [Chat] extra but no conversation ID!")
|
||||
} else {
|
||||
Log.i(
|
||||
"$TAG Found [Chat] extra with local [$localSipUri] and peer [$remoteSipUri] addresses"
|
||||
)
|
||||
val pair = Pair(localSipUri, remoteSipUri)
|
||||
sharedViewModel.showConversationEvent.value = Event(pair)
|
||||
Log.i("$TAG Found [Chat] extra with conversation ID [$conversationId]")
|
||||
sharedViewModel.showConversationEvent.value = Event(conversationId)
|
||||
}
|
||||
args?.clear()
|
||||
|
||||
|
|
@ -665,25 +680,23 @@ class MainActivity : GenericActivity() {
|
|||
Log.i(
|
||||
"$TAG App is already started and in debug fragment, navigating to conversations list"
|
||||
)
|
||||
val pair = parseShortcutIfAny(intent)
|
||||
if (pair != null) {
|
||||
val conversationId = parseShortcutIfAny(intent)
|
||||
if (conversationId != null) {
|
||||
Log.i(
|
||||
"$TAG Navigating from debug to conversation with local [${pair.first}] and peer [${pair.second}] addresses, computed from shortcut ID"
|
||||
"$TAG Navigating from debug to conversation with ID [$conversationId], computed from shortcut ID"
|
||||
)
|
||||
sharedViewModel.showConversationEvent.value = Event(pair)
|
||||
sharedViewModel.showConversationEvent.value = Event(conversationId)
|
||||
}
|
||||
|
||||
val action = DebugFragmentDirections.actionDebugFragmentToConversationsListFragment()
|
||||
findNavController().navigate(action)
|
||||
} else {
|
||||
val pair = parseShortcutIfAny(intent)
|
||||
if (pair != null) {
|
||||
val localSipUri = pair.first
|
||||
val remoteSipUri = pair.second
|
||||
val conversationId = parseShortcutIfAny(intent)
|
||||
if (conversationId != null) {
|
||||
Log.i(
|
||||
"$TAG Navigating to conversation with local [$localSipUri] and peer [$remoteSipUri] addresses, computed from shortcut ID"
|
||||
"$TAG Navigating to conversation with conversation ID [$conversationId] addresses, computed from shortcut ID"
|
||||
)
|
||||
sharedViewModel.showConversationEvent.value = Event(pair)
|
||||
sharedViewModel.showConversationEvent.value = Event(conversationId)
|
||||
}
|
||||
|
||||
if (findNavController().currentDestination?.id == R.id.conversationsListFragment) {
|
||||
|
|
@ -698,11 +711,11 @@ class MainActivity : GenericActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun parseShortcutIfAny(intent: Intent): Pair<String, String>? {
|
||||
private fun parseShortcutIfAny(intent: Intent): String? {
|
||||
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
|
||||
if (shortcutId != null) {
|
||||
Log.i("$TAG Found shortcut ID [$shortcutId]")
|
||||
return LinphoneUtils.getLocalAndPeerSipUrisFromChatRoomId(shortcutId)
|
||||
return shortcutId
|
||||
} else {
|
||||
Log.i("$TAG No shortcut ID was found")
|
||||
}
|
||||
|
|
@ -785,4 +798,18 @@ class MainActivity : GenericActivity() {
|
|||
dialog.show()
|
||||
currentlyDisplayedAuthDialog = dialog
|
||||
}
|
||||
|
||||
private fun exportFileToNativeMediaGallery(filePath: String) {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
Log.i("$TAG Export file [$filePath] to Android's MediaStore")
|
||||
val mediaStorePath = FileUtils.addContentToMediaStore(filePath)
|
||||
if (mediaStorePath.isNotEmpty()) {
|
||||
Log.i("$TAG File [$filePath] has been successfully exported to MediaStore")
|
||||
} else {
|
||||
Log.e("$TAG Failed to export file [$filePath] to MediaStore!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class ConversationsContactsAndSuggestionsListAdapter :
|
|||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val model = getItem(position)
|
||||
return if (model.localAddress != null) {
|
||||
return if (model.conversationId.isNotEmpty()) {
|
||||
CONVERSATION_TYPE
|
||||
} else if (model.friend != null) {
|
||||
if (model.starred) {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import org.linphone.core.tools.Log
|
|||
import org.linphone.databinding.ChatBubbleIncomingBinding
|
||||
import org.linphone.databinding.ChatBubbleOutgoingBinding
|
||||
import org.linphone.databinding.ChatConversationEventBinding
|
||||
import org.linphone.databinding.ChatConversationSecuredFirstEventBinding
|
||||
import org.linphone.databinding.ChatConversationE2eEncryptedFirstEventBinding
|
||||
import org.linphone.ui.main.chat.model.EventLogModel
|
||||
import org.linphone.ui.main.chat.model.EventModel
|
||||
import org.linphone.ui.main.chat.model.MessageModel
|
||||
|
|
@ -70,13 +70,19 @@ class ConversationEventAdapter :
|
|||
MutableLiveData<Event<MessageModel>>()
|
||||
}
|
||||
|
||||
private var isConversationSecured: Boolean = false
|
||||
|
||||
fun setIsConversationSecured(secured: Boolean) {
|
||||
isConversationSecured = secured
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
// We only want to display it at top
|
||||
return position == 0
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val binding = ChatConversationSecuredFirstEventBinding.inflate(LayoutInflater.from(context))
|
||||
val binding = ChatConversationE2eEncryptedFirstEventBinding.inflate(LayoutInflater.from(context))
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import androidx.annotation.UiThread
|
|||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import org.linphone.databinding.ChatLongPressMenuBinding
|
||||
import org.linphone.databinding.ChatConversationLongPressMenuBinding
|
||||
|
||||
@UiThread
|
||||
class ConversationDialogFragment(
|
||||
|
|
@ -71,7 +71,7 @@ class ConversationDialogFragment(
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val view = ChatLongPressMenuBinding.inflate(layoutInflater)
|
||||
val view = ChatConversationLongPressMenuBinding.inflate(layoutInflater)
|
||||
view.isMuted = isMuted
|
||||
view.isGroup = isGroup
|
||||
view.isReadOnly = isReadOnly
|
||||
|
|
|
|||
|
|
@ -91,13 +91,10 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() {
|
|||
binding.viewModel = viewModel
|
||||
observeToastEvents(viewModel)
|
||||
|
||||
val localSipUri = args.localSipUri
|
||||
val remoteSipUri = args.remoteSipUri
|
||||
Log.i(
|
||||
"$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
|
||||
)
|
||||
val conversationId = args.conversationId
|
||||
Log.i("$TAG Looking up for conversation with conversation ID [$conversationId]")
|
||||
val chatRoom = sharedViewModel.displayedChatRoom
|
||||
viewModel.findChatRoom(chatRoom, localSipUri, remoteSipUri)
|
||||
viewModel.findChatRoom(chatRoom, conversationId)
|
||||
|
||||
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
|
||||
binding.documentsList.addItemDecoration(headerItemDecoration)
|
||||
|
|
@ -143,12 +140,12 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() {
|
|||
|
||||
val bundle = Bundle()
|
||||
bundle.apply {
|
||||
putString("localSipUri", viewModel.localSipUri)
|
||||
putString("remoteSipUri", viewModel.remoteSipUri)
|
||||
putString("conversationId", viewModel.conversationId)
|
||||
putString("path", path)
|
||||
putBoolean("isEncrypted", fileModel.isEncrypted)
|
||||
putLong("timestamp", fileModel.fileCreationTimestamp)
|
||||
putString("originalPath", fileModel.originalPath)
|
||||
putBoolean("isFromEphemeralMessage", fileModel.isFromEphemeralMessage)
|
||||
putBoolean("isMedia", false)
|
||||
}
|
||||
when (FileUtils.getMimeType(mime)) {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
|
|||
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
|
||||
import org.linphone.ui.main.fragment.SlidingPaneChildFragment
|
||||
import org.linphone.utils.DialogUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.RecyclerViewHeaderDecoration
|
||||
|
||||
@UiThread
|
||||
|
|
@ -66,11 +65,7 @@ class ConversationForwardMessageFragment : SlidingPaneChildFragment() {
|
|||
override fun goBack(): Boolean {
|
||||
sharedViewModel.messageToForwardEvent.value?.consume {
|
||||
Log.w("$TAG Cancelling message forward")
|
||||
viewModel.showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(R.string.conversation_message_forward_cancelled_toast, R.drawable.forward)
|
||||
)
|
||||
)
|
||||
viewModel.showRedToast(R.string.conversation_message_forward_cancelled_toast, R.drawable.forward)
|
||||
}
|
||||
|
||||
return findNavController().popBackStack()
|
||||
|
|
@ -125,17 +120,11 @@ class ConversationForwardMessageFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
|
||||
viewModel.chatRoomCreatedEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
Log.i(
|
||||
"$TAG Navigating to conversation [${pair.second}] with local address [${pair.first}]"
|
||||
)
|
||||
|
||||
it.consume { conversationId ->
|
||||
Log.i("$TAG Navigating to conversation [$conversationId]")
|
||||
if (findNavController().currentDestination?.id == R.id.conversationForwardMessageFragment) {
|
||||
val localSipUri = pair.first
|
||||
val remoteSipUri = pair.second
|
||||
val action = ConversationForwardMessageFragmentDirections.actionConversationForwardMessageFragmentToConversationFragment(
|
||||
localSipUri,
|
||||
remoteSipUri
|
||||
conversationId
|
||||
)
|
||||
disableConsumingEventOnPause = true
|
||||
findNavController().navigate(action)
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ import org.linphone.utils.addCharacterAtPosition
|
|||
import org.linphone.utils.hideKeyboard
|
||||
import org.linphone.utils.setKeyboardInsetListener
|
||||
import org.linphone.utils.showKeyboard
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@UiThread
|
||||
open class ConversationFragment : SlidingPaneChildFragment() {
|
||||
|
|
@ -212,8 +213,11 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
.viewTreeObserver
|
||||
.removeOnGlobalLayoutListener(this)
|
||||
|
||||
if (::scrollListener.isInitialized) {
|
||||
binding.eventsList.addOnScrollListener(scrollListener)
|
||||
binding.root.setKeyboardInsetListener { keyboardVisible ->
|
||||
sendMessageViewModel.isKeyboardOpen.value = keyboardVisible
|
||||
if (keyboardVisible) {
|
||||
sendMessageViewModel.isEmojiPickerOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
val unreadCount = viewModel.unreadMessagesCount.value ?: 0
|
||||
|
|
@ -275,15 +279,21 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
|
||||
override fun afterTextChanged(p0: Editable?) {
|
||||
sendMessageViewModel.closeParticipantsList()
|
||||
if (viewModel.isGroup.value == true) {
|
||||
sendMessageViewModel.closeParticipantsList()
|
||||
|
||||
val split = p0.toString().split(" ")
|
||||
for (part in split) {
|
||||
if (part == "@") {
|
||||
Log.i("$TAG '@' found, opening participants list")
|
||||
sendMessageViewModel.openParticipantsList()
|
||||
val split = p0.toString().split(" ")
|
||||
for (part in split) {
|
||||
if (part == "@") {
|
||||
Log.i("$TAG '@' found, opening participants list")
|
||||
sendMessageViewModel.openParticipantsList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (p0.toString().isNotEmpty()) {
|
||||
sendMessageViewModel.notifyChatMessageIsBeingComposed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -301,7 +311,9 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
if (e.action == MotionEvent.ACTION_UP) {
|
||||
if ((rv.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() == 0) {
|
||||
if (e.y >= 0 && e.y <= headerItemDecoration.getDecorationHeight(0)) {
|
||||
showEndToEndEncryptionDetailsBottomSheet()
|
||||
if (viewModel.isEndToEndEncrypted.value == true) {
|
||||
showEndToEndEncryptionDetailsBottomSheet()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -471,14 +483,11 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
RecyclerViewSwipeUtils(callbacks).attachToRecyclerView(binding.eventsList)
|
||||
|
||||
val localSipUri = args.localSipUri
|
||||
val remoteSipUri = args.remoteSipUri
|
||||
Log.i(
|
||||
"$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
|
||||
)
|
||||
val conversationId = args.conversationId
|
||||
Log.i("$TAG Looking up for conversation with conversation ID [$conversationId]")
|
||||
val chatRoom = sharedViewModel.displayedChatRoom
|
||||
viewModel.findChatRoom(chatRoom, localSipUri, remoteSipUri)
|
||||
Compatibility.setLocusIdInContentCaptureSession(binding.root, localSipUri, remoteSipUri)
|
||||
viewModel.findChatRoom(chatRoom, conversationId)
|
||||
Compatibility.setLocusIdInContentCaptureSession(binding.root, conversationId)
|
||||
|
||||
viewModel.chatRoomFoundEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { found ->
|
||||
|
|
@ -494,6 +503,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
} else {
|
||||
sendMessageViewModel.configureChatRoom(viewModel.chatRoom)
|
||||
adapter.setIsConversationSecured(viewModel.isEndToEndEncrypted.value == true)
|
||||
|
||||
// Wait for chat room to be ready before trying to forward a message in it
|
||||
sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner) { event ->
|
||||
|
|
@ -575,6 +585,8 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
|
||||
viewModel.isEndToEndEncrypted.observe(viewLifecycleOwner) { encrypted ->
|
||||
adapter.setIsConversationSecured(encrypted)
|
||||
|
||||
if (encrypted) {
|
||||
binding.eventsList.addItemDecoration(headerItemDecoration)
|
||||
binding.eventsList.addOnItemTouchListener(listItemTouchListener)
|
||||
|
|
@ -690,7 +702,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
|
||||
binding.setWarningConversationDisabledClickListener {
|
||||
showUnsafeConversationDetailsBottomSheet()
|
||||
showUnsafeConversationDisabledDetailsBottomSheet()
|
||||
}
|
||||
|
||||
binding.searchField.setOnEditorActionListener { view, actionId, _ ->
|
||||
|
|
@ -767,7 +779,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
if (messageLongPressViewModel.visible.value == true) return@consume
|
||||
Log.i("$TAG Requesting to open web browser on page [$url]")
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
|
|
@ -786,13 +798,6 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
viewModel.isGroup.observe(viewLifecycleOwner) { group ->
|
||||
if (group) {
|
||||
Log.i("$TAG Adding text observer to message sending area")
|
||||
binding.sendArea.messageToSend.addTextChangedListener(textObserver)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.messageDeletedEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
val message = getString(R.string.conversation_message_deleted_toast)
|
||||
|
|
@ -939,12 +944,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
})
|
||||
|
||||
binding.root.setKeyboardInsetListener { keyboardVisible ->
|
||||
sendMessageViewModel.isKeyboardOpen.value = keyboardVisible
|
||||
if (keyboardVisible) {
|
||||
sendMessageViewModel.isEmojiPickerOpen.value = false
|
||||
}
|
||||
}
|
||||
binding.sendArea.messageToSend.addTextChangedListener(textObserver)
|
||||
|
||||
scrollListener = object : ConversationScrollListener(layoutManager) {
|
||||
@UiThread
|
||||
|
|
@ -985,6 +985,10 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
.viewTreeObserver
|
||||
.addOnGlobalLayoutListener(globalLayoutObserver)
|
||||
|
||||
if (::scrollListener.isInitialized) {
|
||||
binding.eventsList.addOnScrollListener(scrollListener)
|
||||
}
|
||||
|
||||
try {
|
||||
adapter.registerAdapterDataObserver(dataObserver)
|
||||
} catch (e: IllegalStateException) {
|
||||
|
|
@ -1091,8 +1095,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
if (findNavController().currentDestination?.id == R.id.conversationFragment) {
|
||||
val action =
|
||||
ConversationFragmentDirections.actionConversationFragmentToConversationInfoFragment(
|
||||
viewModel.localSipUri,
|
||||
viewModel.remoteSipUri
|
||||
viewModel.conversationId,
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
|
@ -1112,12 +1115,12 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
|
||||
val bundle = Bundle()
|
||||
bundle.apply {
|
||||
putString("localSipUri", viewModel.localSipUri)
|
||||
putString("remoteSipUri", viewModel.remoteSipUri)
|
||||
putString("conversationId", viewModel.conversationId)
|
||||
putString("path", path)
|
||||
putBoolean("isEncrypted", fileModel.isEncrypted)
|
||||
putLong("timestamp", fileModel.fileCreationTimestamp)
|
||||
putString("originalPath", fileModel.originalPath)
|
||||
putBoolean("isFromEphemeralMessage", fileModel.isFromEphemeralMessage)
|
||||
}
|
||||
when (mimeType) {
|
||||
FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> {
|
||||
|
|
@ -1198,8 +1201,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
if (findNavController().currentDestination?.id == R.id.conversationFragment) {
|
||||
val action =
|
||||
ConversationFragmentDirections.actionConversationFragmentToConversationMediaListFragment(
|
||||
localSipUri = viewModel.localSipUri,
|
||||
remoteSipUri = viewModel.remoteSipUri
|
||||
viewModel.conversationId
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
|
@ -1210,8 +1212,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
if (findNavController().currentDestination?.id == R.id.conversationFragment) {
|
||||
val action =
|
||||
ConversationFragmentDirections.actionConversationFragmentToConversationDocumentsListFragment(
|
||||
localSipUri = viewModel.localSipUri,
|
||||
remoteSipUri = viewModel.remoteSipUri
|
||||
viewModel.conversationId
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
|
@ -1420,13 +1421,13 @@ open class ConversationFragment : SlidingPaneChildFragment() {
|
|||
bottomSheetDialog = e2eEncryptionDetailsBottomSheet
|
||||
}
|
||||
|
||||
private fun showUnsafeConversationDetailsBottomSheet() {
|
||||
val unsafeConversationDetailsBottomSheet = UnsafeConversationDetailsDialogFragment()
|
||||
unsafeConversationDetailsBottomSheet.show(
|
||||
private fun showUnsafeConversationDisabledDetailsBottomSheet() {
|
||||
val unsafeConversationDisabledDetailsBottomSheet = UnsafeConversationDisabledDetailsDialogFragment()
|
||||
unsafeConversationDisabledDetailsBottomSheet.show(
|
||||
requireActivity().supportFragmentManager,
|
||||
UnsafeConversationDetailsDialogFragment.TAG
|
||||
UnsafeConversationDisabledDetailsDialogFragment.TAG
|
||||
)
|
||||
bottomSheetDialog = unsafeConversationDetailsBottomSheet
|
||||
bottomSheetDialog = unsafeConversationDisabledDetailsBottomSheet
|
||||
}
|
||||
|
||||
private fun showOpenOrExportFileDialog(path: String, mime: String, bundle: Bundle) {
|
||||
|
|
|
|||
|
|
@ -96,13 +96,10 @@ class ConversationInfoFragment : SlidingPaneChildFragment() {
|
|||
binding.viewModel = viewModel
|
||||
observeToastEvents(viewModel)
|
||||
|
||||
val localSipUri = args.localSipUri
|
||||
val remoteSipUri = args.remoteSipUri
|
||||
Log.i(
|
||||
"$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
|
||||
)
|
||||
val conversationId = args.conversationId
|
||||
Log.i("$TAG Looking up for conversation with conversation ID [$conversationId]")
|
||||
val chatRoom = sharedViewModel.displayedChatRoom
|
||||
viewModel.findChatRoom(chatRoom, localSipUri, remoteSipUri)
|
||||
viewModel.findChatRoom(chatRoom, conversationId)
|
||||
|
||||
binding.participants.isNestedScrollingEnabled = false
|
||||
binding.participants.setHasFixedSize(false)
|
||||
|
|
@ -116,7 +113,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() {
|
|||
it.consume { found ->
|
||||
if (found) {
|
||||
Log.i(
|
||||
"$TAG Found matching conversation for local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
|
||||
"$TAG Found matching conversation with conversation ID [$conversationId]"
|
||||
)
|
||||
startPostponedEnterTransition()
|
||||
} else {
|
||||
|
|
@ -333,7 +330,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() {
|
|||
if (findNavController().currentDestination?.id == R.id.conversationInfoFragment) {
|
||||
Log.i("$TAG Going to shared media fragment")
|
||||
val action =
|
||||
ConversationInfoFragmentDirections.actionConversationInfoFragmentToConversationMediaListFragment(localSipUri, remoteSipUri)
|
||||
ConversationInfoFragmentDirections.actionConversationInfoFragmentToConversationMediaListFragment(conversationId)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
|
|
@ -342,7 +339,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() {
|
|||
if (findNavController().currentDestination?.id == R.id.conversationInfoFragment) {
|
||||
Log.i("$TAG Going to shared documents fragment")
|
||||
val action =
|
||||
ConversationInfoFragmentDirections.actionConversationInfoFragmentToConversationDocumentsListFragment(localSipUri, remoteSipUri)
|
||||
ConversationInfoFragmentDirections.actionConversationInfoFragmentToConversationDocumentsListFragment(conversationId)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,13 +94,10 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() {
|
|||
binding.viewModel = viewModel
|
||||
observeToastEvents(viewModel)
|
||||
|
||||
val localSipUri = args.localSipUri
|
||||
val remoteSipUri = args.remoteSipUri
|
||||
Log.i(
|
||||
"$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
|
||||
)
|
||||
val conversationId = args.conversationId
|
||||
Log.i("$TAG Looking up for conversation with conversation ID [$conversationId]")
|
||||
val chatRoom = sharedViewModel.displayedChatRoom
|
||||
viewModel.findChatRoom(chatRoom, localSipUri, remoteSipUri)
|
||||
viewModel.findChatRoom(chatRoom, conversationId)
|
||||
|
||||
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
|
||||
binding.mediaList.addItemDecoration(headerItemDecoration)
|
||||
|
|
@ -172,12 +169,12 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() {
|
|||
|
||||
val bundle = Bundle()
|
||||
bundle.apply {
|
||||
putString("localSipUri", viewModel.localSipUri)
|
||||
putString("remoteSipUri", viewModel.remoteSipUri)
|
||||
putString("conversationId", viewModel.conversationId)
|
||||
putString("path", path)
|
||||
putBoolean("isEncrypted", fileModel.isEncrypted)
|
||||
putLong("timestamp", fileModel.fileCreationTimestamp)
|
||||
putString("originalPath", fileModel.originalPath)
|
||||
putBoolean("isFromEphemeralMessage", fileModel.isFromEphemeralMessage)
|
||||
putBoolean("isMedia", true)
|
||||
}
|
||||
when (FileUtils.getMimeType(mime)) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import org.linphone.databinding.ChatListFragmentBinding
|
|||
import org.linphone.ui.GenericActivity
|
||||
import org.linphone.ui.fileviewer.FileViewerActivity
|
||||
import org.linphone.ui.fileviewer.MediaViewerActivity
|
||||
import org.linphone.ui.main.MainActivity.Companion.ARGUMENTS_CONVERSATION_ID
|
||||
import org.linphone.ui.main.chat.adapter.ConversationsListAdapter
|
||||
import org.linphone.ui.main.chat.viewmodel.ConversationsListViewModel
|
||||
import org.linphone.ui.main.fragment.AbstractMainFragment
|
||||
|
|
@ -162,9 +163,7 @@ class ConversationsListFragment : AbstractMainFragment() {
|
|||
it.consume { model ->
|
||||
Log.i("$TAG Show conversation with ID [${model.id}]")
|
||||
sharedViewModel.displayedChatRoom = model.chatRoom
|
||||
sharedViewModel.showConversationEvent.value = Event(
|
||||
Pair(model.localSipUri, model.remoteSipUri)
|
||||
)
|
||||
sharedViewModel.showConversationEvent.value = Event(model.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -191,16 +190,9 @@ class ConversationsListFragment : AbstractMainFragment() {
|
|||
}
|
||||
|
||||
sharedViewModel.showConversationEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
val localSipUri = pair.first
|
||||
val remoteSipUri = pair.second
|
||||
Log.i(
|
||||
"$TAG Navigating to conversation fragment with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
|
||||
)
|
||||
val action = ConversationFragmentDirections.actionGlobalConversationFragment(
|
||||
localSipUri,
|
||||
remoteSipUri
|
||||
)
|
||||
it.consume { conversationId ->
|
||||
Log.i("$TAG Navigating to conversation fragment with ID [$conversationId]")
|
||||
val action = ConversationFragmentDirections.actionGlobalConversationFragment(conversationId)
|
||||
binding.chatNavContainer.findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
|
|
@ -214,6 +206,8 @@ class ConversationsListFragment : AbstractMainFragment() {
|
|||
uri
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
} else {
|
||||
Log.e("$TAG Failed to navigate to meeting waiting room, wrong current destination (expected conversationsListFragment but was something else)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -328,12 +322,10 @@ class ConversationsListFragment : AbstractMainFragment() {
|
|||
|
||||
val args = arguments
|
||||
if (args != null) {
|
||||
val localSipUri = args.getString("LocalSipUri")
|
||||
val remoteSipUri = args.getString("RemoteSipUri")
|
||||
if (localSipUri != null && remoteSipUri != null) {
|
||||
Log.i("$TAG Found local [$localSipUri] & remote [$remoteSipUri] URIs in arguments")
|
||||
val pair = Pair(localSipUri, remoteSipUri)
|
||||
sharedViewModel.showConversationEvent.value = Event(pair)
|
||||
val conversationId = args.getString(ARGUMENTS_CONVERSATION_ID)
|
||||
if (!conversationId.isNullOrEmpty()) {
|
||||
Log.i("$TAG Found conversation ID [$conversationId] in arguments")
|
||||
sharedViewModel.showConversationEvent.value = Event(conversationId)
|
||||
args.clear()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import androidx.annotation.UiThread
|
|||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import org.linphone.databinding.ChatConversationE2eDetailsBottomSheetBinding
|
||||
import org.linphone.databinding.ChatConversationE2eEncryptedDetailsBottomSheetBinding
|
||||
|
||||
@UiThread
|
||||
class EndToEndEncryptionDetailsDialogFragment(
|
||||
|
|
@ -62,7 +62,7 @@ class EndToEndEncryptionDetailsDialogFragment(
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val view = ChatConversationE2eDetailsBottomSheetBinding.inflate(layoutInflater)
|
||||
val view = ChatConversationE2eEncryptedDetailsBottomSheetBinding.inflate(layoutInflater)
|
||||
return view.root
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,11 +93,11 @@ class StartConversationFragment : GenericAddressPickerFragment() {
|
|||
}
|
||||
|
||||
viewModel.chatRoomCreatedEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
it.consume { conversationId ->
|
||||
Log.i(
|
||||
"$TAG Conversation [${pair.second}] for local address [${pair.first}] has been created, navigating to it"
|
||||
"$TAG Conversation [$conversationId] has been created, navigating to it"
|
||||
)
|
||||
sharedViewModel.showConversationEvent.value = Event(pair)
|
||||
sharedViewModel.showConversationEvent.value = Event(conversationId)
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ import androidx.annotation.UiThread
|
|||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import org.linphone.databinding.ChatConversationUnsafeDetailsBottomSheetBinding
|
||||
import org.linphone.databinding.ChatConversationUnsafeDisabledDetailsBottomSheetBinding
|
||||
|
||||
@UiThread
|
||||
class UnsafeConversationDetailsDialogFragment(
|
||||
class UnsafeConversationDisabledDetailsDialogFragment(
|
||||
private val onDismiss: (() -> Unit)? = null
|
||||
) : BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
const val TAG = "UnsafeConversationDetailsDialogFragment"
|
||||
const val TAG = "UnsafeConversationDisabledDetailsDialogFragment"
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
|
|
@ -62,7 +62,7 @@ class UnsafeConversationDetailsDialogFragment(
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val view = ChatConversationUnsafeDetailsBottomSheetBinding.inflate(layoutInflater)
|
||||
val view = ChatConversationUnsafeDisabledDetailsBottomSheetBinding.inflate(layoutInflater)
|
||||
return view.root
|
||||
}
|
||||
}
|
||||
|
|
@ -48,11 +48,7 @@ class ConversationModel
|
|||
private const val TAG = "[Conversation Model]"
|
||||
}
|
||||
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
|
||||
val localSipUri = chatRoom.localAddress.asStringUriOnly()
|
||||
|
||||
val remoteSipUri = chatRoom.peerAddress.asStringUriOnly()
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
|
||||
val isGroup = !chatRoom.hasCapability(Capabilities.OneToOne.toInt()) && chatRoom.hasCapability(
|
||||
Capabilities.Conference.toInt()
|
||||
|
|
@ -60,6 +56,8 @@ class ConversationModel
|
|||
|
||||
val isEncrypted = chatRoom.hasCapability(Capabilities.Encrypted.toInt())
|
||||
|
||||
val isEncryptionAvailable = LinphoneUtils.isEndToEndEncryptedChatAvailable(chatRoom.core)
|
||||
|
||||
val isReadOnly = MutableLiveData<Boolean>()
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ class EventLogModel
|
|||
onWebUrlClicked: ((url: String) -> Unit)? = null,
|
||||
onContactClicked: ((friendRefKey: String) -> Unit)? = null,
|
||||
onRedToastToShow: ((pair: Pair<Int, Int>) -> Unit)? = null,
|
||||
onVoiceRecordingPlaybackEnded: ((id: String) -> Unit)? = null
|
||||
onVoiceRecordingPlaybackEnded: ((id: String) -> Unit)? = null,
|
||||
onFileToExportToNativeGallery: ((path: String) -> Unit)? = null
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "[Event Log Model]"
|
||||
|
|
@ -89,7 +90,8 @@ class EventLogModel
|
|||
onWebUrlClicked,
|
||||
onContactClicked,
|
||||
onRedToastToShow,
|
||||
onVoiceRecordingPlaybackEnded
|
||||
onVoiceRecordingPlaybackEnded,
|
||||
onFileToExportToNativeGallery
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ package org.linphone.ui.main.chat.model
|
|||
import android.media.MediaMetadataRetriever
|
||||
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
|
||||
import android.media.ThumbnailUtils
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.UiThread
|
||||
|
|
@ -35,6 +34,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext
|
|||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class FileModel
|
||||
@AnyThread
|
||||
|
|
@ -45,6 +45,7 @@ class FileModel
|
|||
val fileCreationTimestamp: Long,
|
||||
val isEncrypted: Boolean,
|
||||
val originalPath: String,
|
||||
val isFromEphemeralMessage: Boolean,
|
||||
val isWaitingToBeDownloaded: Boolean = false,
|
||||
val flexboxLayoutWrapBefore: Boolean = false,
|
||||
private val onClicked: ((model: FileModel) -> Unit)? = null
|
||||
|
|
@ -185,7 +186,7 @@ class FileModel
|
|||
private fun getDuration() {
|
||||
try {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(coreContext.context, Uri.parse(path))
|
||||
retriever.setDataSource(coreContext.context, path.toUri())
|
||||
val durationInMs = retriever.extractMetadata(METADATA_KEY_DURATION)?.toInt() ?: 0
|
||||
val seconds = durationInMs / 1000
|
||||
val duration = TimestampUtils.durationToString(seconds)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ package org.linphone.ui.main.chat.model
|
|||
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.core.Address
|
||||
|
||||
class MessageBottomSheetParticipantModel
|
||||
|
|
@ -35,8 +37,23 @@ class MessageBottomSheetParticipantModel
|
|||
) {
|
||||
val sipUri = address.asStringUriOnly()
|
||||
|
||||
val showSipUri = MutableLiveData<Boolean>()
|
||||
|
||||
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(address)
|
||||
|
||||
init {
|
||||
showSipUri.postValue(false)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun toggleShowSipUri() {
|
||||
if (!isOurOwnReaction && !corePreferences.onlyDisplaySipUriUsername) {
|
||||
showSipUri.postValue(showSipUri.value == false)
|
||||
} else {
|
||||
clicked()
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun clicked() {
|
||||
onClick?.invoke()
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class MessageDeliveryModel
|
|||
message: ChatMessage,
|
||||
state: ParticipantImdnState
|
||||
) {
|
||||
Log.i("$TAG Participant IMDN state changed [${state.state}], updating delivery status")
|
||||
computeDeliveryStatus()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.flow
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatMessage
|
||||
|
|
@ -82,7 +83,8 @@ class MessageModel
|
|||
private val onWebUrlClicked: ((url: String) -> Unit)? = null,
|
||||
private val onContactClicked: ((friendRefKey: String) -> Unit)? = null,
|
||||
private val onRedToastToShow: ((pair: Pair<Int, Int>) -> Unit)? = null,
|
||||
private val onVoiceRecordingPlaybackEnded: ((id: String) -> Unit)? = null
|
||||
private val onVoiceRecordingPlaybackEnded: ((id: String) -> Unit)? = null,
|
||||
private val onFileToExportToNativeGallery: ((path: String) -> Unit)? = null
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "[Message Model]"
|
||||
|
|
@ -98,12 +100,14 @@ class MessageModel
|
|||
|
||||
val isOutgoing = chatMessage.isOutgoing
|
||||
|
||||
val isInError = chatMessage.state == ChatMessage.State.NotDelivered
|
||||
val isInError = MutableLiveData<Boolean>()
|
||||
|
||||
val timestamp = chatMessage.time
|
||||
|
||||
val time = TimestampUtils.toString(timestamp)
|
||||
|
||||
val hideDeliveryStatus = !isOutgoing && coreContext.core.imdnToEverybodyThreshold == 1
|
||||
|
||||
val chatRoomIsReadOnly = chatMessage.chatRoom.isReadOnly ||
|
||||
(
|
||||
!chatMessage.chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt()) && LinphoneUtils.getAccountForAddress(
|
||||
|
|
@ -222,6 +226,28 @@ class MessageModel
|
|||
}
|
||||
}
|
||||
}
|
||||
isInError.postValue(messageState == ChatMessage.State.NotDelivered)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun onFileTransferTerminated(message: ChatMessage, content: Content) {
|
||||
Log.i("$TAG File [${content.name}] from message [${message.messageId}] transfer terminated")
|
||||
|
||||
// Never do auto media export for ephemeral messages!
|
||||
if (corePreferences.makePublicMediaFilesDownloaded && !message.isEphemeral) {
|
||||
val path = content.filePath
|
||||
if (path.isNullOrEmpty()) return
|
||||
|
||||
val mime = "${content.type}/${content.subtype}"
|
||||
val mimeType = FileUtils.getMimeType(mime)
|
||||
when (mimeType) {
|
||||
FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> {
|
||||
Log.i("$TAG Exporting file path [$path] to the native media gallery")
|
||||
onFileToExportToNativeGallery?.invoke(path)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
@ -274,6 +300,8 @@ class MessageModel
|
|||
init {
|
||||
updateAvatarModel()
|
||||
|
||||
isInError.postValue(chatMessage.state == ChatMessage.State.NotDelivered)
|
||||
|
||||
groupedWithNextMessage.postValue(isGroupedWithNextOne)
|
||||
groupedWithPreviousMessage.postValue(isGroupedWithPreviousOne)
|
||||
isPlayingVoiceRecord.postValue(false)
|
||||
|
|
@ -443,6 +471,7 @@ class MessageModel
|
|||
timestamp,
|
||||
isFileEncrypted,
|
||||
originalPath,
|
||||
chatMessage.isEphemeral,
|
||||
flexboxLayoutWrapBefore = wrapBefore
|
||||
) { model ->
|
||||
onContentClicked?.invoke(model)
|
||||
|
|
@ -470,7 +499,8 @@ class MessageModel
|
|||
content.fileSize.toLong(),
|
||||
timestamp,
|
||||
isFileEncrypted,
|
||||
path
|
||||
path,
|
||||
chatMessage.isEphemeral
|
||||
) { model ->
|
||||
onContentClicked?.invoke(model)
|
||||
}
|
||||
|
|
@ -482,6 +512,7 @@ class MessageModel
|
|||
timestamp,
|
||||
isFileEncrypted,
|
||||
name,
|
||||
chatMessage.isEphemeral,
|
||||
isWaitingToBeDownloaded = true
|
||||
) { model ->
|
||||
downloadContent(model, content)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ package org.linphone.ui.main.chat.model
|
|||
import android.view.View
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.ui.main.contacts.model.ContactAvatarModel
|
||||
|
||||
|
|
@ -39,6 +41,8 @@ class ParticipantModel
|
|||
) {
|
||||
val sipUri = address.asStringUriOnly()
|
||||
|
||||
val showSipUri = MutableLiveData<Boolean>()
|
||||
|
||||
val avatarModel: ContactAvatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(
|
||||
address
|
||||
)
|
||||
|
|
@ -49,6 +53,19 @@ class ParticipantModel
|
|||
avatarModel.friend
|
||||
)
|
||||
|
||||
init {
|
||||
showSipUri.postValue(false)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun toggleShowSipUri() {
|
||||
if (!corePreferences.onlyDisplaySipUriUsername) {
|
||||
showSipUri.postValue(showSipUri.value == false)
|
||||
} else {
|
||||
onClicked()
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun onClicked() {
|
||||
onClicked?.invoke(this)
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ import org.linphone.LinphoneApplication.Companion.coreContext
|
|||
import org.linphone.R
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ConferenceParams
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.MediaDirection
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.ui.GenericViewModel
|
||||
|
|
@ -51,9 +49,7 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
|
|||
|
||||
lateinit var chatRoom: ChatRoom
|
||||
|
||||
lateinit var localSipUri: String
|
||||
|
||||
lateinit var remoteSipUri: String
|
||||
lateinit var conversationId: String
|
||||
|
||||
fun isChatRoomInitialized(): Boolean {
|
||||
return ::chatRoom.isInitialized
|
||||
|
|
@ -68,17 +64,13 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
|
|||
}
|
||||
|
||||
@UiThread
|
||||
fun findChatRoom(room: ChatRoom?, localSipUri: String, remoteSipUri: String) {
|
||||
this.localSipUri = localSipUri
|
||||
this.remoteSipUri = remoteSipUri
|
||||
|
||||
fun findChatRoom(room: ChatRoom?, conversationId: String) {
|
||||
coreContext.postOnCoreThread { core ->
|
||||
Log.i(
|
||||
"$TAG Looking for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
|
||||
)
|
||||
Log.i("$TAG Looking for conversation with conversation ID [$conversationId]")
|
||||
if (room != null && ::chatRoom.isInitialized && chatRoom == room) {
|
||||
Log.i("$TAG Conversation object already in memory, skipping")
|
||||
|
||||
this@AbstractConversationViewModel.conversationId = conversationId
|
||||
beforeNotifyingChatRoomFound(sameOne = true)
|
||||
chatRoomFoundEvent.postValue(Event(true))
|
||||
afterNotifyingChatRoomFound(sameOne = true)
|
||||
|
|
@ -86,17 +78,12 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
|
|||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
val localAddress = Factory.instance().createAddress(localSipUri)
|
||||
val remoteAddress = Factory.instance().createAddress(remoteSipUri)
|
||||
|
||||
if (room != null && (!::chatRoom.isInitialized || chatRoom != room)) {
|
||||
if (localAddress?.weakEqual(room.localAddress) == true && remoteAddress?.weakEqual(
|
||||
room.peerAddress
|
||||
) == true
|
||||
) {
|
||||
if (conversationId == LinphoneUtils.getConversationId(room)) {
|
||||
Log.i("$TAG Conversation object available in sharedViewModel, using it")
|
||||
chatRoom = room
|
||||
|
||||
this@AbstractConversationViewModel.conversationId = conversationId
|
||||
beforeNotifyingChatRoomFound(sameOne = false)
|
||||
chatRoomFoundEvent.postValue(Event(true))
|
||||
afterNotifyingChatRoomFound(sameOne = false)
|
||||
|
|
@ -105,18 +92,11 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
if (localAddress != null && remoteAddress != null) {
|
||||
if (conversationId.isNotEmpty()) {
|
||||
Log.i("$TAG Searching for conversation in Core using local & peer SIP addresses")
|
||||
val params: ConferenceParams? = null
|
||||
val found = core.searchChatRoom(
|
||||
params,
|
||||
localAddress,
|
||||
remoteAddress,
|
||||
arrayOfNulls<Address>(
|
||||
0
|
||||
)
|
||||
)
|
||||
val found = core.searchChatRoomByIdentifier(conversationId)
|
||||
if (found != null) {
|
||||
this@AbstractConversationViewModel.conversationId = conversationId
|
||||
if (::chatRoom.isInitialized && chatRoom == found) {
|
||||
Log.i("$TAG Conversation object already in memory, keeping it")
|
||||
beforeNotifyingChatRoomFound(sameOne = true)
|
||||
|
|
@ -173,14 +153,7 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
|
|||
val conference = LinphoneUtils.createGroupCall(account, chatRoom.subject.orEmpty())
|
||||
if (conference == null) {
|
||||
Log.e("$TAG Failed to create group call!")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conference_failed_to_create_group_call_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conference_failed_to_create_group_call_toast, R.drawable.warning_circle)
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
|
|
@ -199,14 +172,7 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
|
|||
)
|
||||
if (conference.inviteParticipants(participants, callParams) != 0) {
|
||||
Log.e("$TAG Failed to invite participants into group call!")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conference_failed_to_create_group_call_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conference_failed_to_create_group_call_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ class ChatMessageLongPressViewModel : GenericViewModel() {
|
|||
|
||||
val onDismissedEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private lateinit var emojiBottomSheet: ChatBubbleEmojiPickerBottomSheetBinding
|
||||
private lateinit var emojiBottomSheetBehavior: BottomSheetBehavior<View>
|
||||
|
||||
init {
|
||||
|
|
@ -91,7 +90,7 @@ class ChatMessageLongPressViewModel : GenericViewModel() {
|
|||
hideCopyTextToClipboard.value = model.text.value.isNullOrEmpty()
|
||||
isChatRoomReadOnly.value = model.chatRoomIsReadOnly
|
||||
isMessageOutgoing.value = model.isOutgoing
|
||||
isMessageInError.value = model.isInError
|
||||
isMessageInError.value = model.isInError.value == true
|
||||
horizontalBias.value = if (model.isOutgoing) 1f else 0f
|
||||
messageModel.value = model
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ class ConversationDocumentsListViewModel
|
|||
MutableLiveData<Event<FileModel>>()
|
||||
}
|
||||
|
||||
override fun beforeNotifyingChatRoomFound(sameOne: Boolean) {
|
||||
@WorkerThread
|
||||
override fun afterNotifyingChatRoomFound(sameOne: Boolean) {
|
||||
loadDocumentsList()
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ class ConversationDocumentsListViewModel
|
|||
|
||||
val list = arrayListOf<FileModel>()
|
||||
Log.i(
|
||||
"$TAG Loading document contents for conversation [${LinphoneUtils.getChatRoomId(
|
||||
"$TAG Loading document contents for conversation [${LinphoneUtils.getConversationId(
|
||||
chatRoom
|
||||
)}]"
|
||||
)
|
||||
|
|
@ -79,12 +80,26 @@ class ConversationDocumentsListViewModel
|
|||
val size = documentContent.size.toLong()
|
||||
val timestamp = documentContent.creationTimestamp
|
||||
if (path.isNotEmpty() && name.isNotEmpty()) {
|
||||
val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath) {
|
||||
val messageId = documentContent.relatedChatMessageId
|
||||
val ephemeral = if (messageId != null) {
|
||||
val chatMessage = chatRoom.findMessage(messageId)
|
||||
if (chatMessage == null) {
|
||||
Log.w("$TAG Failed to find message using ID [$messageId] related to this content, can't get real info about being related to ephemeral message")
|
||||
}
|
||||
chatMessage?.isEphemeral ?: chatRoom.isEphemeralEnabled
|
||||
} else {
|
||||
Log.e("$TAG No chat message ID related to this content, can't get real info about being related to ephemeral message")
|
||||
chatRoom.isEphemeralEnabled
|
||||
}
|
||||
|
||||
val model =
|
||||
FileModel(path, name, size, timestamp, isEncrypted, originalPath, ephemeral) {
|
||||
openDocumentEvent.postValue(Event(it))
|
||||
}
|
||||
list.add(model)
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("$TAG [${documents.size}] documents have been processed")
|
||||
documentsList.postValue(list)
|
||||
operationInProgress.postValue(false)
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ class ConversationForwardMessageViewModel
|
|||
|
||||
val operationInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val chatRoomCreatedEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String>>>()
|
||||
val chatRoomCreatedEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val showNumberOrAddressPickerDialogEvent: MutableLiveData<Event<ArrayList<ContactNumberOrAddressModel>>> by lazy {
|
||||
|
|
@ -83,33 +83,19 @@ class ConversationForwardMessageViewModel
|
|||
val state = chatRoom.state
|
||||
if (state == ChatRoom.State.Instantiated) return
|
||||
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]")
|
||||
|
||||
if (state == ChatRoom.State.Created) {
|
||||
Log.i("$TAG Conversation [$id] successfully created")
|
||||
chatRoom.removeListener(this)
|
||||
operationInProgress.postValue(false)
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("$TAG Conversation [$id] creation has failed!")
|
||||
chatRoom.removeListener(this)
|
||||
operationInProgress.postValue(false)
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conversation_failed_to_create_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -135,16 +121,9 @@ class ConversationForwardMessageViewModel
|
|||
@UiThread
|
||||
fun handleClickOnModel(model: ConversationContactOrSuggestionModel) {
|
||||
coreContext.postOnCoreThread { core ->
|
||||
if (model.localAddress != null) {
|
||||
if (model.conversationId.isNotEmpty()) {
|
||||
Log.i("$TAG User clicked on an existing conversation")
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
model.localAddress.asStringUriOnly(),
|
||||
model.address.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(model.conversationId))
|
||||
if (searchFilter.value.orEmpty().isNotEmpty()) {
|
||||
// Clear filter after it was used
|
||||
coreContext.postOnMainThread {
|
||||
|
|
@ -156,7 +135,7 @@ class ConversationForwardMessageViewModel
|
|||
|
||||
val friend = model.friend
|
||||
if (friend == null) {
|
||||
Log.i("$TAG Friend is null, using address [${model.address}]")
|
||||
Log.i("$TAG Friend is null, using address [${model.address.asStringUriOnly()}]")
|
||||
onAddressSelected(model.address)
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
|
@ -195,18 +174,20 @@ class ConversationForwardMessageViewModel
|
|||
params.isChatEnabled = true
|
||||
params.isGroupEnabled = false
|
||||
params.subject = AppUtils.getString(R.string.conversation_one_to_one_hidden_subject)
|
||||
params.account = account
|
||||
|
||||
val chatParams = params.chatParams ?: return
|
||||
chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
|
||||
val sameDomain = remote.domain == corePreferences.defaultDomain && remote.domain == account.params.domain
|
||||
if (account.params.instantMessagingEncryptionMandatory && sameDomain) {
|
||||
Log.i("$TAG Account is in secure mode & domain matches, creating a E2E conversation")
|
||||
Log.i("$TAG Account is in secure mode & domain matches, creating an E2E encrypted conversation")
|
||||
chatParams.backend = ChatRoom.Backend.FlexisipChat
|
||||
params.securityLevel = Conference.SecurityLevel.EndToEnd
|
||||
} else if (!account.params.instantMessagingEncryptionMandatory) {
|
||||
if (LinphoneUtils.isEndToEndEncryptedChatAvailable(core)) {
|
||||
Log.i(
|
||||
"$TAG Account is in interop mode but LIME is available, creating a E2E conversation"
|
||||
"$TAG Account is in interop mode but LIME is available, creating an E2E encrypted conversation"
|
||||
)
|
||||
chatParams.backend = ChatRoom.Backend.FlexisipChat
|
||||
params.securityLevel = Conference.SecurityLevel.EndToEnd
|
||||
|
|
@ -222,14 +203,7 @@ class ConversationForwardMessageViewModel
|
|||
"$TAG Account is in secure mode, can't chat with SIP address of different domain [${remote.asStringUriOnly()}]"
|
||||
)
|
||||
operationInProgress.postValue(false)
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conversation_invalid_participant_due_to_security_mode_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conversation_invalid_participant_due_to_security_mode_toast, R.drawable.warning_circle)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -240,63 +214,35 @@ class ConversationForwardMessageViewModel
|
|||
Log.i(
|
||||
"$TAG No existing 1-1 conversation between local account [${localAddress?.asStringUriOnly()}] and remote [${remote.asStringUriOnly()}] was found for given parameters, let's create it"
|
||||
)
|
||||
val chatRoom = core.createChatRoom(params, localAddress, participants)
|
||||
val chatRoom = core.createChatRoom(params, participants)
|
||||
if (chatRoom != null) {
|
||||
if (chatParams.backend == ChatRoom.Backend.FlexisipChat) {
|
||||
if (chatRoom.state == ChatRoom.State.Created) {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG 1-1 conversation [$id] has been created")
|
||||
operationInProgress.postValue(false)
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
} else {
|
||||
Log.i("$TAG Conversation isn't in Created state yet, wait for it")
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
}
|
||||
} else {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Conversation successfully created [$id]")
|
||||
operationInProgress.postValue(false)
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
}
|
||||
} else {
|
||||
Log.e("$TAG Failed to create 1-1 conversation with [${remote.asStringUriOnly()}]!")
|
||||
operationInProgress.postValue(false)
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conversation_failed_to_create_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle)
|
||||
}
|
||||
} else {
|
||||
Log.w(
|
||||
"$TAG A 1-1 conversation between local account [${localAddress?.asStringUriOnly()}] and remote [${remote.asStringUriOnly()}] for given parameters already exists!"
|
||||
)
|
||||
operationInProgress.postValue(false)
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
existingChatRoom.localAddress.asStringUriOnly(),
|
||||
existingChatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(LinphoneUtils.getConversationId(existingChatRoom)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class ConversationInfoViewModel
|
|||
R.string.conversation_info_participant_added_to_conversation_toast,
|
||||
getParticipant(eventLog)
|
||||
)
|
||||
showFormattedGreenToastEvent.postValue(Event(Pair(message, R.drawable.user_circle)))
|
||||
showFormattedGreenToast(message, R.drawable.user_circle)
|
||||
|
||||
computeParticipantsList()
|
||||
infoChangedEvent.postValue(Event(true))
|
||||
|
|
@ -121,7 +121,7 @@ class ConversationInfoViewModel
|
|||
R.string.conversation_info_participant_removed_from_conversation_toast,
|
||||
getParticipant(eventLog)
|
||||
)
|
||||
showFormattedGreenToastEvent.postValue(Event(Pair(message, R.drawable.user_circle)))
|
||||
showFormattedGreenToast(message, R.drawable.user_circle)
|
||||
|
||||
computeParticipantsList()
|
||||
infoChangedEvent.postValue(Event(true))
|
||||
|
|
@ -143,7 +143,7 @@ class ConversationInfoViewModel
|
|||
getParticipant(eventLog)
|
||||
)
|
||||
}
|
||||
showFormattedGreenToastEvent.postValue(Event(Pair(message, R.drawable.user_circle)))
|
||||
showFormattedGreenToast(message, R.drawable.user_circle)
|
||||
|
||||
computeParticipantsList()
|
||||
}
|
||||
|
|
@ -151,11 +151,9 @@ class ConversationInfoViewModel
|
|||
@WorkerThread
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
Log.i(
|
||||
"$TAG Conversation [${LinphoneUtils.getChatRoomId(chatRoom)}] has a new subject [${chatRoom.subject}]"
|
||||
)
|
||||
showGreenToastEvent.postValue(
|
||||
Event(Pair(R.string.conversation_subject_changed_toast, R.drawable.check))
|
||||
"$TAG Conversation [${LinphoneUtils.getConversationId(chatRoom)}] has a new subject [${chatRoom.subject}]"
|
||||
)
|
||||
showGreenToast(R.string.conversation_subject_changed_toast, R.drawable.check)
|
||||
|
||||
subject.postValue(chatRoom.subject)
|
||||
infoChangedEvent.postValue(Event(true))
|
||||
|
|
@ -166,34 +164,13 @@ class ConversationInfoViewModel
|
|||
Log.i("$TAG Ephemeral event [${eventLog.type}]")
|
||||
when (eventLog.type) {
|
||||
EventLog.Type.ConferenceEphemeralMessageEnabled -> {
|
||||
showGreenToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conversation_ephemeral_messages_enabled_toast,
|
||||
R.drawable.clock_countdown
|
||||
)
|
||||
)
|
||||
)
|
||||
showGreenToast(R.string.conversation_ephemeral_messages_enabled_toast, R.drawable.clock_countdown)
|
||||
}
|
||||
EventLog.Type.ConferenceEphemeralMessageDisabled -> {
|
||||
showGreenToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conversation_ephemeral_messages_disabled_toast,
|
||||
R.drawable.clock_countdown
|
||||
)
|
||||
)
|
||||
)
|
||||
showGreenToast(R.string.conversation_ephemeral_messages_disabled_toast, R.drawable.clock_countdown)
|
||||
}
|
||||
else -> {
|
||||
showGreenToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conversation_ephemeral_messages_lifetime_changed_toast,
|
||||
R.drawable.clock_countdown
|
||||
)
|
||||
)
|
||||
)
|
||||
showGreenToast(R.string.conversation_ephemeral_messages_lifetime_changed_toast, R.drawable.clock_countdown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -241,7 +218,7 @@ class ConversationInfoViewModel
|
|||
fun leaveGroup() {
|
||||
coreContext.postOnCoreThread {
|
||||
if (isChatRoomInitialized()) {
|
||||
Log.i("$TAG Leaving conversation [${LinphoneUtils.getChatRoomId(chatRoom)}]")
|
||||
Log.i("$TAG Leaving conversation [${LinphoneUtils.getConversationId(chatRoom)}]")
|
||||
chatRoom.leave()
|
||||
}
|
||||
groupLeftEvent.postValue(Event(true))
|
||||
|
|
@ -253,7 +230,7 @@ class ConversationInfoViewModel
|
|||
coreContext.postOnCoreThread {
|
||||
if (isChatRoomInitialized()) {
|
||||
Log.i(
|
||||
"$TAG Cleaning conversation [${LinphoneUtils.getChatRoomId(chatRoom)}] history"
|
||||
"$TAG Cleaning conversation [${LinphoneUtils.getConversationId(chatRoom)}] history"
|
||||
)
|
||||
chatRoom.deleteHistory()
|
||||
}
|
||||
|
|
@ -303,7 +280,7 @@ class ConversationInfoViewModel
|
|||
coreContext.postOnCoreThread {
|
||||
val address = participantModel.address
|
||||
Log.i(
|
||||
"$TAG Removing participant [$address] from the conversation [${LinphoneUtils.getChatRoomId(
|
||||
"$TAG Removing participant [$address] from the conversation [${LinphoneUtils.getConversationId(
|
||||
chatRoom
|
||||
)}]"
|
||||
)
|
||||
|
|
@ -324,7 +301,7 @@ class ConversationInfoViewModel
|
|||
coreContext.postOnCoreThread {
|
||||
val address = participantModel.address
|
||||
Log.i(
|
||||
"$TAG Granting admin rights to participant [$address] from the conversation [${LinphoneUtils.getChatRoomId(
|
||||
"$TAG Granting admin rights to participant [$address] from the conversation [${LinphoneUtils.getConversationId(
|
||||
chatRoom
|
||||
)}]"
|
||||
)
|
||||
|
|
@ -345,7 +322,7 @@ class ConversationInfoViewModel
|
|||
coreContext.postOnCoreThread {
|
||||
val address = participantModel.address
|
||||
Log.i(
|
||||
"$TAG Removing admin rights from participant [$address] from the conversation [${LinphoneUtils.getChatRoomId(
|
||||
"$TAG Removing admin rights from participant [$address] from the conversation [${LinphoneUtils.getConversationId(
|
||||
chatRoom
|
||||
)}]"
|
||||
)
|
||||
|
|
@ -431,14 +408,7 @@ class ConversationInfoViewModel
|
|||
val ok = chatRoom.addParticipants(toAddList.toTypedArray())
|
||||
if (!ok) {
|
||||
Log.w("$TAG Failed to add some/all participants to the group!")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conversation_failed_to_add_participant_to_group_conversation_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conversation_failed_to_add_participant_to_group_conversation_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,11 +36,14 @@ class ConversationMediaListViewModel
|
|||
|
||||
val mediaList = MutableLiveData<List<FileModel>>()
|
||||
|
||||
val operationInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val openMediaEvent: MutableLiveData<Event<FileModel>> by lazy {
|
||||
MutableLiveData<Event<FileModel>>()
|
||||
}
|
||||
|
||||
override fun beforeNotifyingChatRoomFound(sameOne: Boolean) {
|
||||
@WorkerThread
|
||||
override fun afterNotifyingChatRoomFound(sameOne: Boolean) {
|
||||
loadMediaList()
|
||||
}
|
||||
|
||||
|
|
@ -52,9 +55,11 @@ class ConversationMediaListViewModel
|
|||
|
||||
@WorkerThread
|
||||
private fun loadMediaList() {
|
||||
operationInProgress.postValue(true)
|
||||
|
||||
val list = arrayListOf<FileModel>()
|
||||
Log.i(
|
||||
"$TAG Loading media contents for conversation [${LinphoneUtils.getChatRoomId(
|
||||
"$TAG Loading media contents for conversation [${LinphoneUtils.getConversationId(
|
||||
chatRoom
|
||||
)}]"
|
||||
)
|
||||
|
|
@ -78,13 +83,16 @@ class ConversationMediaListViewModel
|
|||
val size = mediaContent.size.toLong()
|
||||
val timestamp = mediaContent.creationTimestamp
|
||||
if (path.isNotEmpty() && name.isNotEmpty()) {
|
||||
val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath) {
|
||||
val model =
|
||||
FileModel(path, name, size, timestamp, isEncrypted, originalPath, chatRoom.isEphemeralEnabled) {
|
||||
openMediaEvent.postValue(Event(it))
|
||||
}
|
||||
list.add(model)
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("$TAG [${media.size}] media have been processed")
|
||||
mediaList.postValue(list)
|
||||
operationInProgress.postValue(false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import org.linphone.utils.AppUtils
|
|||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class ConversationViewModel
|
||||
@UiThread
|
||||
|
|
@ -72,6 +73,8 @@ class ConversationViewModel
|
|||
|
||||
val isEndToEndEncrypted = MutableLiveData<Boolean>()
|
||||
|
||||
val isEndToEndEncryptionAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val isGroup = MutableLiveData<Boolean>()
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
|
@ -288,7 +291,7 @@ class ConversationViewModel
|
|||
list.remove(found)
|
||||
eventsList = list
|
||||
updateEvents.postValue(Event(true))
|
||||
isEmpty.postValue(eventsList.isEmpty)
|
||||
isEmpty.postValue(eventsList.isEmpty())
|
||||
} else {
|
||||
Log.e("$TAG Failed to find matching message in conversation events list")
|
||||
}
|
||||
|
|
@ -313,7 +316,8 @@ class ConversationViewModel
|
|||
}
|
||||
|
||||
init {
|
||||
coreContext.postOnCoreThread {
|
||||
coreContext.postOnCoreThread { core ->
|
||||
isEndToEndEncryptionAvailable.postValue(LinphoneUtils.isEndToEndEncryptedChatAvailable(core))
|
||||
coreContext.contactsManager.addListener(contactsListener)
|
||||
}
|
||||
|
||||
|
|
@ -428,7 +432,7 @@ class ConversationViewModel
|
|||
list.remove(found)
|
||||
eventsList = list
|
||||
updateEvents.postValue(Event(true))
|
||||
isEmpty.postValue(eventsList.isEmpty)
|
||||
isEmpty.postValue(eventsList.isEmpty())
|
||||
} else {
|
||||
Log.e(
|
||||
"$TAG Failed to find chat message id [${chatMessageModel.id}] in events list!"
|
||||
|
|
@ -470,7 +474,7 @@ class ConversationViewModel
|
|||
fun updateCurrentlyDisplayedConversation() {
|
||||
coreContext.postOnCoreThread {
|
||||
if (isChatRoomInitialized()) {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i(
|
||||
"$TAG Asking notifications manager not to notify messages for conversation [$id]"
|
||||
)
|
||||
|
|
@ -524,7 +528,7 @@ class ConversationViewModel
|
|||
list.addAll(eventsList)
|
||||
eventsList = list
|
||||
updateEvents.postValue(Event(true))
|
||||
isEmpty.postValue(eventsList.isEmpty)
|
||||
isEmpty.postValue(eventsList.isEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -647,7 +651,7 @@ class ConversationViewModel
|
|||
Log.i("$TAG Extracted [${list.size}] events from conversation history in database")
|
||||
eventsList = list
|
||||
updateEvents.postValue(Event(true))
|
||||
isEmpty.postValue(eventsList.isEmpty)
|
||||
isEmpty.postValue(eventsList.isEmpty())
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
@ -694,7 +698,7 @@ class ConversationViewModel
|
|||
list.addAll(newList)
|
||||
eventsList = list
|
||||
updateEvents.postValue(Event(true))
|
||||
isEmpty.postValue(eventsList.isEmpty)
|
||||
isEmpty.postValue(eventsList.isEmpty())
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
@ -726,7 +730,7 @@ class ConversationViewModel
|
|||
list.addAll(eventsList)
|
||||
eventsList = list
|
||||
updateEvents.postValue(Event(true))
|
||||
isEmpty.postValue(eventsList.isEmpty)
|
||||
isEmpty.postValue(eventsList.isEmpty())
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
@ -761,6 +765,19 @@ class ConversationViewModel
|
|||
},
|
||||
{ id ->
|
||||
voiceRecordPlaybackEndedEvent.postValue(Event(id))
|
||||
},
|
||||
{ filePath ->
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
Log.i("$TAG Export file [$filePath] to Android's MediaStore")
|
||||
val mediaStorePath = FileUtils.addContentToMediaStore(filePath)
|
||||
if (mediaStorePath.isNotEmpty()) {
|
||||
Log.i("$TAG File [$filePath] has been successfully exported to MediaStore")
|
||||
} else {
|
||||
Log.e("$TAG Failed to export file [$filePath] to MediaStore!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
eventsList.add(model)
|
||||
|
|
@ -784,7 +801,7 @@ class ConversationViewModel
|
|||
eventsList.addAll(processGroupedEvents(arrayListOf(event)))
|
||||
} else {
|
||||
for (event in history) {
|
||||
if (groupedEventLogs.isEmpty) {
|
||||
if (groupedEventLogs.isEmpty()) {
|
||||
groupedEventLogs.add(event)
|
||||
continue
|
||||
}
|
||||
|
|
@ -925,11 +942,7 @@ class ConversationViewModel
|
|||
} else {
|
||||
R.string.conversation_search_no_more_match
|
||||
}
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(message, R.drawable.magnifying_glass)
|
||||
)
|
||||
)
|
||||
showRedToast(message, R.drawable.magnifying_glass)
|
||||
} else {
|
||||
Log.i(
|
||||
"$TAG Found result [${match.chatMessage?.messageId}] while looking up for message with text [$textToSearch] in direction [$direction] starting from message [${latestMatch?.chatMessage?.messageId}]"
|
||||
|
|
@ -962,7 +975,7 @@ class ConversationViewModel
|
|||
|
||||
@UiThread
|
||||
fun copyFileToUri(filePath: String, dest: Uri) {
|
||||
val source = Uri.parse(FileUtils.getProperFilePath(filePath))
|
||||
val source = FileUtils.getProperFilePath(filePath).toUri()
|
||||
Log.i("$TAG Copying file URI [$source] to [$dest]")
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
|
@ -971,24 +984,10 @@ class ConversationViewModel
|
|||
Log.i(
|
||||
"$TAG File [$filePath] has been successfully exported to documents"
|
||||
)
|
||||
showGreenToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.file_successfully_exported_to_documents_toast,
|
||||
R.drawable.check
|
||||
)
|
||||
)
|
||||
)
|
||||
showGreenToast(R.string.file_successfully_exported_to_documents_toast, R.drawable.check)
|
||||
} else {
|
||||
Log.e("$TAG Failed to export file [$filePath] to documents!")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.export_file_to_documents_error_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.export_file_to_documents_error_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import org.linphone.core.Friend
|
|||
import org.linphone.core.tools.Log
|
||||
import org.linphone.ui.main.chat.model.ConversationModel
|
||||
import org.linphone.ui.main.viewmodel.AbstractMainViewModel
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ConversationsListViewModel
|
||||
|
|
@ -55,7 +54,7 @@ class ConversationsListViewModel
|
|||
state: ChatRoom.State?
|
||||
) {
|
||||
Log.i(
|
||||
"$TAG Conversation [${LinphoneUtils.getChatRoomId(chatRoom)}] state changed [$state]"
|
||||
"$TAG Conversation [${LinphoneUtils.getConversationId(chatRoom)}] state changed [$state]"
|
||||
)
|
||||
|
||||
when (state) {
|
||||
|
|
@ -67,7 +66,17 @@ class ConversationsListViewModel
|
|||
|
||||
@WorkerThread
|
||||
override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
|
||||
reorderChatRooms()
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
val found = conversations.value.orEmpty().find {
|
||||
it.id == id
|
||||
}
|
||||
if (found == null) {
|
||||
Log.i("$TAG Message sent for a conversation not yet in the list (probably was empty), adding it")
|
||||
addChatRoom(chatRoom)
|
||||
} else {
|
||||
Log.i("$TAG Message sent for an existing conversation, re-order them")
|
||||
reorderChatRooms()
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
@ -76,7 +85,17 @@ class ConversationsListViewModel
|
|||
chatRoom: ChatRoom,
|
||||
messages: Array<out ChatMessage>
|
||||
) {
|
||||
reorderChatRooms()
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
val found = conversations.value.orEmpty().find {
|
||||
it.id == id
|
||||
}
|
||||
if (found == null) {
|
||||
Log.i("$TAG Message(s) received for a conversation not yet in the list (probably was empty), adding it")
|
||||
addChatRoom(chatRoom)
|
||||
} else {
|
||||
Log.i("$TAG Message(s) received for an existing conversation, re-order them")
|
||||
reorderChatRooms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,7 +189,8 @@ class ConversationsListViewModel
|
|||
}
|
||||
|
||||
val hideEmptyChatRooms = coreContext.core.config.getBool("misc", "hide_empty_chat_rooms", true)
|
||||
if (hideEmptyChatRooms && chatRoom.lastMessageInHistory == null) {
|
||||
// Hide empty chat rooms only applies to 1-1 conversations
|
||||
if (hideEmptyChatRooms && !LinphoneUtils.isChatRoomAGroup(chatRoom) && chatRoom.lastMessageInHistory == null) {
|
||||
Log.w("$TAG Chat room with local address [${localAddress.asStringUriOnly()}] and peer address [${peerAddress.asStringUriOnly()}] is empty, not adding it to match Core setting")
|
||||
return
|
||||
}
|
||||
|
|
@ -220,11 +240,7 @@ class ConversationsListViewModel
|
|||
)
|
||||
}
|
||||
|
||||
showGreenToastEvent.postValue(
|
||||
Event(
|
||||
Pair(R.string.conversation_deleted_toast, R.drawable.chat_teardrop_text)
|
||||
)
|
||||
)
|
||||
showGreenToast(R.string.conversation_deleted_toast, R.drawable.chat_teardrop_text)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
|
|||
|
|
@ -154,8 +154,10 @@ class SendMessageInConversationViewModel
|
|||
isFileTransferServerAvailable.postValue(!core.fileTransferServer.isNullOrEmpty())
|
||||
}
|
||||
|
||||
isKeyboardOpen.value = false
|
||||
isEmojiPickerOpen.value = false
|
||||
areFilePickersOpen.value = false
|
||||
isVoiceRecording.value = false
|
||||
isPlayingVoiceRecord.value = false
|
||||
isCallConversation.value = false
|
||||
maxNumberOfAttachmentsReached.value = false
|
||||
|
|
@ -191,6 +193,7 @@ class SendMessageInConversationViewModel
|
|||
|
||||
@UiThread
|
||||
fun configureChatRoom(room: ChatRoom) {
|
||||
Log.i("$TAG Chat room configured")
|
||||
chatRoom = room
|
||||
coreContext.postOnCoreThread {
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
|
|
@ -247,6 +250,8 @@ class SendMessageInConversationViewModel
|
|||
@UiThread
|
||||
fun sendMessage() {
|
||||
coreContext.postOnCoreThread {
|
||||
val isBasicChatRoom: Boolean = chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())
|
||||
|
||||
val messageToReplyTo = chatMessageToReplyTo
|
||||
val message = if (messageToReplyTo != null) {
|
||||
Log.i("$TAG Sending message as reply to [${messageToReplyTo.messageId}]")
|
||||
|
|
@ -254,10 +259,12 @@ class SendMessageInConversationViewModel
|
|||
} else {
|
||||
chatRoom.createEmptyMessage()
|
||||
}
|
||||
var contentAdded = false
|
||||
|
||||
val toSend = textToSend.value.orEmpty().trim()
|
||||
if (toSend.isNotEmpty()) {
|
||||
message.addUtf8TextContent(toSend)
|
||||
contentAdded = true
|
||||
}
|
||||
|
||||
if (isVoiceRecording.value == true && voiceMessageRecorder.file != null) {
|
||||
|
|
@ -267,7 +274,14 @@ class SendMessageInConversationViewModel
|
|||
Log.i(
|
||||
"$TAG Voice recording content created, file name is ${content.name} and duration is ${content.fileDuration}"
|
||||
)
|
||||
message.addContent(content)
|
||||
if (isBasicChatRoom && contentAdded) {
|
||||
val voiceMessage = chatRoom.createEmptyMessage()
|
||||
voiceMessage.addContent(content)
|
||||
voiceMessage.send()
|
||||
} else {
|
||||
message.addContent(content)
|
||||
contentAdded = true
|
||||
}
|
||||
} else {
|
||||
Log.e("$TAG Voice recording content couldn't be created!")
|
||||
}
|
||||
|
|
@ -292,7 +306,14 @@ class SendMessageInConversationViewModel
|
|||
// Let the file body handler take care of the upload
|
||||
content.filePath = attachment.path
|
||||
|
||||
message.addFileContent(content)
|
||||
if (isBasicChatRoom && contentAdded) {
|
||||
val fileMessage = chatRoom.createEmptyMessage()
|
||||
fileMessage.addFileContent(content)
|
||||
fileMessage.send()
|
||||
} else {
|
||||
message.addFileContent(content)
|
||||
contentAdded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -318,6 +339,16 @@ class SendMessageInConversationViewModel
|
|||
attachments.postValue(attachmentsList)
|
||||
|
||||
chatMessageToReplyTo = null
|
||||
maxNumberOfAttachmentsReached.postValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun notifyChatMessageIsBeingComposed() {
|
||||
coreContext.postOnCoreThread {
|
||||
if (::chatRoom.isInitialized) {
|
||||
chatRoom.compose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -353,14 +384,7 @@ class SendMessageInConversationViewModel
|
|||
Log.w(
|
||||
"$TAG Max number of attachments [$MAX_FILES_TO_ATTACH] reached, file [$file] won't be attached"
|
||||
)
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conversation_maximum_number_of_attachments_reached,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conversation_maximum_number_of_attachments_reached, R.drawable.warning_circle)
|
||||
viewModelScope.launch {
|
||||
Log.i("$TAG Deleting temporary file [$file]")
|
||||
FileUtils.deleteFile(file)
|
||||
|
|
@ -373,7 +397,7 @@ class SendMessageInConversationViewModel
|
|||
|
||||
val fileName = FileUtils.getNameFromFilePath(file)
|
||||
val timestamp = System.currentTimeMillis() / 1000
|
||||
val model = FileModel(file, fileName, 0, timestamp, false, file) { model ->
|
||||
val model = FileModel(file, fileName, 0, timestamp, false, file, false) { model ->
|
||||
removeAttachment(model.path)
|
||||
}
|
||||
|
||||
|
|
@ -409,7 +433,7 @@ class SendMessageInConversationViewModel
|
|||
attachments.value = list
|
||||
maxNumberOfAttachmentsReached.value = list.size >= MAX_FILES_TO_ATTACH
|
||||
|
||||
if (list.isEmpty) {
|
||||
if (list.isEmpty()) {
|
||||
isFileAttachmentsListOpen.value = false
|
||||
}
|
||||
}
|
||||
|
|
@ -423,9 +447,7 @@ class SendMessageInConversationViewModel
|
|||
Log.i("$TAG Sending forwarded message")
|
||||
forwardedMessage.send()
|
||||
|
||||
showGreenToastEvent.postValue(
|
||||
Event(Pair(R.string.conversation_message_forwarded_toast, R.drawable.forward))
|
||||
)
|
||||
showGreenToast(R.string.conversation_message_forwarded_toast, R.drawable.forward)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -580,14 +602,7 @@ class SendMessageInConversationViewModel
|
|||
"$TAG Max duration for voice recording exceeded (${maxVoiceRecordDuration}ms), stopping."
|
||||
)
|
||||
stopVoiceRecorder()
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.conversation_voice_recording_max_duration_reached_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.conversation_voice_recording_max_duration_reached_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
|
@ -652,11 +667,7 @@ class SendMessageInConversationViewModel
|
|||
val lowMediaVolume = AudioUtils.isMediaVolumeLow(context)
|
||||
if (lowMediaVolume) {
|
||||
Log.w("$TAG Media volume is low, notifying user as they may not hear voice message")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(R.string.media_playback_low_volume_warning_toast, R.drawable.speaker_slash)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.media_playback_low_volume_warning_toast, R.drawable.speaker_slash)
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ class StartConversationViewModel
|
|||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
val chatRoomCreatedEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String>>>()
|
||||
val chatRoomCreatedEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val chatRoomListener = object : ChatRoomListenerStub() {
|
||||
|
|
@ -65,21 +65,14 @@ class StartConversationViewModel
|
|||
val state = chatRoom.state
|
||||
if (state == ChatRoom.State.Instantiated) return
|
||||
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]")
|
||||
|
||||
if (state == ChatRoom.State.Created) {
|
||||
Log.i("$TAG Conversation [$id] successfully created")
|
||||
chatRoom.removeListener(this)
|
||||
operationInProgress.postValue(false)
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("$TAG Conversation [$id] creation has failed!")
|
||||
chatRoom.removeListener(this)
|
||||
|
|
@ -119,6 +112,8 @@ class StartConversationViewModel
|
|||
params.isGroupEnabled = true
|
||||
params.subject = groupChatRoomSubject
|
||||
params.securityLevel = Conference.SecurityLevel.EndToEnd
|
||||
params.account = account
|
||||
|
||||
val chatParams = params.chatParams ?: return@postOnCoreThread
|
||||
chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
chatParams.backend = ChatRoom.Backend.FlexisipChat
|
||||
|
|
@ -127,30 +122,18 @@ class StartConversationViewModel
|
|||
for (participant in selection.value.orEmpty()) {
|
||||
participants.add(participant.address)
|
||||
}
|
||||
val localAddress = account.params.identityAddress
|
||||
|
||||
val participantsArray = arrayOf<Address>()
|
||||
val chatRoom = core.createChatRoom(
|
||||
params,
|
||||
localAddress,
|
||||
participants.toArray(participantsArray)
|
||||
)
|
||||
val chatRoom = core.createChatRoom(params, participants.toArray(participantsArray))
|
||||
if (chatRoom != null) {
|
||||
if (chatParams.backend == ChatRoom.Backend.FlexisipChat) {
|
||||
if (chatRoom.state == ChatRoom.State.Created) {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i(
|
||||
"$TAG Group conversation [$id] ($groupChatRoomSubject) has been created"
|
||||
)
|
||||
operationInProgress.postValue(false)
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
} else {
|
||||
Log.i(
|
||||
"$TAG Conversation [$groupChatRoomSubject] isn't in Created state yet, wait for it"
|
||||
|
|
@ -158,17 +141,10 @@ class StartConversationViewModel
|
|||
chatRoom.addListener(chatRoomListener)
|
||||
}
|
||||
} else {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Conversation successfully created [$id] ($groupChatRoomSubject)")
|
||||
operationInProgress.postValue(false)
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
}
|
||||
} else {
|
||||
Log.e("$TAG Failed to create group conversation [$groupChatRoomSubject]!")
|
||||
|
|
@ -197,18 +173,20 @@ class StartConversationViewModel
|
|||
params.isChatEnabled = true
|
||||
params.isGroupEnabled = false
|
||||
params.subject = AppUtils.getString(R.string.conversation_one_to_one_hidden_subject)
|
||||
params.account = account
|
||||
|
||||
val chatParams = params.chatParams ?: return
|
||||
chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
|
||||
val sameDomain = remote.domain == corePreferences.defaultDomain && remote.domain == account.params.domain
|
||||
if (account.params.instantMessagingEncryptionMandatory && sameDomain) {
|
||||
Log.i("$TAG Account is in secure mode & domain matches, creating a E2E conversation")
|
||||
Log.i("$TAG Account is in secure mode & domain matches, creating an E2E encrypted conversation")
|
||||
chatParams.backend = ChatRoom.Backend.FlexisipChat
|
||||
params.securityLevel = Conference.SecurityLevel.EndToEnd
|
||||
} else if (!account.params.instantMessagingEncryptionMandatory) {
|
||||
if (LinphoneUtils.isEndToEndEncryptedChatAvailable(core)) {
|
||||
Log.i(
|
||||
"$TAG Account is in interop mode but LIME is available, creating a E2E conversation"
|
||||
"$TAG Account is in interop mode but LIME is available, creating an E2E encrypted conversation"
|
||||
)
|
||||
chatParams.backend = ChatRoom.Backend.FlexisipChat
|
||||
params.securityLevel = Conference.SecurityLevel.EndToEnd
|
||||
|
|
@ -237,37 +215,24 @@ class StartConversationViewModel
|
|||
Log.i(
|
||||
"$TAG No existing 1-1 conversation between local account [${localAddress?.asStringUriOnly()}] and remote [${remote.asStringUriOnly()}] was found for given parameters, let's create it"
|
||||
)
|
||||
val chatRoom = core.createChatRoom(params, localAddress, participants)
|
||||
val chatRoom = core.createChatRoom(params, participants)
|
||||
if (chatRoom != null) {
|
||||
if (chatParams.backend == ChatRoom.Backend.FlexisipChat) {
|
||||
if (chatRoom.state == ChatRoom.State.Created) {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val state = chatRoom.state
|
||||
if (state == ChatRoom.State.Created) {
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG 1-1 conversation [$id] has been created")
|
||||
operationInProgress.postValue(false)
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
} else {
|
||||
Log.i("$TAG Conversation isn't in Created state yet, wait for it")
|
||||
Log.i("$TAG Conversation isn't in Created state yet (state is [$state]), wait for it")
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
}
|
||||
} else {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Conversation successfully created [$id]")
|
||||
operationInProgress.postValue(false)
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
}
|
||||
} else {
|
||||
Log.e("$TAG Failed to create 1-1 conversation with [${remote.asStringUriOnly()}]!")
|
||||
|
|
@ -281,14 +246,7 @@ class StartConversationViewModel
|
|||
"$TAG A 1-1 conversation between local account [${localAddress?.asStringUriOnly()}] and remote [${remote.asStringUriOnly()}] for given parameters already exists!"
|
||||
)
|
||||
operationInProgress.postValue(false)
|
||||
chatRoomCreatedEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
existingChatRoom.localAddress.asStringUriOnly(),
|
||||
existingChatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
chatRoomCreatedEvent.postValue(Event(LinphoneUtils.getConversationId(existingChatRoom)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import android.content.ClipData
|
|||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import android.view.LayoutInflater
|
||||
|
|
@ -52,6 +51,7 @@ import org.linphone.utils.AppUtils
|
|||
import org.linphone.utils.ConfirmationDialogModel
|
||||
import org.linphone.utils.DialogUtils
|
||||
import org.linphone.utils.Event
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@UiThread
|
||||
class ContactFragment : SlidingPaneChildFragment() {
|
||||
|
|
@ -171,7 +171,7 @@ class ContactFragment : SlidingPaneChildFragment() {
|
|||
viewModel.openNativeContactEditor.observe(viewLifecycleOwner) {
|
||||
it.consume { uri ->
|
||||
val editIntent = Intent(Intent.ACTION_EDIT).apply {
|
||||
setDataAndType(Uri.parse(uri), ContactsContract.Contacts.CONTENT_ITEM_TYPE)
|
||||
setDataAndType(uri.toUri(), ContactsContract.Contacts.CONTENT_ITEM_TYPE)
|
||||
putExtra("finishActivityOnSaveCompleted", true)
|
||||
}
|
||||
startActivity(editIntent)
|
||||
|
|
@ -188,9 +188,9 @@ class ContactFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
|
||||
viewModel.goToConversationEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
Log.i("$TAG Going to conversation [${pair.first}][${pair.second}]")
|
||||
sharedViewModel.showConversationEvent.value = Event(pair)
|
||||
it.consume { conversationId ->
|
||||
Log.i("$TAG Going to conversation [$conversationId]")
|
||||
sharedViewModel.showConversationEvent.value = Event(conversationId)
|
||||
sharedViewModel.navigateToConversationsEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -287,7 +287,7 @@ class ContactFragment : SlidingPaneChildFragment() {
|
|||
)
|
||||
val smsIntent: Intent = Intent().apply {
|
||||
action = Intent.ACTION_SENDTO
|
||||
data = Uri.parse("smsto:$number")
|
||||
data = "smsto:$number".toUri()
|
||||
putExtra("address", number)
|
||||
putExtra("sms_body", smsBody)
|
||||
}
|
||||
|
|
@ -319,7 +319,7 @@ class ContactFragment : SlidingPaneChildFragment() {
|
|||
|
||||
private fun showConfirmTrustCallDialog(contactName: String, deviceSipUri: String) {
|
||||
val label = AppUtils.getFormattedString(
|
||||
org.linphone.R.string.contact_dialog_increase_trust_level_message,
|
||||
R.string.contact_dialog_increase_trust_level_message,
|
||||
contactName,
|
||||
deviceSipUri
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import org.linphone.core.SecurityLevel
|
|||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class ContactAvatarModel
|
||||
@WorkerThread
|
||||
|
|
@ -151,7 +152,7 @@ class ContactAvatarModel
|
|||
private fun getAvatarUri(friend: Friend): Uri? {
|
||||
val picturePath = friend.photo
|
||||
if (!picturePath.isNullOrEmpty()) {
|
||||
return Uri.parse(picturePath)
|
||||
return picturePath.toUri()
|
||||
}
|
||||
|
||||
val refKey = friend.refKey
|
||||
|
|
|
|||
|
|
@ -39,15 +39,24 @@ class ContactNumberOrAddressModel
|
|||
) {
|
||||
val selected = MutableLiveData<Boolean>()
|
||||
|
||||
private var actionDoneCallback: (() -> Unit)? = null
|
||||
|
||||
@UiThread
|
||||
fun setActionDoneCallback(lambda: () -> Unit) {
|
||||
actionDoneCallback = lambda
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun onClicked() {
|
||||
listener.onClicked(this)
|
||||
actionDoneCallback?.invoke()
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun onLongPress(): Boolean {
|
||||
selected.value = true
|
||||
listener.onLongPress(this)
|
||||
actionDoneCallback?.invoke()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ class NumberOrAddressPickerDialogModel
|
|||
val dismissEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
init {
|
||||
for (model in list) {
|
||||
model.setActionDoneCallback {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
sipAddressesAndPhoneNumbers.value = list
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
*/
|
||||
package org.linphone.ui.main.contacts.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.WorkerThread
|
||||
|
|
@ -42,6 +41,7 @@ import org.linphone.ui.GenericViewModel
|
|||
import org.linphone.ui.main.contacts.model.NewOrEditNumberOrAddressModel
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class ContactNewOrEditViewModel
|
||||
@UiThread
|
||||
|
|
@ -145,14 +145,7 @@ class ContactNewOrEditViewModel
|
|||
val organization = company.value.orEmpty().trim()
|
||||
if (fn.isEmpty() && ln.isEmpty() && organization.isEmpty()) {
|
||||
Log.e("$TAG At least a mandatory field wasn't filled, aborting save")
|
||||
showRedToastEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
R.string.contact_editor_mandatory_field_not_filled_toast,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
)
|
||||
)
|
||||
showRedToast(R.string.contact_editor_mandatory_field_not_filled_toast, R.drawable.warning_circle)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +183,7 @@ class ContactNewOrEditViewModel
|
|||
isImage = true,
|
||||
overrideExisting = true
|
||||
)
|
||||
val oldFile = Uri.parse(FileUtils.getProperFilePath(picture))
|
||||
val oldFile = FileUtils.getProperFilePath(picture).toUri()
|
||||
viewModelScope.launch {
|
||||
FileUtils.copyFile(oldFile, newFile)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,8 +117,8 @@ class ContactViewModel
|
|||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val goToConversationEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String>>>()
|
||||
val goToConversationEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val vCardTerminatedEvent: MutableLiveData<Event<Pair<String, File>>> by lazy {
|
||||
|
|
@ -198,21 +198,14 @@ class ContactViewModel
|
|||
val state = chatRoom.state
|
||||
if (state == ChatRoom.State.Instantiated) return
|
||||
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]")
|
||||
|
||||
if (state == ChatRoom.State.Created) {
|
||||
Log.i("$TAG Conversation [$id] successfully created")
|
||||
chatRoom.removeListener(this)
|
||||
operationInProgress.postValue(false)
|
||||
goToConversationEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("$TAG Conversation [$id] creation has failed!")
|
||||
chatRoom.removeListener(this)
|
||||
|
|
@ -506,20 +499,22 @@ class ContactViewModel
|
|||
params.isChatEnabled = true
|
||||
params.isGroupEnabled = false
|
||||
params.subject = AppUtils.getString(R.string.conversation_one_to_one_hidden_subject)
|
||||
params.account = account
|
||||
|
||||
val chatParams = params.chatParams ?: return
|
||||
chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
|
||||
val sameDomain = remote.domain == corePreferences.defaultDomain && remote.domain == account.params.domain
|
||||
if (account.params.instantMessagingEncryptionMandatory && sameDomain) {
|
||||
Log.i(
|
||||
"$TAG Account is in secure mode & domain matches, creating a E2E conversation"
|
||||
"$TAG Account is in secure mode & domain matches, creating an E2E encrypted conversation"
|
||||
)
|
||||
chatParams.backend = ChatRoom.Backend.FlexisipChat
|
||||
params.securityLevel = Conference.SecurityLevel.EndToEnd
|
||||
} else if (!account.params.instantMessagingEncryptionMandatory) {
|
||||
if (LinphoneUtils.isEndToEndEncryptedChatAvailable(core)) {
|
||||
Log.i(
|
||||
"$TAG Account is in interop mode but LIME is available, creating a E2E conversation"
|
||||
"$TAG Account is in interop mode but LIME is available, creating an E2E encrypted conversation"
|
||||
)
|
||||
chatParams.backend = ChatRoom.Backend.FlexisipChat
|
||||
params.securityLevel = Conference.SecurityLevel.EndToEnd
|
||||
|
|
@ -543,49 +538,33 @@ class ContactViewModel
|
|||
val existingChatRoom = core.searchChatRoom(params, localAddress, null, participants)
|
||||
if (existingChatRoom != null) {
|
||||
Log.i(
|
||||
"$TAG Found existing conversation [${LinphoneUtils.getChatRoomId(
|
||||
"$TAG Found existing conversation [${LinphoneUtils.getConversationId(
|
||||
existingChatRoom
|
||||
)}], going to it"
|
||||
)
|
||||
goToConversationEvent.postValue(
|
||||
Event(Pair(localSipUri, existingChatRoom.peerAddress.asStringUriOnly()))
|
||||
)
|
||||
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(existingChatRoom)))
|
||||
} else {
|
||||
Log.i(
|
||||
"$TAG No existing conversation between [$localSipUri] and [$remoteSipUri] was found, let's create it"
|
||||
)
|
||||
operationInProgress.postValue(true)
|
||||
val chatRoom = core.createChatRoom(params, localAddress, participants)
|
||||
val chatRoom = core.createChatRoom(params, participants)
|
||||
if (chatRoom != null) {
|
||||
if (chatParams.backend == ChatRoom.Backend.FlexisipChat) {
|
||||
if (chatRoom.state == ChatRoom.State.Created) {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG 1-1 conversation [$id] has been created")
|
||||
operationInProgress.postValue(false)
|
||||
goToConversationEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
} else {
|
||||
Log.i("$TAG Conversation isn't in Created state yet, wait for it")
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
}
|
||||
} else {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
val id = LinphoneUtils.getConversationId(chatRoom)
|
||||
Log.i("$TAG Conversation successfully created [$id]")
|
||||
operationInProgress.postValue(false)
|
||||
goToConversationEvent.postValue(
|
||||
Event(
|
||||
Pair(
|
||||
chatRoom.localAddress.asStringUriOnly(),
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
)
|
||||
)
|
||||
)
|
||||
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
|
||||
}
|
||||
} else {
|
||||
Log.e(
|
||||
|
|
|
|||
|
|
@ -242,9 +242,7 @@ class ContactsListViewModel
|
|||
coreContext.contactsManager.contactRemoved(contactModel.friend)
|
||||
contactModel.friend.remove()
|
||||
coreContext.contactsManager.notifyContactsListChanged()
|
||||
showGreenToastEvent.postValue(
|
||||
Event(Pair(R.string.contact_deleted_toast, R.drawable.warning_circle))
|
||||
)
|
||||
showGreenToast(R.string.contact_deleted_toast, R.drawable.warning_circle)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
package org.linphone.ui.main.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
|
|
@ -46,6 +45,7 @@ import org.linphone.ui.assistant.AssistantActivity
|
|||
import org.linphone.ui.main.MainActivity
|
||||
import org.linphone.ui.main.settings.fragment.AccountProfileFragmentDirections
|
||||
import org.linphone.ui.main.viewmodel.DrawerMenuViewModel
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@UiThread
|
||||
class DrawerMenuFragment : GenericMainFragment() {
|
||||
|
|
@ -146,7 +146,7 @@ class DrawerMenuFragment : GenericMainFragment() {
|
|||
viewModel.openLinkInBrowserEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { link ->
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ abstract class GenericAddressPickerFragment : GenericMainFragment() {
|
|||
coreContext.postOnCoreThread { core ->
|
||||
val friend = model.friend
|
||||
if (friend == null) {
|
||||
Log.i("$TAG Friend is null, using address [${model.address}]")
|
||||
Log.i("$TAG Friend is null, using address [${model.address.asStringUriOnly()}]")
|
||||
val fakeFriend = core.createFriend()
|
||||
fakeFriend.addAddress(model.address)
|
||||
onAddressSelected(model.address, fakeFriend)
|
||||
|
|
|
|||
|
|
@ -39,10 +39,6 @@ import org.linphone.ui.main.fragment.GenericMainFragment
|
|||
import org.linphone.ui.main.help.viewmodel.HelpViewModel
|
||||
|
||||
class DebugFragment : GenericMainFragment() {
|
||||
companion object {
|
||||
private const val TAG = "[Debug Fragment]"
|
||||
}
|
||||
|
||||
private lateinit var binding: HelpDebugFragmentBinding
|
||||
|
||||
val viewModel: HelpViewModel by navGraphViewModels(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
package org.linphone.ui.main.help.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
|
@ -36,6 +35,7 @@ import org.linphone.ui.main.fragment.GenericMainFragment
|
|||
import org.linphone.ui.main.help.viewmodel.HelpViewModel
|
||||
import org.linphone.utils.ConfirmationDialogModel
|
||||
import org.linphone.utils.DialogUtils
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@UiThread
|
||||
class HelpFragment : GenericMainFragment() {
|
||||
|
|
@ -78,7 +78,7 @@ class HelpFragment : GenericMainFragment() {
|
|||
binding.setPrivacyPolicyClickListener {
|
||||
val url = getString(R.string.website_privacy_policy_url)
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
|
|
@ -90,7 +90,7 @@ class HelpFragment : GenericMainFragment() {
|
|||
binding.setLicensesClickListener {
|
||||
val url = getString(R.string.website_open_source_licences_usage_url)
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
|
|
@ -102,7 +102,7 @@ class HelpFragment : GenericMainFragment() {
|
|||
binding.setTranslateClickListener {
|
||||
val url = getString(R.string.website_translate_weblate_url)
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
|
|
@ -157,7 +157,7 @@ class HelpFragment : GenericMainFragment() {
|
|||
model.confirmEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(browserIntent)
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
|
|
|
|||
|
|
@ -148,23 +148,20 @@ class HistoryFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
|
||||
viewModel.goToConversationEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
Log.i("$TAG Going to conversation [${pair.first}][${pair.second}]")
|
||||
sharedViewModel.showConversationEvent.value = Event(pair)
|
||||
it.consume { conversationId ->
|
||||
Log.i("$TAG Going to conversation [$conversationId]")
|
||||
sharedViewModel.showConversationEvent.value = Event(conversationId)
|
||||
sharedViewModel.navigateToConversationsEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.goToMeetingConversationEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
val localAddress = pair.first
|
||||
val remoteAddress = pair.second
|
||||
it.consume { conversationId ->
|
||||
if (findNavController().currentDestination?.id == R.id.historyFragment) {
|
||||
Log.i("$TAG Going to meeting conversation [$localAddress][$remoteAddress]")
|
||||
Log.i("$TAG Going to meeting conversation [$conversationId]")
|
||||
val action =
|
||||
HistoryFragmentDirections.actionHistoryFragmentToConferenceConversationFragment(
|
||||
localAddress,
|
||||
remoteAddress
|
||||
conversationId
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,11 @@ class StartCallFragment : GenericAddressPickerFragment() {
|
|||
|
||||
viewModel.leaveFragmentEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
goBack()
|
||||
// Post on main thread to allow for main activity to be resumed
|
||||
coreContext.postOnMainThread {
|
||||
Log.i("$TAG Going back")
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -144,8 +144,8 @@ class HistoryListViewModel
|
|||
val account = LinphoneUtils.getDefaultAccount()
|
||||
val logs = account?.callLogs ?: coreContext.core.callLogs
|
||||
for (callLog in logs) {
|
||||
if (callLog.remoteAddress.asStringUriOnly().contains(filter)) {
|
||||
val model = CallLogModel(callLog)
|
||||
val model = CallLogModel(callLog)
|
||||
if (isCallLogMatchingFilter(model, filter)) {
|
||||
list.add(model)
|
||||
count += 1
|
||||
}
|
||||
|
|
@ -158,4 +158,12 @@ class HistoryListViewModel
|
|||
Log.i("$TAG Fetched [${list.size}] call log(s)")
|
||||
callLogs.postValue(list)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun isCallLogMatchingFilter(model: CallLogModel, filter: String): Boolean {
|
||||
if (filter.isEmpty()) return true
|
||||
|
||||
val friendName = model.avatarModel.friend.name ?: LinphoneUtils.getDisplayName(model.address)
|
||||
return friendName.contains(filter, ignoreCase = true) || model.address.asStringUriOnly().contains(filter, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue