first commit
This commit is contained in:
commit
8cf155b9e9
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: How to create a proper bug report
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
First of all, please say "Hi" or "Hello", it doesn't cost much.
|
||||
|
||||
|
||||
1. **Describe the bug** (mandatory)
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
Also, if applicable, **do you reproduce it with linphone-android latest release from the Play Store?**
|
||||
|
||||
**If the issue is about the SDK (build, issue, etc...) open the ticket in the [linphone-sdk](https://github.com/BelledonneCommunications/linphone-sdk) repository or one of it's submodules!**
|
||||
|
||||
2. **To Reproduce** (mandatory)
|
||||
|
||||
Please detail steps to reproduce the behavior.
|
||||
|
||||
3. **Expected behavior** (mandatory)
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
4. **Please complete the following information** (mandatory)
|
||||
|
||||
- Device: [e.g. Samsung Note 20 Ultra]
|
||||
- OS: [e.g. Android 12]
|
||||
- Version of the App [e.g. 4.6.11]
|
||||
- Version of the SDK [e.g 5.1.48]
|
||||
- Where you did got it from (Play Store, F-Droid, local build)
|
||||
- Please tell us if your Android is a Lineage OS or another variant.
|
||||
|
||||
If you are using a SDK that isn't the latest release, please update first as it's likely your issue is already solved.
|
||||
|
||||
5. **SDK logs** (mandatory)
|
||||
|
||||
Enable debug logs in advanced section of the settings, restart the app, reproduce the issue and then go back to advanced settings, click on "Send logs" and copy/paste the link here.
|
||||
|
||||
It's also explained [in the README](https://github.com/BelledonneCommunications/linphone-android#behavior-issue).
|
||||
|
||||
In case of a call issue, please attach logs from both devices!
|
||||
|
||||
6. **Adb logcat logs** (mandatory if native crash)
|
||||
|
||||
In case of a crash of the app, please also provide the symbolized stack trace of the crash using adb logcat.
|
||||
|
||||
Here's the command for a arm64 device: `adb logcat | grep ndk-stack -sym <sdk build directory>/libs-debug/arm64-v8a/`
|
||||
|
||||
For more information, please refer to [this section of the README](https://github.com/BelledonneCommunications/linphone-android#native-crash) file.
|
||||
|
||||
7. **Screenshots** (optionnal)
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
8. **Additional context** (optionnal)
|
||||
|
||||
Add any other context about the problem here.
|
||||
|
||||
|
||||
Thank you in advance for filling bug reports properly!
|
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
*.orig
|
||||
*.rej
|
||||
.DS_Store
|
||||
.gradle
|
||||
.idea
|
||||
.settings
|
||||
adb.pid
|
||||
bc-android.keystore
|
||||
build
|
||||
*.iml
|
||||
lint.xml
|
||||
local.properties
|
||||
res/.DS_Store
|
||||
res/raw/lpconfig.xsd
|
||||
.d
|
||||
.*clang*
|
||||
**/*.iml
|
||||
**/.classpath
|
||||
**/.project
|
||||
**/*.kdev4
|
||||
**/.vscode
|
||||
res/value-hi_IN
|
||||
linphone-sdk-android/*.aar
|
||||
app/debug
|
||||
app/release
|
||||
app/releaseAppBundle
|
||||
app/releaseWithCrashlytics
|
||||
keystore.properties
|
||||
app/src/main/res/xml/contacts.xml
|
35
.gitlab-ci-files/job-android.yml
Normal file
35
.gitlab-ci-files/job-android.yml
Normal file
@ -0,0 +1,35 @@
|
||||
job-android:
|
||||
|
||||
stage: build
|
||||
tags: [ "docker-android" ]
|
||||
image: gitlab.linphone.org:4567/bc/public/linphone-android/bc-dev-android:20220609_android_33
|
||||
|
||||
before_script:
|
||||
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then eval $(ssh-agent -s); fi
|
||||
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then echo "$SCP_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null; fi
|
||||
- echo "$ANDROID_SETTINGS_GRADLE" > settings.gradle
|
||||
|
||||
script:
|
||||
- sdkmanager
|
||||
- scp -oStrictHostKeyChecking=no $DEPLOY_SERVER:$ANDROID_KEYSTORE_PATH app/
|
||||
- scp -oStrictHostKeyChecking=no $DEPLOY_SERVER:$ANDROID_GOOGLE_SERVICES_PATH app/
|
||||
- echo storePassword=$ANDROID_KEYSTORE_PASSWORD > keystore.properties
|
||||
- echo keyPassword=$ANDROID_KEYSTORE_KEY_PASSWORD >> keystore.properties
|
||||
- echo keyAlias=$ANDROID_KEYSTORE_KEY_ALIAS >> keystore.properties
|
||||
- echo storeFile=$ANDROID_KEYSTORE_FILE >> keystore.properties
|
||||
- ./gradlew app:dependencies | grep org.linphone
|
||||
- ./gradlew assembleDebug
|
||||
- ./gradlew assembleRelease
|
||||
|
||||
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
|
||||
|
||||
|
||||
.scheduled-job-android:
|
||||
extends: job-android
|
||||
only:
|
||||
- schedules
|
12
.gitlab-ci-files/job-upload.yml
Normal file
12
.gitlab-ci-files/job-upload.yml
Normal file
@ -0,0 +1,12 @@
|
||||
job-android-upload:
|
||||
|
||||
stage: deploy
|
||||
tags: [ "deploy" ]
|
||||
|
||||
only:
|
||||
- schedules
|
||||
dependencies:
|
||||
- job-android
|
||||
|
||||
script:
|
||||
- cd app/build/outputs/apk/ && rsync ./debug/*.apk $DEPLOY_SERVER:$ANDROID_DEPLOY_DIRECTORY
|
19
.gitlab-ci.yml
Normal file
19
.gitlab-ci.yml
Normal file
@ -0,0 +1,19 @@
|
||||
#################################################
|
||||
# Base configuration
|
||||
#################################################
|
||||
|
||||
|
||||
|
||||
#################################################
|
||||
# Platforms to test
|
||||
#################################################
|
||||
|
||||
|
||||
include:
|
||||
- '.gitlab-ci-files/job-android.yml'
|
||||
- '.gitlab-ci-files/job-upload.yml'
|
||||
|
||||
|
||||
stages:
|
||||
- build
|
||||
- deploy
|
11
.tx/config
Normal file
11
.tx/config
Normal file
@ -0,0 +1,11 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = fr_CA:fr-rCA,pt_BR:pt-rBR,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW,da_DK:da-rDK,sv_SE:sv-rSE,kn_IN:kn-rIN,nl_NL:nl-rNL,en_NL:en-rNL,he:iw
|
||||
minimum_perc = 1
|
||||
type = ANDROID
|
||||
|
||||
[linphone-android.stringsxml]
|
||||
file_filter = app/src/main/res/values-<lang>/strings.xml
|
||||
source_file = app/src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
|
599
CHANGELOG.md
Normal file
599
CHANGELOG.md
Normal file
@ -0,0 +1,599 @@
|
||||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
Group changes to describe their impact on the project, as follows:
|
||||
|
||||
Added for new features.
|
||||
Changed for changes in existing functionality.
|
||||
Deprecated for once-stable features removed in upcoming releases.
|
||||
Removed for deprecated features removed in this release.
|
||||
Fixed for any bug fixes.
|
||||
Security to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
## [5.0.2] - 2023-01-05
|
||||
|
||||
### Changed
|
||||
- Export files to native gallery is now available even if automatically download files setting is enabled
|
||||
|
||||
### Fixed
|
||||
- Makes sure sip.linphone.org accounts have a LIME X3DH server URL for E2E chat messages encryption
|
||||
- Files not being exported to native gallery sometimes
|
||||
- Crashes reported by Google Play Store & Crashlytics
|
||||
|
||||
## [5.0.1] - 2022-12-16
|
||||
|
||||
### Changed
|
||||
- File transfer progress indication & error status improvements
|
||||
|
||||
### Fixed
|
||||
- Wrong LIME status for participant that has multiple devices
|
||||
- No longer sends video when switching from audio only to another conference layout
|
||||
- SIP URI regex pattern to prevent HTTP URLs containing '@' to be handled as SIP URI
|
||||
|
||||
## [5.0.0] - 2022-12-06
|
||||
|
||||
### Added
|
||||
- Post Quantum encryption when using ZRTP
|
||||
- Conference creation with scheduling, video, different layouts, showing who is speaking and who is muted, etc...
|
||||
- Group calls directly from group chat rooms
|
||||
- Chat rooms can be individually muted (no notification when receiving a chat message)
|
||||
- When a message is received wait a short amount of time to check if more are to be received to notify them all at once
|
||||
- Outgoing call video in early-media if requested by callee
|
||||
- Image & Video in-app viewers allow for full-screen display
|
||||
- Display name can be set during assistant when creating / logging in a sip.linphone.org account
|
||||
- Android 13 support, using new post notifications & media permissions
|
||||
- Call recordings can be exported
|
||||
- Setting to prevent international prefix from account to be applied to call & chat
|
||||
- Themed app icon is now supported for Android 13+
|
||||
|
||||
### Changed
|
||||
- In-call views have been re-designed
|
||||
- "Media Encryption Mandatory" setting now allows for any media encryption (instead of only the one selected in the above setting previously)
|
||||
- Improved how call logs are handled to improve performances
|
||||
- Improved how contact avatars are generated
|
||||
- 3-dots menu even for basic chat rooms with more options
|
||||
- Phone numbers & email addresses are now clickable links in chat messages
|
||||
- Go to call activity when you click on launcher icon if there is at least one active call
|
||||
|
||||
### Fixed
|
||||
- Multiple file download attempt from the same chat bubble at the same time needed app restart to properly download each file
|
||||
- Call stopped when removing app from recent tasks
|
||||
- Generated avatars in dark mode
|
||||
- Call state in self-managed TelecomManager service if it takes longer to be created than the call to be answered
|
||||
- Show service notification sooner to prevent crash if Core creation takes too long
|
||||
- Incoming call screen not being showed up to user (& screen staying off) when using app in Samsung secure folder
|
||||
- One to one chat room creation process waiting indefinitely if chat room already exists
|
||||
- Contact edition (SIP addresses & phone numbers) not working due to original value being lost in Friend parsing
|
||||
- Automatically start call recording
|
||||
- "Blinking" in some views when presence is being received
|
||||
- Trying to keep the preferred driver (OpenSLES / AAudio) when switching device
|
||||
- Issues when storing presence in native contacts + potentially duplicated SIP addresses in contact details
|
||||
- Chat room scroll position lost when going into sub-view
|
||||
- Trim user input to remove any space at end of string due to keyboard auto completion
|
||||
- No longer makes requests to our LIME server (end-to-end encryption keys server) for non sip.linphone.org accounts
|
||||
- Fixed incoming call/notification not ringing if Do not Disturb mode is enabled except for favorite contacts
|
||||
|
||||
## [4.6.14] - 2022-09-19
|
||||
|
||||
### Fixed
|
||||
- ANR that happens sometimes when playing voice recording
|
||||
|
||||
### Changed
|
||||
- Improved contact loader by querying only relevant fields
|
||||
|
||||
## [4.6.13] - 2022-08-25
|
||||
|
||||
### Fixed
|
||||
- Disable Telecom Manager feature on Android < 10 to prevent crash due to Android 9 OS bug
|
||||
- Fixed crash due to AAudio's waitForStateChange (SDK fix)
|
||||
|
||||
## [4.6.12] - 2022-07-29
|
||||
|
||||
### Fixed
|
||||
- Call notification not being removed if service channel is disabled & background mode is enabled
|
||||
- Wrong display name in chat notification sometimes
|
||||
- Removed secure chat button if no LIME server configured or no conference factory URI set
|
||||
- Disable TelecomManager feature when the device doesn't support it
|
||||
|
||||
### Changed
|
||||
- ContactsLoader have been updated, shouldn't crash anymore
|
||||
|
||||
## [4.6.11] - 2022-06-27
|
||||
|
||||
### Fixed
|
||||
- Various crashes due to unhandled exceptions
|
||||
- Echo canceller calibration not using speaker (SDK fix)
|
||||
|
||||
## [4.6.10] - 2022-06-07
|
||||
|
||||
### Fixed
|
||||
- Fixed contact address used instead of identity address when creating a basic chat room from history or contact details
|
||||
- Fixed call notification still visible after call ended on some devices
|
||||
- Fixed incoming call activity not displayed on some devices
|
||||
- Fixed Malaysian dial plan (SDK fix)
|
||||
- Fixed incoming call ringing even if Do not disturb mode is enabled (SDK fix)
|
||||
|
||||
## [4.6.9] - 2022-05-30
|
||||
|
||||
### Fixed
|
||||
- ANR when screen turns OFF/ON while app is in foreground
|
||||
- Crash due to missing CoreContext instance in TelecomManager service
|
||||
- One-to-One encrypted chat room creation if it already exists
|
||||
- Crash if ConnectionService feature isn't supported by the device
|
||||
|
||||
### Changed
|
||||
- Updated translations from Weblate
|
||||
- Improved audio devices logs
|
||||
|
||||
## [4.6.8] - 2022-05-23
|
||||
|
||||
### Fixed
|
||||
- Crash due to missing CoreContext in CoreService
|
||||
- Crash in BootReceiver if auto start is disabled
|
||||
- Other crashes
|
||||
|
||||
## [4.6.7] - 2022-05-04
|
||||
|
||||
### Changed
|
||||
- Do not start Core in Application, prevents service notification from appearing by itself
|
||||
- When switching from bluetooth or headset device to earpiece/speaker, also change microphone
|
||||
- Prevent empty chat bubble by sending only space character(s)
|
||||
|
||||
### Fixed
|
||||
- Phone numbers with non-ASCII labels missing from address book
|
||||
- Wrong audio device displayed in call statistics
|
||||
- Various issues from Crashlytics
|
||||
|
||||
## [4.6.6] - 2022-04-26
|
||||
|
||||
### Changed
|
||||
- Prevent requests to LIME X3DH & long term presence servers when not using a sip.linphone.org account
|
||||
- Updated DE & RU translations
|
||||
- Improved UI on landscape tablets
|
||||
|
||||
### Fixed
|
||||
- Catching exceptions in new ContactsLoader reported on PlayStore
|
||||
- Missing phone numbers in contacts when label contains a space character (5.1.24 SDK fix)
|
||||
- Prevent app from starting by itself due to DummySyncService
|
||||
- Hide chat rooms settings not working properly
|
||||
|
||||
## [4.6.5] - 2022-04-11
|
||||
|
||||
### Changed
|
||||
- Only display phone number if it matches SIP address username
|
||||
- Using new MagicSearch API to improve contacts list performances
|
||||
|
||||
### Fixed
|
||||
- Prevent concurrent exception while loading native address book contacts
|
||||
|
||||
## [4.6.4] - 2022-04-06
|
||||
|
||||
### Added
|
||||
- Set video information in CallStyle incoming call notification
|
||||
|
||||
### Changed
|
||||
- Massive rework of how native contacts from address book are handled to improve performances
|
||||
- Only display phone number from LDAP search result if it matches SIP address' username
|
||||
|
||||
### Fixed
|
||||
- Do not use CallStyle notification on Samsung devices, they are currently displayed badly
|
||||
- Fixed microphone muted when starting a new call if microphone was muted at the end of the previous one
|
||||
- Added LDAP contact display name to SIP address
|
||||
- Prevent read-only 1-1 chat room
|
||||
- Fixed chat room last updated time not updated sometimes
|
||||
|
||||
## [4.6.3] - 2022-03-08
|
||||
|
||||
### Added
|
||||
- Improvements in contacts matching
|
||||
|
||||
### Changed
|
||||
- "Operation in progress" spinner hidden when contacts display/filter takes less than 200ms
|
||||
|
||||
### Fixed
|
||||
- Contacts order when multiple address book contacts share the same number / SIP address
|
||||
- Wrongly formatted phone numbers not displayed anymore
|
||||
- Incoming call activity not displayed on LineageOS sometimes
|
||||
- Various crashes related to Telecom Manager exceptions not being caught
|
||||
|
||||
## [4.6.2] - 2022-03-01
|
||||
|
||||
### Added
|
||||
- Request BLUETOOTH_CONNECT permission on Android 12+ devices, if not we won't be notified when a BT device is being connected/disconnected while app is alive.
|
||||
- LDAP settings if SDK is built with OpenLDAP (requires 5.1.1 or higher linphone-sdk), will add contacts if any
|
||||
- SIP addresses & phone numbers can be selected in history & contact details view
|
||||
- Text can be selected in file viewer & config viewer
|
||||
- Prevent screen to turn off while recording a voice message
|
||||
|
||||
### Changed
|
||||
- Contacts lists now show LDAP contacts if any
|
||||
|
||||
### Fixed
|
||||
- Negative gain in audio settings is allowed again
|
||||
- STUN server URL setting not enabling it for non sip.linphone.org accounts
|
||||
- Contacts list header case comparison
|
||||
- Stop voice recording playback when sending chat message
|
||||
- Call activity not finishing when hanging up sometimes
|
||||
- Auto start setting disabled not working if background mode setting was enabled
|
||||
|
||||
## [4.6.1] - 2022-02-14
|
||||
|
||||
### Fixed
|
||||
- Quit button not working when background mode was enabled
|
||||
- Crash when background mode was enabled and service notification channel was disabled
|
||||
- Crashes while changing audio route
|
||||
- Crash while fetching contacts
|
||||
- Crash when rotating the device (SDK fix)
|
||||
|
||||
## [4.6.0] - 2022-02-09
|
||||
|
||||
### Added
|
||||
- Reply to chat message feature (with original message preview)
|
||||
- Swipe action on chat messages to reply / delete
|
||||
- Voice recordings in chat feature
|
||||
- Allow video recording in chat file sharing
|
||||
- Unread messages indicator in chat conversation that separates read & unread messages
|
||||
- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API (disables SDK audio focus)
|
||||
- Ask Android to not process what user types in an encrypted chat room to improve privacy, see [IME_FLAG_NO_PERSONALIZED_LEARNING](https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING)
|
||||
- SIP URIs in chat messages are clickable to easily initiate a call
|
||||
- New video call UI on foldable device like Galaxy Z Fold
|
||||
- Setting to automatically record all calls
|
||||
- When using a physical keyboard, use left control + enter keys to send message
|
||||
- Using CallStyle notifications for calls for devices running Android 12 or newer
|
||||
- New fragment explaining generic SIP account limitations contrary to sip.linphone.org SIP accounts
|
||||
- Link to Weblate added in about page
|
||||
|
||||
### Changed
|
||||
- UI has been reworked around SlidingPane component to better handle tablets & foldable devices
|
||||
- No longer scroll to bottom of chat room when new messages are received, a new button shows up to do it and it displays conversation's unread messages count
|
||||
- Animations have been replaced to use com.google.android.material.transition ones
|
||||
- Using new [Unified Content API](https://developer.android.com/about/versions/12/features/unified-content-api) to share files from keyboard (or other sources)
|
||||
- Received messages are now trimmed
|
||||
- Bumped dependencies, gradle updated from 4.2.2 to 7.0.2
|
||||
- Target Android SDK version set to 31 (Android 12)
|
||||
- Splashscreen is using new APIs
|
||||
- SDK updated to 5.1.0 release
|
||||
- Updated translations
|
||||
|
||||
### Fixed
|
||||
- Chat notifications disappearing when app restarts
|
||||
- "Infinite backstack", now each view is stored (at most) once in the backstack
|
||||
- Voice messages / call recordings will be played on headset/headphones instead of speaker, if possible
|
||||
- Going back to the dialer when pressing back in a chat room after clicking on a chat message notification
|
||||
- Missing international prefix / phone number in assistant after granting permission
|
||||
- Display issue for incoming call notification preventing to use answer/hangup actions on some Xiaomi devices (like Redmi Note 9S)
|
||||
- Missing foreground service notification for background mode
|
||||
|
||||
### Removed
|
||||
- Launcher Activity has been replaced by [Splash Screen API](https://developer.android.com/reference/kotlin/androidx/core/splashscreen/SplashScreen)
|
||||
- Dialer will no longer make DTMF sound when pressing digits
|
||||
- Launcher activity
|
||||
- Global push notification setting in Network, use the switch in each Account instead
|
||||
- No longer need to monitor device rotation and give information to the Core, it does it by itself
|
||||
|
||||
## [4.5.6] - 2021-11-08
|
||||
|
||||
### Changed
|
||||
- SDK updated to 5.0.49
|
||||
|
||||
## [4.5.5] - 2021-10-28
|
||||
|
||||
### Changed
|
||||
- SDK updated to 5.0.45
|
||||
|
||||
## [4.5.4] - 2021-10-19
|
||||
|
||||
### Changed
|
||||
- SDK updated to 5.0.38
|
||||
|
||||
### Fixed
|
||||
- Side menu not showing the newly configured account until next start
|
||||
|
||||
## [4.5.3] - 2021-10-04
|
||||
|
||||
### Added
|
||||
- Russian translation
|
||||
|
||||
### Changed
|
||||
- SDK updated to 5.0.31
|
||||
|
||||
### Fixed
|
||||
- AccountSettingsViewModel leak causing number of REGISTER to grow
|
||||
|
||||
## [4.5.2] - 2021-08-27
|
||||
|
||||
### Added
|
||||
- Added a contact cache at app level
|
||||
- Glide cache cleared on low memory
|
||||
|
||||
### Changed
|
||||
- Fixed encrypted file export when VFS is enabled
|
||||
- Fixed in-app video player size when VFS is enabled
|
||||
- Fixed background mode setting
|
||||
- Fixed proximity sensor during calls
|
||||
- Fixed missing notification for missed call when call history view is active
|
||||
- Fixed shortcuts on launcher
|
||||
- Fixed a few memory leaks
|
||||
- Fixed various crashes & other issues
|
||||
- SDK bumped to 5.0.10
|
||||
|
||||
## [4.5.1] - 2021-07-15
|
||||
|
||||
### Changed
|
||||
- Bugs & crashes have been fixed
|
||||
- SDK bumped to 5.0.1
|
||||
|
||||
## [4.5.0] - 2021-07-08
|
||||
|
||||
This version is a full rewrite of the app in kotlin, using modern Android components like navigation, viewmodel, databinding, coroutines, etc...
|
||||
|
||||
### Added
|
||||
|
||||
- Using linphone SDK 5.0 API to better handle audio route (see linphone-sdk changelog)
|
||||
- All files used by the app can now be encrypted for more security (VFS setting)
|
||||
- In-app file viewers for PDFs, images, videos, sounds and texts
|
||||
- Ephemeral messages
|
||||
- Messages can be forwarded between chat rooms
|
||||
- Numpad can be displayed in outgoing call view if the call has early media
|
||||
- Can display multiple files in the same chat bubble
|
||||
- Display video in recordings if available
|
||||
- "Swipe left to delete" action available on calls history, contacts & chat rooms list
|
||||
- "Swipe right" to mark a chat room as read
|
||||
- Android 11 people & conversation compliant
|
||||
- New animations between fragments and for unread chat messages / missed calls counters (can be disabled)
|
||||
- Bubble & conversation support for chat message notifications
|
||||
- Direct share support for chat room shortcuts
|
||||
- Option to mark messages as read when dismissing the notification
|
||||
- More settings are available
|
||||
- Call view can be displayed in full-screen
|
||||
- Display phone number label (home, work, etc...) in contacts' details
|
||||
|
||||
### Changed
|
||||
|
||||
- Call history view groups call from the same SIP URI (like linphone-iphone)
|
||||
- Reworked conference (using new linphone-sdk APIs)
|
||||
- Route audio to headset / headphones / bluetooth device automatically when available
|
||||
- Send logs / Reset logs buttons moved from About page to Advanced Settings like iOS
|
||||
- Improved how Android native contacts are used
|
||||
- Switched to material design for text input fields & switches
|
||||
- Launcher shortcuts can be to either contacts or chat rooms
|
||||
- Improved preview when sharing video files through the chat
|
||||
- UI changes
|
||||
|
||||
### Removed
|
||||
|
||||
- "back-to-call" button from dialer & chat views, use notification or overlay (see call settings for in-app/system-wide overlay)
|
||||
- Don't ask for "Do not disturb settings" permission anymore
|
||||
- Previous translations, starting again from scratch using Weblate instead of Transifex
|
||||
|
||||
### [4.4.0] - 2021-03-29
|
||||
|
||||
### Added
|
||||
- Dedicated notification channel for missed calls
|
||||
|
||||
### Changed
|
||||
- SDK updated to 4.5.0
|
||||
- Min Android version updated from 21 to 23 (Android 6) due to SDK audio routes feature
|
||||
- Rely on SDK audio routes feature instead of doing it in the application
|
||||
- User can now check incoming messages delivery status in group chat rooms
|
||||
- Asking user to read and accept privacy policy and general terms
|
||||
- Updated translations
|
||||
- Various crashes & issues fixed
|
||||
|
||||
## [4.3.1] - 2020-09-25
|
||||
|
||||
### Fixed
|
||||
- Added phoneCall foregroundServiceType for Android Q and newer
|
||||
- Contact sorting when first character has an accent
|
||||
|
||||
### Changed
|
||||
- SDK updated to 4.4.2
|
||||
- Updated translations
|
||||
|
||||
## [4.3.0] - 2020-06-23
|
||||
|
||||
### Added
|
||||
- Forward message between chat rooms
|
||||
|
||||
### Changed
|
||||
- Files from chat messages are now stored in a private space and will be deleted when the message or room will be deleted
|
||||
- SDK updated to 4.4 version
|
||||
- Fixed ANRs
|
||||
- Fixed various issues
|
||||
|
||||
## [4.2.3] - 2020-03-03
|
||||
|
||||
### Changed
|
||||
- Fixed various crashes
|
||||
- Updated SDK to 4.3.3
|
||||
|
||||
## [4.2.2] - 2020-02-24
|
||||
|
||||
### Changed
|
||||
- Fixed various issues
|
||||
- Updated SDK to 4.3.1
|
||||
- Removed AAudio plugin for now (we have observed quality issues on some popular devices with their latest updates)
|
||||
|
||||
## [4.2.1] - 2020-01-13
|
||||
|
||||
### Changed
|
||||
- Fixed various issues
|
||||
|
||||
## [4.2.0] - 2019-12-09
|
||||
|
||||
### Added
|
||||
- Added shortcuts to contacts' latest chat rooms
|
||||
- Improved device's do not disturb policy compliance
|
||||
- Added sample application to help developpers getting started with our SDK
|
||||
- Added picture in picture feature if supported instead of video overlay
|
||||
- Added camera preview as dialer's background on tablets
|
||||
- Contact section in the settings
|
||||
- Using new AAudio & Camera2 frameworks for better performances (if available)
|
||||
- Android 10 compatibility
|
||||
- New plugin loader to be compatible with app bundle distribution mode
|
||||
- Restart service if foreground service setting is on when app is updated
|
||||
- Change bluetooth volume while in call if BT device connected and used
|
||||
|
||||
### Changed
|
||||
- Improved performances to reduce startup time
|
||||
- Call statistics are now available for each call & conference
|
||||
- Added our own devices in LIME encrypted chatrooms' security view
|
||||
- No longer display incoming call activity from Service, instead use incoming call notification with full screen intent
|
||||
- Improved reply notification when replying to a chat message from the notification
|
||||
- License changed from GPLv2 to GPLv3
|
||||
- Switched from MD5 to SHA-256 as password protection algorithm
|
||||
|
||||
## [4.1.0] - 2019-05-03
|
||||
|
||||
### Added
|
||||
- End-to-end encryption for instant messaging, for both one-to-one and group conversations.
|
||||
- Video H.265 codec support, based on android MediaCodec.
|
||||
- Enhanced call and IM notifications, so that it is possible to answer, decline, reply or mark as read directly from them.
|
||||
- Setting to request attachments to be automatically downloaded, unconditionnally or based on their size.
|
||||
- Possibility to send multiple attachments (images, documents) in a same message.
|
||||
- Possibility to share multiple images through Linphone from an external application (ex: photo app)
|
||||
- Rich input from keyboard (images, animated gifs...) when composing messages.
|
||||
- Rendering of animated gifs in conversations.
|
||||
- Button to invite contacts to use Linphone by sending them a SMS.
|
||||
- Possibility to record calls (audio only), and replay them from the "Recordings" menu.
|
||||
- Remote provisioning from a QR code providing the http(s) url of a provisioning server.
|
||||
- Option for a dark theme
|
||||
|
||||
### Changed
|
||||
- Compilation procedure is simplified: a binary SDK containing dependencies (liblinphone) is retrieved automatically from a Maven repository.
|
||||
Full compilation remains absolutely supported. Please check local README.md for more details.
|
||||
- Updated translations, mainly French and English.
|
||||
- Call history view shows last calls for a given contact.
|
||||
- Improved ergonomy of answer/decline buttons, including accessibility support.
|
||||
- Enhanced user interface, including new icons, cleanups of unused graphical resources.
|
||||
- Contact view is faster thanks to an asynchronous fetching.
|
||||
- Adaptive icon for Android 8+.
|
||||
- Video overlay now also shows local view.
|
||||
- Reworked settings view, cleanup of useless settings.
|
||||
- About section links to full GPLv2 license text.
|
||||
|
||||
### Deprecated
|
||||
- The video rendering method based on GL2JNIView is deprecated in favour of TextureView, which is easier to use.
|
||||
Please read [this article](https://wiki.linphone.org/xwiki/wiki/public/view/Lib/Features/Android%20TextureView%20Display/) for more information.
|
||||
|
||||
### Fixed
|
||||
- One to one text conversations mixed up when initiated from differents SIP accounts.
|
||||
|
||||
|
||||
## [4.0.1] - 2018-06-26
|
||||
|
||||
### Fixed
|
||||
- fix loading of plugins
|
||||
- fix issue with video stream, not started when receiving an incoming call just after the app is launched
|
||||
- fix issue with TURN
|
||||
|
||||
## [4.0.0] - 2018-06-15
|
||||
|
||||
### Added
|
||||
- Group chat between linphone.org SIP accounts.
|
||||
- new JAVA/JNI wrapper. This new wrapper is automatically generated from liblinphone C API. It breaks compatibility with previous, hand-made wrapper.
|
||||
(more information about new wrapper [here.](https://wiki.linphone.org/xwiki/wiki/public/view/Lib/Linphone%20%28Android%29%20Java%20wrapper/) )
|
||||
|
||||
### Deprecated
|
||||
- hand-made java API in submodules/linphone/java is deprecated. However it is still possible to use it by checking out the 3.4.x branch of linphone-android.
|
||||
|
||||
### Fixed
|
||||
- issue with changing push notification token not passed to library, possibly resulting in a loss of incoming calls.
|
||||
|
||||
## [3.3.0] - 2017-10-18
|
||||
|
||||
### Added
|
||||
- Integration with Android O
|
||||
- New video adaptive bitrate algorithm(More informations [here](https://wiki.linphone.org/xwiki/wiki/public/view/FAQ/How%20does%20adaptive%20bitrate%20algorithm%20work%20%3F/))
|
||||
|
||||
### Changed
|
||||
- Application is no more managing in-call wakelock, it's now managed by the library
|
||||
|
||||
### Fixed
|
||||
- Crashs in new chat view
|
||||
- Contacts management
|
||||
- Random crash in chatroom
|
||||
- Improve chats list loading time
|
||||
|
||||
## [3.2.7] - 2017-05-15
|
||||
|
||||
### Fixed
|
||||
- Crash with devices X86 on Android < 5
|
||||
|
||||
## [3.2.6] - 2017-04-10
|
||||
|
||||
### Added
|
||||
- Notification of message reading on chat
|
||||
- New permission to kill linphone app
|
||||
|
||||
### Fixed
|
||||
- Crash with firebase push
|
||||
- Problems with contacts
|
||||
|
||||
## [3.2.5] - 2017-03-06
|
||||
|
||||
### Added
|
||||
- Doze mode(energy saving) button in Network settings
|
||||
|
||||
### Changed
|
||||
- Migrate Linphone build from ANT to gradle
|
||||
- No pause VOIP Call on incoming GSM call until we off hook this
|
||||
- Subscription friends list enabled by default only for linphone domain
|
||||
|
||||
### Fixed
|
||||
- Rotation after screen locking
|
||||
- Contacts background task
|
||||
- No more asking phone number for non-linphone domain
|
||||
- Bug with Linphone credential login
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Lime integration
|
||||
|
||||
## [3.2.4] - 2017-01-19
|
||||
|
||||
### Fixed
|
||||
- Some crashs
|
||||
- Some UI bugs
|
||||
|
||||
## [3.2.3] - 2017-01-11
|
||||
|
||||
### Fixed
|
||||
- Somes crashs
|
||||
|
||||
### Changed
|
||||
- Improved performance of contacts loading
|
||||
|
||||
## [3.2.2] - 2017-01-04
|
||||
|
||||
### Fixed
|
||||
- Some bug with the download of OpenH264 for Android < 5.1
|
||||
- Some crashs
|
||||
|
||||
### Changed
|
||||
- Disable AAC codecs
|
||||
|
||||
## [3.2.1] - 2016-11-24
|
||||
|
||||
### Added
|
||||
- Open H264 binary download for ARM Android < 5.1
|
||||
|
||||
### Fixed
|
||||
- Crashes for x86 CPU at starting
|
||||
- Crashes in somes view in cause of bad locale time
|
||||
- Crashes in contacts view if we don't have permission
|
||||
|
||||
## [3.2.0] - 2016-11-10
|
||||
|
||||
### Added
|
||||
- Change your password in your account settings
|
||||
|
||||
### Changed
|
||||
- Media H264 support improved for Android >= 5.1
|
||||
- Optimize memory footprint and performance of contacts list an IM view
|
||||
|
||||
### Fixed
|
||||
- Crashes Android 6/7 at starting
|
||||
- Permissions issues
|
||||
- Layout of tablet views
|
674
LICENSE.txt
Normal file
674
LICENSE.txt
Normal file
@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
179
README.md
Normal file
179
README.md
Normal file
@ -0,0 +1,179 @@
|
||||
|
||||
[![pipeline status](https://gitlab.linphone.org/BC/public/linphone-android/badges/master/pipeline.svg)](https://gitlab.linphone.org/BC/public/linphone-android/commits/master) [![weblate status](https://weblate.linphone.org/widgets/linphone/-/linphone-android/svg-badge.svg)](https://weblate.linphone.org/engage/linphone/?utm_source=widget)
|
||||
|
||||
Linphone is an open source softphone for voice and video over IP calling and instant messaging.
|
||||
|
||||
It is fully SIP-based, for all calling, presence and IM features.
|
||||
|
||||
General description is available from [linphone web site](https://www.linphone.org/technical-corner/linphone).
|
||||
|
||||
### How to get it
|
||||
|
||||
[<img src="metadata/google-play-badge.png" height="60" alt="Get it on Google Play">](https://play.google.com/store/apps/details?id=org.linphone)[<img src="metadata/f-droid-badge.png" height="60" alt="Get it on F-Droid">](https://f-droid.org/en/packages/org.linphone/)
|
||||
|
||||
You can also download APKs signed with our key from [our website](https://download.linphone.org/releases/android/?C=M;O=D).
|
||||
|
||||
### License
|
||||
|
||||
Copyright © Belledonne Communications
|
||||
|
||||
Linphone is dual licensed, and is available either :
|
||||
|
||||
- under a [GNU/GPLv3 license](https://www.gnu.org/licenses/gpl-3.0.en.html), for free (open source). Please make sure that you understand and agree with the terms of this license before using it (see LICENSE file for details).
|
||||
|
||||
- under a proprietary license, for a fee, to be used in closed source applications. Contact [Belledonne Communications](https://www.linphone.org/contact) for any question about costs and services.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Supported features and RFCs : https://www.linphone.org/technical-corner/linphone/features
|
||||
|
||||
- Linphone public wiki : https://wiki.linphone.org/xwiki/wiki/public/view/Linphone/
|
||||
|
||||
- Tutorials : https://gitlab.linphone.org/BC/public/tutorials/-/tree/master/android/kotlin
|
||||
|
||||
# What's new
|
||||
|
||||
App has been totally rewritten in Kotlin using modern components such as Navigation, Data Binding, View Models, coroutines, etc...
|
||||
Check the [CHANGELOG](./CHANGELOG.md) file for a more detailed list.
|
||||
The first linphone-android release that will be based on this will be 4.5.0, using 5.0.0 SDK.
|
||||
|
||||
We're also taking a fresh start regarding translations so less languages will be available for a while.
|
||||
If you want to contribute, you are welcome to do so, check the [Translations](#Translations) section below.
|
||||
|
||||
org.linphone.legacy flavor (old java wrapper if you didn't migrate your app code to the new one yet) is no longer supported starting 5.0.0 SDK.
|
||||
|
||||
The sample project has been removed, we now recommend you to take a look at our [tutorials](https://gitlab.linphone.org/BC/public/tutorials/-/tree/master/android/kotlin).
|
||||
|
||||
# Building the app
|
||||
|
||||
If you have Android Studio, simply open the project, wait for the gradle synchronization and then build/install the app.
|
||||
It will download the linphone library from our Maven repository as an AAR file so you don't have to build anything yourself.
|
||||
|
||||
If you don't have Android Studio, you can build and install the app using gradle:
|
||||
```
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
will compile the APK file (assembleRelease to instead if you want to build a release package), and then
|
||||
```
|
||||
./gradlew installDebug
|
||||
```
|
||||
to install the generated APK in the previous step (use installRelease instead if you built a release package).
|
||||
|
||||
APK files are stored within ```./app/build/outputs/apk/debug/``` and ```./app/build/outputs/apk/release/``` directories.
|
||||
|
||||
When building a release AppBundle, use releaseAppBundle target instead of release.
|
||||
Also make sure you have a NDK installed and that you have an environment variable named ```ANDROID_NDK_HOME``` that contains the path to the NDK.
|
||||
This is to be able to include native libraries symbols into app bundle for the Play Store.
|
||||
|
||||
## Building a local SDK
|
||||
|
||||
1. Clone the linphone-sdk repository from out gitlab:
|
||||
```
|
||||
git clone https://gitlab.linphone.org/BC/public/linphone-sdk.git --recursive
|
||||
```
|
||||
|
||||
2. Follow the instructions in the linphone-sdk/README file to build the SDK.
|
||||
|
||||
3. Create or edit the gradle.properties file in $GRADLE_USER_HOME (usually ~/.gradle/) and add the absolute path to your linphone-sdk build directory, for example:
|
||||
```
|
||||
LinphoneSdkBuildDir=/home/<username>/linphone-sdk/build/
|
||||
```
|
||||
|
||||
4. Rebuild the app in Android Studio.
|
||||
|
||||
## Native debugging
|
||||
|
||||
1. Install LLDB from SDK Tools in Android-studio.
|
||||
|
||||
2. In Android-studio go to Run->Edit Configurations->Debugger.
|
||||
|
||||
3. Select 'Dual' or 'Native' and add the path to linphone-sdk debug libraries (build/libs-debug/ for example).
|
||||
|
||||
4. Open native file and put your breakpoint on it.
|
||||
|
||||
5. Make sure you are using the debug AAR in the app/build.gradle script and not the release one (to have faster builds by default the release AAR is used even for debug APK flavor).
|
||||
|
||||
6. Debug app.
|
||||
|
||||
## Known issues
|
||||
|
||||
- If you encounter the `couldn't find "libc++_shared.so"` crash when the app starts, simply clean the project in Android Studio (under Build menu) and build again.
|
||||
Also check you have built the SDK for the right CPU architecture using the `-DLINPHONESDK_ANDROID_ARCHS=armv7,arm64,x86,x86_64` cmake parameter.
|
||||
|
||||
- Push notification might not work when app has been started by Android Studio consecutively to an install. Remove the app from the recent activity view and start it again using the launcher icon to resolve this.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Behavior issue
|
||||
|
||||
When submitting an issue on our [Github repository](https://github.com/BelledonneCommunications/linphone-android), please follow the template and attach the matching library logs:
|
||||
|
||||
1. To enable them, go to Settings -> Advanced and toggle `Debug Mode`. If they are already enabled, clear them first using the `Reset logs` button on the About page.
|
||||
|
||||
2. Then restart the app, reproduce the issue and upload the logs using the `Send logs` button on the advanced settings page.
|
||||
|
||||
3. Finally paste the link to the uploaded logs (link is already in the clipboard after a successful upload).
|
||||
|
||||
### Native crash
|
||||
|
||||
First of all, to be able to get a symbolized stack trace, you need the debug version of our libraries.
|
||||
|
||||
If you haven't built the SDK locally (see [building a local SDK](#BuildingalocalSDK)), here's how to get them:
|
||||
|
||||
1. Go to our [maven repository](https://download.linphone.org/maven_repository/org/linphone/linphone-sdk-android-debug/), in the linphone-android-debug directory.
|
||||
|
||||
2. Download the AAR file with **the exact same version** as the AAR that was used to generate the crash's stacktrace.
|
||||
|
||||
3. Extract the AAR somewhere on your computer (it's a simple ZIP file even it's doesn't have the extension). Libraries are stored inside the ```jni``` folder (a directory for each architectured built, usually ```arm64-v8a, armeabi-v7a, x86_64 and x86```).
|
||||
|
||||
4. To get consistent with locally built SDK, rename the ```jni``` directory into ```libs-debug```.
|
||||
|
||||
Now you need the ```ndk-stack``` tool and possibly ```adb logcat```.
|
||||
|
||||
If your computer isn't used for Android development, you can download those tools from [Google website](https://developer.android.com/studio#downloads), in the ```Command line tools only``` section.
|
||||
|
||||
Once you have the debug libraries and the proper tools installed, you can use the ```ndk-stack``` tool to symbolize your stacktrace. Note that you also need to know the architecture (armv7, arm64, x86, etc...) of the libraries that were used.
|
||||
|
||||
If you know the CPU architecture of your device (most probably arm64 if it's a recent device) you can use the following to get the stacktrace from a device plugged to a computer:
|
||||
```
|
||||
adb logcat -d | ndk-stack -sym ./libs-debug/arm64-v8a/
|
||||
```
|
||||
If you don't know the CPU architecture, use the following instead:
|
||||
```
|
||||
adb logcat -d | ndk-stack -sym ./libs-debug/`adb shell getprop ro.product.cpu.abi | tr -d '\r'`
|
||||
```
|
||||
Warning: This command won't print anything until you reproduce the crash!
|
||||
|
||||
## Create an APK with a different package name
|
||||
|
||||
Before the 4.1 release, there were a lot of files to edit to change the package name.
|
||||
Now, simply edit the app/build.gradle file and change the value returned by method ```getPackageName()```
|
||||
The next build will automatically use this value everywhere thanks to ```manifestPlaceholders``` feature of gradle and Android.
|
||||
|
||||
You may have already noticed that the app installed by Android Studio has ```org.linphone.debug``` package name.
|
||||
If you build the app as release, the package name will be ```org.linphone```.
|
||||
|
||||
## Firebase push notifications
|
||||
|
||||
Now that Google Cloud Messaging has been deprecated and will be completely removed on April 11th 2019, the only official way of using push notifications is through Firebase.
|
||||
|
||||
However to make Firebase push notifications work, the project needs to have a ```app/google-services.json``` file that contains the configuration.
|
||||
We have archived our own, so you can build your linphone-android application and still receive push notifications from our free SIP service (sip.linphone.org).
|
||||
If you delete it, you won't receive any push notification.
|
||||
|
||||
If you have your own push server, replace this file by yours.
|
||||
|
||||
## Translations
|
||||
|
||||
We no longer use transifex for the translation process, instead we have deployed our own instance of [Weblate](https://weblate.linphone.org/).
|
||||
|
||||
Due to the full app rewrite we can't re-use previous translations, so we'll be very happy if you want to contribute.
|
||||
|
||||
# CONTRIBUTIONS
|
||||
|
||||
In order to submit a patch for inclusion in linphone's source code:
|
||||
|
||||
1. First make sure your patch applies to latest git sources before submitting: patches made to old versions can't and won't be merged.
|
||||
2. Fill out and send us an email with the link of pull-request and the [Contributor Agreement](https://linphone.org/sites/default/files/bc-contributor-agreement_0.pdf) for your patch to be included in the git tree.
|
||||
|
||||
The goal of this agreement to grant us peaceful exercise of our rights on the linphone source code, while not losing your rights on your contribution.
|
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
275
app/build.gradle
Normal file
275
app/build.gradle
Normal file
@ -0,0 +1,275 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'org.jlleitschuh.gradle.ktlint'
|
||||
}
|
||||
|
||||
def appVersionName = "5.0.2"
|
||||
def appVersionCode = 50002
|
||||
|
||||
static def getPackageName() {
|
||||
return "org.linphone"
|
||||
}
|
||||
|
||||
def firebaseEnabled = new File(projectDir.absolutePath +'/google-services.json').exists()
|
||||
|
||||
def crashlyticsEnabled = new File(projectDir.absolutePath +'/google-services.json').exists() && new File(LinphoneSdkBuildDir + '/libs/').exists() && new File(LinphoneSdkBuildDir + '/libs-debug/').exists()
|
||||
|
||||
if (firebaseEnabled) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
|
||||
if (crashlyticsEnabled) {
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
}
|
||||
|
||||
def gitBranch = new ByteArrayOutputStream()
|
||||
task getGitVersion() {
|
||||
def gitVersion = appVersionName
|
||||
def gitVersionStream = new ByteArrayOutputStream()
|
||||
def gitCommitsCount = new ByteArrayOutputStream()
|
||||
def gitCommitHash = new ByteArrayOutputStream()
|
||||
|
||||
try {
|
||||
exec {
|
||||
executable "git" args "describe", "--abbrev=0"
|
||||
standardOutput = gitVersionStream
|
||||
}
|
||||
exec {
|
||||
executable "git" args "rev-list", gitVersionStream.toString().trim() + "..HEAD", "--count"
|
||||
standardOutput = gitCommitsCount
|
||||
}
|
||||
exec {
|
||||
executable "git" args "rev-parse", "--short", "HEAD"
|
||||
standardOutput = gitCommitHash
|
||||
}
|
||||
exec {
|
||||
executable "git" args "name-rev", "--name-only", "HEAD"
|
||||
standardOutput = gitBranch
|
||||
}
|
||||
|
||||
if (gitCommitsCount.toString().toInteger() == 0) {
|
||||
gitVersion = gitVersionStream.toString().trim()
|
||||
} else {
|
||||
gitVersion = gitVersionStream.toString().trim() + "." + gitCommitsCount.toString().trim() + "+" + gitCommitHash.toString().trim()
|
||||
}
|
||||
println("Git version: " + gitVersion + " (" + appVersionCode + ")")
|
||||
} catch (ignored) {
|
||||
println("Git not found, using " + gitVersion + " (" + appVersionCode + ")")
|
||||
}
|
||||
project.version = gitVersion
|
||||
}
|
||||
|
||||
configurations {
|
||||
customImplementation.extendsFrom implementation
|
||||
}
|
||||
|
||||
task linphoneSdkSource() {
|
||||
doLast {
|
||||
configurations.customImplementation.getIncoming().each {
|
||||
it.getResolutionResult().allComponents.each {
|
||||
if (it.id.getDisplayName().contains("linphone-sdk-android")) {
|
||||
println 'Linphone SDK used is ' + it.moduleVersion.version + ' from ' + it.properties["repositoryName"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.tasks['preBuild'].dependsOn 'getGitVersion'
|
||||
project.tasks['preBuild'].dependsOn 'linphoneSdkSource'
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion '33.0.0'
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
versionCode appVersionCode
|
||||
versionName "${project.version}"
|
||||
applicationId getPackageName()
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all {
|
||||
outputFileName = "linphone-android-${variant.buildType.name}-${project.version}.apk"
|
||||
}
|
||||
|
||||
var enableFirebaseService = "false"
|
||||
if (firebaseEnabled) {
|
||||
enableFirebaseService = "true"
|
||||
}
|
||||
// See https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for why extractNativeLibs is set to true in debug flavor
|
||||
if (variant.buildType.name == "release" || variant.buildType.name == "releaseWithCrashlytics") {
|
||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
||||
linphone_file_provider: getPackageName() + ".fileprovider",
|
||||
appLabel: "@string/app_name",
|
||||
firebaseServiceEnabled: enableFirebaseService,
|
||||
extractNativeLibs: "false"]
|
||||
} else {
|
||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
||||
linphone_file_provider: getPackageName() + ".debug.fileprovider",
|
||||
appLabel: "@string/app_name_debug",
|
||||
firebaseServiceEnabled: enableFirebaseService,
|
||||
extractNativeLibs: "true"]
|
||||
}
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
signingConfig signingConfigs.release
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
resValue "string", "linphone_app_branch", gitBranch.toString().trim()
|
||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
||||
resValue "string", "file_provider", getPackageName() + ".fileprovider"
|
||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
||||
|
||||
if (!firebaseEnabled) {
|
||||
resValue "string", "gcm_defaultSenderId", "none"
|
||||
}
|
||||
|
||||
resValue "bool", "crashlytics_enabled", "false"
|
||||
}
|
||||
|
||||
releaseWithCrashlytics {
|
||||
initWith release
|
||||
|
||||
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
|
||||
|
||||
if (crashlyticsEnabled) {
|
||||
firebaseCrashlytics {
|
||||
nativeSymbolUploadEnabled true
|
||||
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
debuggable true
|
||||
jniDebuggable true
|
||||
|
||||
resValue "string", "linphone_app_branch", gitBranch.toString().trim()
|
||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
||||
resValue "string", "file_provider", getPackageName() + ".debug.fileprovider"
|
||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
||||
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
|
||||
|
||||
if (!firebaseEnabled) {
|
||||
resValue "string", "gcm_defaultSenderId", "none"
|
||||
}
|
||||
|
||||
if (crashlyticsEnabled) {
|
||||
firebaseCrashlytics {
|
||||
nativeSymbolUploadEnabled true
|
||||
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
namespace 'org.linphone'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03"
|
||||
implementation "androidx.window:window:1.0.0"
|
||||
|
||||
def nav_version = "2.5.3"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
|
||||
|
||||
// https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.0
|
||||
implementation 'com.google.android.material:material:1.7.0'
|
||||
// https://github.com/google/flexbox-layout/blob/main/LICENSE Apache v2.0
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
|
||||
implementation 'androidx.emoji:emoji:1.1.0'
|
||||
implementation 'androidx.emoji:emoji-bundled:1.1.0'
|
||||
|
||||
// https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0
|
||||
def coil_version = "2.1.0"
|
||||
implementation("io.coil-kt:coil:$coil_version")
|
||||
implementation("io.coil-kt:coil-gif:$coil_version")
|
||||
implementation("io.coil-kt:coil-svg:$coil_version")
|
||||
implementation("io.coil-kt:coil-video:$coil_version")
|
||||
|
||||
// https://github.com/Baseflow/PhotoView/blob/master/LICENSE Apache v2.0
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
|
||||
implementation platform('com.google.firebase:firebase-bom:31.0.3')
|
||||
if (crashlyticsEnabled) {
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ndk'
|
||||
} else {
|
||||
compileOnly 'com.google.firebase:firebase-crashlytics-ndk'
|
||||
}
|
||||
if (firebaseEnabled) {
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
}
|
||||
|
||||
implementation 'org.linphone:linphone-sdk-android:5.2+'
|
||||
|
||||
// Only enable leak canary prior to release
|
||||
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||
}
|
||||
|
||||
task generateContactsXml(type: Copy) {
|
||||
from 'contacts.xml'
|
||||
into "src/main/res/xml/"
|
||||
outputs.upToDateWhen { file('src/main/res/xml/contacts.xml').exists() }
|
||||
filter {
|
||||
line -> line
|
||||
.replaceAll('%%AUTO_GENERATED%%', 'This file has been automatically generated, do not edit or commit !')
|
||||
.replaceAll('%%PACKAGE_NAME%%', getPackageName())
|
||||
|
||||
}
|
||||
}
|
||||
project.tasks['preBuild'].dependsOn 'generateContactsXml'
|
||||
|
||||
ktlint {
|
||||
android = true
|
||||
ignoreFailures = true
|
||||
}
|
||||
|
||||
project.tasks['preBuild'].dependsOn 'ktlintFormat'
|
||||
|
||||
if (crashlyticsEnabled) {
|
||||
afterEvaluate {
|
||||
assembleDebug.finalizedBy(uploadCrashlyticsSymbolFileDebug)
|
||||
packageDebugBundle.finalizedBy(uploadCrashlyticsSymbolFileDebug)
|
||||
|
||||
assembleReleaseWithCrashlytics.finalizedBy(uploadCrashlyticsSymbolFileReleaseWithCrashlytics)
|
||||
packageReleaseWithCrashlytics.finalizedBy(uploadCrashlyticsSymbolFileReleaseWithCrashlytics)
|
||||
}
|
||||
}
|
11
app/contacts.xml
Normal file
11
app/contacts.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ContactsSource xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- %%AUTO_GENERATED%% -->
|
||||
<ContactsDataKind
|
||||
android:detailColumn="data3"
|
||||
android:detailSocialSummary="true"
|
||||
android:icon="@drawable/linphone_logo_tinted"
|
||||
android:mimeType="vnd.android.cursor.item/vnd.%%PACKAGE_NAME%%.provider.sip_address"
|
||||
android:summaryColumn="data2" />
|
||||
<!-- You can't use @string/linphone_address_mime_type above ! You have to hardcode it... -->
|
||||
</ContactsSource>
|
57
app/google-services.json
Normal file
57
app/google-services.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "929724111839",
|
||||
"firebase_url": "https://linphone-android-8a563.firebaseio.com",
|
||||
"project_id": "linphone-android-8a563",
|
||||
"storage_bucket": "linphone-android-8a563.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:929724111839:android:4662ea9a056188c4",
|
||||
"android_client_info": {
|
||||
"package_name": "org.linphone"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "929724111839-co5kffto4j7dets7oolvfv0056cvpfbl.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "org.linphone",
|
||||
"certificate_hash": "85463a95603f7b6331899b74b85d53d043dcd500"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "929724111839-v5so1tcd65iil7dd7sde8jgii44h8luf.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCKrwWhkbA7Iy3wpEI8_ZvKOMp5jf6vV6A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:929724111839:android:3cf90ee1d2f8fcb6",
|
||||
"android_client_info": {
|
||||
"package_name": "org.linphone.debug"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "929724111839-v5so1tcd65iil7dd7sde8jgii44h8luf.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCKrwWhkbA7Iy3wpEI8_ZvKOMp5jf6vV6A"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
24
app/proguard-rules.pro
vendored
Normal file
24
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep public class * extends androidx.fragment.app.Fragment { *; }
|
||||
-dontwarn com.google.errorprone.annotations.Immutable
|
243
app/src/main/AndroidManifest.xml
Normal file
243
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,243 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- To be able to display contacts list & match calling/called numbers -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<!-- For in-app contact edition -->
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
|
||||
<!-- Helps filling phone number and country code in assistant -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<!-- Needed for auto start at boot and to ensure the service won't be killed by OS while in call -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- Starting Android 13 we need to ask notification permission -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- Needed for full screen intent in incoming call notifications -->
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<!-- To vibrate when pressing DTMF keys on numpad & incoming calls -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- Needed to attach file(s) in chat room fragment -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"/>
|
||||
<!-- Starting Android 13 you need those 3 permissions instead (https://developer.android.com/about/versions/13/behavior-changes-13) -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<!-- Needed to shared downloaded files if setting is on -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<!-- Both permissions below are for contacts sync account, needed to store presence in native contact if enabled -->
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
|
||||
<!-- Needed for Telecom Manager -->
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
||||
|
||||
<!-- Needed for overlay -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<!-- Needed to check current Do not disturb policy -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||
|
||||
<!-- Needed starting Android 12 for broadcast receiver
|
||||
to be triggered when BT device is connected / disconnected
|
||||
(https://developer.android.com/guide/topics/connectivity/bluetooth/permissions) -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<!-- Needed for foreground service
|
||||
(https://developer.android.com/guide/components/foreground-services) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<application
|
||||
android:name=".LinphoneApplication"
|
||||
android:allowBackup="false"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="${appLabel}"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:extractNativeLibs="${extractNativeLibs}"
|
||||
android:theme="@style/AppTheme"
|
||||
android:allowNativeHeapPointerTagging="false">
|
||||
|
||||
<activity android:name=".activities.main.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/AppSplashScreenTheme">
|
||||
<nav-graph android:value="@navigation/main_nav_graph" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW_LOCUS" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="${linphone_address_mime_type}" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.DIAL" />
|
||||
<action android:name="android.intent.action.CALL" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="tel" />
|
||||
<data android:scheme="sip" />
|
||||
<data android:scheme="sips" />
|
||||
<data android:scheme="linphone" />
|
||||
<data android:scheme="sip-linphone" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="sms" />
|
||||
<data android:scheme="smsto" />
|
||||
<data android:scheme="mms" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".activities.assistant.AssistantActivity"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
|
||||
<activity android:name=".activities.voip.CallActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:turnScreenOn="true"
|
||||
android:showWhenLocked="true"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.chat_bubble.ChatBubbleActivity"
|
||||
android:allowEmbedded="true"
|
||||
android:documentLaunchMode="always"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
<service
|
||||
android:name=".core.CoreService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="phoneCall|camera|microphone"
|
||||
android:stopWithTask="false"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<service android:name="org.linphone.core.tools.firebase.FirebaseMessaging"
|
||||
android:enabled="${firebaseServiceEnabled}"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".contact.DummySyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_adapter" />
|
||||
<meta-data
|
||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||
android:resource="@xml/contacts" />
|
||||
</service>
|
||||
|
||||
<service android:name=".contact.DummyAuthenticationService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</service>
|
||||
|
||||
<service android:name=".telecom.TelecomConnectionService"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- Receivers -->
|
||||
|
||||
<receiver android:name=".core.CorePushReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.linphone.core.action.PUSH_RECEIVED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".notifications.NotificationBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver android:name=".core.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Providers -->
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${linphone_file_provider}"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
41
app/src/main/assets/assistant_default_values
Normal file
41
app/src/main/assets/assistant_default_values
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd">
|
||||
<section name="proxy_default_values">
|
||||
<entry name="avpf" overwrite="true">0</entry>
|
||||
<entry name="dial_escape_plus" overwrite="true">0</entry>
|
||||
<entry name="publish" overwrite="true">0</entry>
|
||||
<entry name="quality_reporting_collector" overwrite="true"></entry>
|
||||
<entry name="quality_reporting_enabled" overwrite="true">0</entry>
|
||||
<entry name="quality_reporting_interval" overwrite="true">0</entry>
|
||||
<entry name="reg_expires" overwrite="true">3600</entry>
|
||||
<entry name="reg_identity" overwrite="true"></entry>
|
||||
<entry name="reg_proxy" overwrite="true"></entry>
|
||||
<entry name="reg_route" overwrite="true"></entry>
|
||||
<entry name="reg_sendregister" overwrite="true">1</entry>
|
||||
<entry name="nat_policy_ref" overwrite="true"></entry>
|
||||
<entry name="realm" overwrite="true"></entry>
|
||||
<entry name="conference_factory_uri" overwrite="true"></entry>
|
||||
<entry name="audio_video_conference_factory_uri" overwrite="true"></entry>
|
||||
<entry name="push_notification_allowed" overwrite="true">0</entry>
|
||||
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">0</entry>
|
||||
<entry name="rtp_bundle" overwrite="true">0</entry>
|
||||
<entry name="lime_server_url" overwrite="true"></entry>
|
||||
</section>
|
||||
<section name="nat_policy_default_values">
|
||||
<entry name="stun_server" overwrite="true"></entry>
|
||||
<entry name="protocols" overwrite="true"></entry>
|
||||
</section>
|
||||
<section name="net">
|
||||
<entry name="friendlist_subscription_enabled" overwrite="true">0</entry>
|
||||
</section>
|
||||
<section name="assistant">
|
||||
<entry name="domain" overwrite="true"></entry>
|
||||
<entry name="algorithm" overwrite="true">MD5</entry>
|
||||
<entry name="password_max_length" overwrite="true">-1</entry>
|
||||
<entry name="password_min_length" overwrite="true">0</entry>
|
||||
<entry name="username_length" overwrite="true">-1</entry>
|
||||
<entry name="username_max_length" overwrite="true">128</entry>
|
||||
<entry name="username_min_length" overwrite="true">1</entry>
|
||||
<entry name="username_regex" overwrite="true">^[a-zA-Z0-9+_.\-]*$</entry>
|
||||
</section>
|
||||
</config>
|
41
app/src/main/assets/assistant_linphone_default_values
Normal file
41
app/src/main/assets/assistant_linphone_default_values
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd">
|
||||
<section name="proxy_default_values">
|
||||
<entry name="avpf" overwrite="true">1</entry>
|
||||
<entry name="dial_escape_plus" overwrite="true">0</entry>
|
||||
<entry name="publish" overwrite="true">0</entry>
|
||||
<entry name="quality_reporting_collector" overwrite="true">sip:voip-metrics@sip.linphone.org;transport=tls</entry>
|
||||
<entry name="quality_reporting_enabled" overwrite="true">1</entry>
|
||||
<entry name="quality_reporting_interval" overwrite="true">180</entry>
|
||||
<entry name="reg_expires" overwrite="true">31536000</entry>
|
||||
<entry name="reg_identity" overwrite="true">sip:?@sip.linphone.org</entry>
|
||||
<entry name="reg_proxy" overwrite="true"><sip:sip.linphone.org;transport=tls></entry>
|
||||
<entry name="reg_route" overwrite="true"><sip:sip.linphone.org;transport=tls></entry>
|
||||
<entry name="reg_sendregister" overwrite="true">1</entry>
|
||||
<entry name="nat_policy_ref" overwrite="true">nat_policy_default_values</entry>
|
||||
<entry name="realm" overwrite="true">sip.linphone.org</entry>
|
||||
<entry name="conference_factory_uri" overwrite="true">sip:conference-factory@sip.linphone.org</entry>
|
||||
<entry name="audio_video_conference_factory_uri" overwrite="true">sip:videoconference-factory@sip.linphone.org</entry>
|
||||
<entry name="push_notification_allowed" overwrite="true">1</entry>
|
||||
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">1</entry>
|
||||
<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>
|
||||
</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="net">
|
||||
<entry name="friendlist_subscription_enabled" overwrite="true">1</entry>
|
||||
</section>
|
||||
<section name="assistant">
|
||||
<entry name="domain" overwrite="true">sip.linphone.org</entry>
|
||||
<entry name="algorithm" overwrite="true">SHA-256</entry>
|
||||
<entry name="password_max_length" overwrite="true">-1</entry>
|
||||
<entry name="password_min_length" overwrite="true">1</entry>
|
||||
<entry name="username_length" overwrite="true">-1</entry>
|
||||
<entry name="username_max_length" overwrite="true">64</entry>
|
||||
<entry name="username_min_length" overwrite="true">1</entry>
|
||||
<entry name="username_regex" overwrite="true">^[a-z0-9+_.\-]*$</entry>
|
||||
</section>
|
||||
</config>
|
43
app/src/main/assets/linphonerc_default
Normal file
43
app/src/main/assets/linphonerc_default
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
## Start of default rc
|
||||
|
||||
[sip]
|
||||
contact="Linphone Android" <sip:linphone.android@unknown-host>
|
||||
use_info=0
|
||||
use_ipv6=1
|
||||
keepalive_period=30000
|
||||
sip_port=-1
|
||||
sip_tcp_port=-1
|
||||
sip_tls_port=-1
|
||||
media_encryption=none
|
||||
|
||||
[net]
|
||||
#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit"
|
||||
download_bw=0
|
||||
upload_bw=0
|
||||
|
||||
[video]
|
||||
size=vga
|
||||
|
||||
[app]
|
||||
tunnel=disabled
|
||||
auto_start=1
|
||||
record_aware=1
|
||||
|
||||
[tunnel]
|
||||
host=
|
||||
port=443
|
||||
|
||||
[misc]
|
||||
log_collection_upload_server_url=https://www.linphone.org:444/lft.php
|
||||
file_transfer_server_url=https://www.linphone.org:444/lft.php
|
||||
version_check_url_root=https://www.linphone.org/releases
|
||||
max_calls=10
|
||||
history_max_size=100
|
||||
conference_layout=1
|
||||
|
||||
[in-app-purchase]
|
||||
server_url=https://subscribe.linphone.org:444/inapp.php
|
||||
purchasable_items_ids=test_account_subscription
|
||||
|
||||
## End of default rc
|
54
app/src/main/assets/linphonerc_factory
Normal file
54
app/src/main/assets/linphonerc_factory
Normal file
@ -0,0 +1,54 @@
|
||||
|
||||
## Start of factory rc
|
||||
|
||||
# This file shall not contain path referencing package name, in order to be portable when app is renamed.
|
||||
# Paths to resources must be set from LinphoneManager, after creating LinphoneCore.
|
||||
|
||||
[net]
|
||||
mtu=1300
|
||||
force_ice_disablement=0
|
||||
|
||||
[rtp]
|
||||
accept_any_encryption=1
|
||||
|
||||
[sip]
|
||||
guess_hostname=1
|
||||
register_only_when_network_is_up=1
|
||||
auto_net_state_mon=1
|
||||
auto_answer_replacing_calls=1
|
||||
ping_with_options=0
|
||||
use_cpim=1
|
||||
zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_KYB512
|
||||
chat_messages_aggregation_delay=1000
|
||||
chat_messages_aggregation=1
|
||||
|
||||
[sound]
|
||||
#remove this property for any application that is not Linphone public version itself
|
||||
ec_calibrator_cool_tones=1
|
||||
|
||||
[video]
|
||||
displaytype=MSAndroidTextureDisplay
|
||||
auto_resize_preview_to_keep_ratio=1
|
||||
max_conference_size=vga
|
||||
|
||||
[misc]
|
||||
enable_basic_to_client_group_chat_room_migration=0
|
||||
enable_simple_group_chat_message_state=0
|
||||
aggregate_imdn=1
|
||||
notify_each_friend_individually_when_presence_received=0
|
||||
|
||||
[app]
|
||||
activation_code_length=4
|
||||
prefer_basic_chat_room=1
|
||||
record_aware=1
|
||||
|
||||
[assistant]
|
||||
xmlrpc_url=https://subscribe.linphone.org:444/wizard.php
|
||||
|
||||
[account_creator]
|
||||
backend=0
|
||||
|
||||
[lime]
|
||||
lime_update_threshold=-1
|
||||
|
||||
## End of factory rc
|
129
app/src/main/java/org/linphone/LinphoneApplication.kt
Normal file
129
app/src/main/java/org/linphone/LinphoneApplication.kt
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.decode.VideoFrameDecoder
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.mediastream.Version
|
||||
|
||||
class LinphoneApplication : Application(), ImageLoaderFactory {
|
||||
companion object {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var corePreferences: CorePreferences
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var coreContext: CoreContext
|
||||
|
||||
private fun createConfig(context: Context) {
|
||||
if (::corePreferences.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)
|
||||
Factory.instance().enableLogCollection(LogCollectionState.Enabled)
|
||||
|
||||
// For VFS
|
||||
Factory.instance().setCacheDir(context.cacheDir.absolutePath)
|
||||
|
||||
corePreferences = CorePreferences(context)
|
||||
corePreferences.copyAssetsFromPackage()
|
||||
|
||||
if (corePreferences.vfsEnabled) {
|
||||
CoreContext.activateVFS()
|
||||
}
|
||||
|
||||
val config = Factory.instance().createConfigWithFactory(corePreferences.configPath, corePreferences.factoryConfigPath)
|
||||
corePreferences.config = config
|
||||
|
||||
val appName = context.getString(R.string.app_name)
|
||||
Factory.instance().setLoggerDomain(appName)
|
||||
Factory.instance().enableLogcatLogs(corePreferences.logcatLogsOutput)
|
||||
if (corePreferences.debugLogs) {
|
||||
Factory.instance().loggingService.setLogLevel(LogLevel.Message)
|
||||
}
|
||||
|
||||
Log.i("[Application] Core config & preferences created")
|
||||
}
|
||||
|
||||
fun ensureCoreExists(
|
||||
context: Context,
|
||||
pushReceived: Boolean = false,
|
||||
service: CoreService? = null,
|
||||
useAutoStartDescription: Boolean = false
|
||||
): Boolean {
|
||||
if (::coreContext.isInitialized && !coreContext.stopped) {
|
||||
Log.d("[Application] Skipping Core creation (push received? $pushReceived)")
|
||||
return false
|
||||
}
|
||||
|
||||
Log.i("[Application] Core context is being created ${if (pushReceived) "from push" else ""}")
|
||||
coreContext = CoreContext(context, corePreferences.config, service, useAutoStartDescription)
|
||||
coreContext.start()
|
||||
return true
|
||||
}
|
||||
|
||||
fun contextExists(): Boolean {
|
||||
return ::coreContext.isInitialized
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val appName = getString(R.string.app_name)
|
||||
android.util.Log.i("[$appName]", "Application is being created")
|
||||
createConfig(applicationContext)
|
||||
Log.i("[Application] Created")
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this)
|
||||
.components {
|
||||
add(VideoFrameDecoder.Factory())
|
||||
add(SvgDecoder.Factory())
|
||||
if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
.memoryCache {
|
||||
MemoryCache.Builder(this)
|
||||
.maxSizePercent(0.25)
|
||||
.build()
|
||||
}
|
||||
.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(cacheDir.resolve("image_cache"))
|
||||
.maxSizePercent(0.02)
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
137
app/src/main/java/org/linphone/activities/GenericActivity.kt
Normal file
137
app/src/main/java/org/linphone/activities/GenericActivity.kt
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Display
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.window.layout.FoldingFeature
|
||||
import androidx.window.layout.WindowInfoTracker
|
||||
import androidx.window.layout.WindowLayoutInfo
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.LinphoneApplication.Companion.ensureCoreExists
|
||||
import org.linphone.R
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class GenericActivity : AppCompatActivity() {
|
||||
private var timer: Timer? = null
|
||||
private var _isDestructionPending = false
|
||||
val isDestructionPending: Boolean
|
||||
get() = _isDestructionPending
|
||||
|
||||
open fun onLayoutChanges(foldingFeature: FoldingFeature?) { }
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Log.i("[Generic Activity] Ensuring Core exists")
|
||||
ensureCoreExists(applicationContext)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
WindowInfoTracker
|
||||
.getOrCreate(this@GenericActivity)
|
||||
.windowLayoutInfo(this@GenericActivity)
|
||||
.collect { newLayoutInfo ->
|
||||
updateCurrentLayout(newLayoutInfo)
|
||||
}
|
||||
}
|
||||
|
||||
requestedOrientation = if (corePreferences.forcePortrait) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
_isDestructionPending = false
|
||||
val nightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
val darkModeEnabled = corePreferences.darkMode
|
||||
when (nightMode) {
|
||||
Configuration.UI_MODE_NIGHT_NO, Configuration.UI_MODE_NIGHT_UNDEFINED -> {
|
||||
if (darkModeEnabled == 1) {
|
||||
// Force dark mode
|
||||
Log.w("[Generic Activity] Forcing night mode")
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
_isDestructionPending = true
|
||||
}
|
||||
}
|
||||
Configuration.UI_MODE_NIGHT_YES -> {
|
||||
if (darkModeEnabled == 0) {
|
||||
// Force light mode
|
||||
Log.w("[Generic Activity] Forcing day mode")
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
_isDestructionPending = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateScreenSize()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Remove service notification if it has been started by device boot
|
||||
coreContext.notificationsManager.stopForegroundNotificationIfPossible()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
|
||||
}
|
||||
|
||||
fun isTablet(): Boolean {
|
||||
return resources.getBoolean(R.bool.isTablet)
|
||||
}
|
||||
|
||||
private fun updateScreenSize() {
|
||||
val metrics = DisplayMetrics()
|
||||
val display: Display = windowManager.defaultDisplay
|
||||
display.getRealMetrics(metrics)
|
||||
val screenWidth = metrics.widthPixels.toFloat()
|
||||
val screenHeight = metrics.heightPixels.toFloat()
|
||||
coreContext.screenWidth = screenWidth
|
||||
coreContext.screenHeight = screenHeight
|
||||
}
|
||||
|
||||
private fun updateCurrentLayout(newLayoutInfo: WindowLayoutInfo) {
|
||||
if (newLayoutInfo.displayFeatures.isEmpty()) {
|
||||
onLayoutChanges(null)
|
||||
} else {
|
||||
for (feature in newLayoutInfo.displayFeatures) {
|
||||
val foldingFeature = feature as? FoldingFeature
|
||||
if (foldingFeature != null) {
|
||||
onLayoutChanges(foldingFeature)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
171
app/src/main/java/org/linphone/activities/GenericFragment.kt
Normal file
171
app/src/main/java/org/linphone/activities/GenericFragment.kt
Normal file
@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
|
||||
companion object {
|
||||
val emptyFragmentsIds = arrayListOf(
|
||||
R.id.emptyChatFragment,
|
||||
R.id.emptyContactFragment,
|
||||
R.id.emptySettingsFragment,
|
||||
R.id.emptyCallHistoryFragment
|
||||
)
|
||||
}
|
||||
|
||||
private var _binding: T? = null
|
||||
protected val binding get() = _binding!!
|
||||
|
||||
protected var useMaterialSharedAxisXForwardAnimation = true
|
||||
|
||||
protected lateinit var sharedViewModel: SharedMainViewModel
|
||||
|
||||
protected fun isSharedViewModelInitialized(): Boolean {
|
||||
return ::sharedViewModel.isInitialized
|
||||
}
|
||||
|
||||
protected fun isBindingAvailable(): Boolean {
|
||||
return _binding != null
|
||||
}
|
||||
|
||||
private fun getFragmentRealClassName(): String {
|
||||
return this.javaClass.name
|
||||
}
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
try {
|
||||
val navController = findNavController()
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} handleOnBackPressed")
|
||||
if (!navController.popBackStack()) {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} couldn't pop")
|
||||
if (!navController.navigateUp()) {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} couldn't navigate up")
|
||||
// Disable this callback & start a new back press event
|
||||
isEnabled = false
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e("[Generic Fragment] ${getFragmentRealClassName()} Can't go back: $ise")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getLayoutId(): Int
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedMainViewModel::class.java]
|
||||
}
|
||||
|
||||
sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} shared main VM sliding pane has changed")
|
||||
onBackPressedCallback.isEnabled = backPressedCallBackEnabled()
|
||||
}
|
||||
|
||||
_binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
|
||||
return _binding!!.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
if (useMaterialSharedAxisXForwardAnimation && corePreferences.enableAnimations) {
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
|
||||
postponeEnterTransition()
|
||||
binding.root.doOnPreDraw { startPostponedEnterTransition() }
|
||||
}
|
||||
|
||||
setupBackPressCallback()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
|
||||
onBackPressedCallback.remove()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
protected fun goBack() {
|
||||
try {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e("[Generic Fragment] ${getFragmentRealClassName()} can't go back: $ise")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBackPressCallback() {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} setupBackPressCallback")
|
||||
|
||||
val backButton = binding.root.findViewById<ImageView>(R.id.back)
|
||||
if (backButton != null) {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} found back button")
|
||||
// If popping navigation back stack entry would bring us to an "empty" fragment
|
||||
// then don't do it if sliding pane layout isn't "flat"
|
||||
onBackPressedCallback.isEnabled = backPressedCallBackEnabled()
|
||||
backButton.setOnClickListener { goBack() }
|
||||
} else {
|
||||
onBackPressedCallback.isEnabled = false
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback)
|
||||
}
|
||||
|
||||
private fun backPressedCallBackEnabled(): Boolean {
|
||||
// This allow to navigate a SlidingPane child nav graph.
|
||||
// This only concerns fragments for which the nav graph is inside a SlidingPane layout.
|
||||
// In our case it's all graphs except the main one.
|
||||
if (findNavController().graph.id == R.id.main_nav_graph_xml) return false
|
||||
|
||||
val isSlidingPaneFlat = sharedViewModel.isSlidingPaneSlideable.value == false
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} isSlidingPaneFlat ? $isSlidingPaneFlat")
|
||||
val isPreviousFragmentEmpty = findNavController().previousBackStackEntry?.destination?.id in emptyFragmentsIds
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} isPreviousFragmentEmpty ? $isPreviousFragmentEmpty")
|
||||
val popBackStack = isSlidingPaneFlat || !isPreviousFragmentEmpty
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} popBackStack ? $popBackStack")
|
||||
return popBackStack
|
||||
}
|
||||
}
|
1170
app/src/main/java/org/linphone/activities/Navigation.kt
Normal file
1170
app/src/main/java/org/linphone/activities/Navigation.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.os.PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class ProximitySensorActivity : GenericActivity() {
|
||||
private lateinit var proximityWakeLock: PowerManager.WakeLock
|
||||
private var proximitySensorEnabled = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
if (!powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
|
||||
Log.w("[Proximity Sensor Activity] PROXIMITY_SCREEN_OFF_WAKE_LOCK isn't supported on this device!")
|
||||
}
|
||||
|
||||
proximityWakeLock = powerManager.newWakeLock(
|
||||
PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
||||
"$packageName;proximity_sensor"
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (coreContext.core.callsNb > 0) {
|
||||
val videoEnabled = coreContext.core.currentCall?.currentParams?.isVideoEnabled ?: false
|
||||
enableProximitySensor(!videoEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
enableProximitySensor(false)
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
enableProximitySensor(false)
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
protected fun enableProximitySensor(enable: Boolean) {
|
||||
if (enable) {
|
||||
if (!proximitySensorEnabled) {
|
||||
Log.i("[Proximity Sensor Activity] Enabling proximity sensor turning off screen")
|
||||
if (!proximityWakeLock.isHeld) {
|
||||
Log.i("[Proximity Sensor Activity] Acquiring PROXIMITY_SCREEN_OFF_WAKE_LOCK")
|
||||
proximityWakeLock.acquire()
|
||||
}
|
||||
proximitySensorEnabled = true
|
||||
}
|
||||
} else {
|
||||
if (proximitySensorEnabled) {
|
||||
Log.i("[Proximity Sensor Activity] Disabling proximity sensor turning off screen")
|
||||
if (proximityWakeLock.isHeld) {
|
||||
Log.i("[Proximity Sensor Activity] Releasing PROXIMITY_SCREEN_OFF_WAKE_LOCK")
|
||||
proximityWakeLock.release(RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY)
|
||||
}
|
||||
proximitySensorEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
interface SnackBarActivity {
|
||||
fun showSnackBar(@StringRes resourceId: Int)
|
||||
fun showSnackBar(@StringRes resourceId: Int, action: Int, listener: () -> Unit)
|
||||
fun showSnackBar(message: String)
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
|
||||
class AssistantActivity : GenericActivity(), SnackBarActivity {
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
private lateinit var coordinator: CoordinatorLayout
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.assistant_activity)
|
||||
|
||||
sharedViewModel = ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
|
||||
coordinator = findViewById(R.id.coordinator)
|
||||
|
||||
corePreferences.firstStart = false
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int) {
|
||||
Snackbar.make(coordinator, resourceId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int, action: Int, listener: () -> Unit) {
|
||||
Snackbar
|
||||
.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG)
|
||||
.setAction(action) {
|
||||
listener()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(message: String) {
|
||||
Snackbar.make(coordinator, message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.widget.TextView
|
||||
import kotlin.collections.ArrayList
|
||||
import org.linphone.R
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.core.Factory
|
||||
|
||||
class CountryPickerAdapter : BaseAdapter(), Filterable {
|
||||
private var countries: ArrayList<DialPlan>
|
||||
|
||||
init {
|
||||
val dialPlans = Factory.instance().dialPlans
|
||||
countries = arrayListOf()
|
||||
countries.addAll(dialPlans)
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view: View = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.assistant_country_picker_cell, parent, false)
|
||||
val dialPlan: DialPlan = countries[position]
|
||||
|
||||
val name = view.findViewById<TextView>(R.id.country_name)
|
||||
name.text = dialPlan.country
|
||||
|
||||
val dialCode = view.findViewById<TextView>(R.id.country_prefix)
|
||||
dialCode.text = String.format("(%s)", dialPlan.countryCallingCode)
|
||||
|
||||
view.tag = dialPlan
|
||||
return view
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): DialPlan {
|
||||
return countries[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return countries.size
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence): FilterResults {
|
||||
val filteredCountries = arrayListOf<DialPlan>()
|
||||
for (dialPlan in Factory.instance().dialPlans) {
|
||||
if (dialPlan.country.contains(constraint, ignoreCase = true) ||
|
||||
dialPlan.countryCallingCode.contains(constraint)
|
||||
) {
|
||||
filteredCountries.add(dialPlan)
|
||||
}
|
||||
}
|
||||
val filterResults = FilterResults()
|
||||
filterResults.values = filteredCountries
|
||||
return filterResults
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun publishResults(
|
||||
constraint: CharSequence,
|
||||
results: FilterResults
|
||||
) {
|
||||
countries = results.values as ArrayList<DialPlan>
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.viewmodels.AbstractPhoneViewModel
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.PermissionHelper
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>() {
|
||||
companion object {
|
||||
const val READ_PHONE_STATE_PERMISSION_REQUEST_CODE = 0
|
||||
}
|
||||
|
||||
abstract val viewModel: AbstractPhoneViewModel
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == READ_PHONE_STATE_PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission granted")
|
||||
updateFromDeviceInfo()
|
||||
} else {
|
||||
Log.w("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun checkPermissions() {
|
||||
if (!resources.getBoolean(R.bool.isTablet)) {
|
||||
if (!PermissionHelper.get().hasReadPhoneStateOrPhoneNumbersPermission()) {
|
||||
Log.i("[Assistant] Asking for READ_PHONE_STATE/READ_PHONE_NUMBERS permission")
|
||||
Compatibility.requestReadPhoneStateOrNumbersPermission(this, READ_PHONE_STATE_PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
updateFromDeviceInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFromDeviceInfo() {
|
||||
val phoneNumber = PhoneNumberUtils.getDevicePhoneNumber(requireContext())
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(requireContext())
|
||||
viewModel.updateFromPhoneNumberAndOrDialPlan(phoneNumber, dialPlan)
|
||||
}
|
||||
|
||||
protected fun showPhoneNumberInfoDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.assistant_phone_number_info_title))
|
||||
.setMessage(
|
||||
getString(R.string.assistant_phone_number_link_info_content) + "\n" +
|
||||
getString(
|
||||
R.string.assistant_phone_number_link_info_content_already_account
|
||||
)
|
||||
)
|
||||
.setNegativeButton(getString(R.string.dialog_ok), null)
|
||||
.show()
|
||||
}
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.AccountLoginViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.AccountLoginViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||
import org.linphone.databinding.AssistantAccountLoginFragmentBinding
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragmentBinding>() {
|
||||
override lateinit var viewModel: AccountLoginViewModel
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_account_login_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
AccountLoginViewModelFactory(sharedAssistantViewModel.getAccountCreator())
|
||||
)[AccountLoginViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
if (resources.getBoolean(R.bool.isTablet)) {
|
||||
viewModel.loginWithUsernamePassword.value = true
|
||||
}
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
val countryPickerFragment = CountryPickerFragment()
|
||||
countryPickerFragment.listener = viewModel
|
||||
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
binding.setForgotPasswordClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.data = Uri.parse(getString(R.string.assistant_forgotten_password_link))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsLogin", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
navigateToPhoneAccountValidation(args)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
coreContext.newAccountConfigured(true)
|
||||
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.invalidCredentialsEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val dialogViewModel =
|
||||
DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
viewModel.removeInvalidProxyConfig()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialogViewModel.showDeleteButton(
|
||||
{
|
||||
viewModel.continueEvenIfInvalidCredentials()
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.assistant_continue_even_if_credentials_invalid)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.*
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.adapters.CountryPickerAdapter
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.databinding.AssistantCountryPickerFragmentBinding
|
||||
|
||||
class CountryPickerFragment() : DialogFragment() {
|
||||
private var _binding: AssistantCountryPickerFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var adapter: CountryPickerAdapter
|
||||
|
||||
var listener: CountryPickedListener? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, R.style.assistant_country_dialog_style)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = AssistantCountryPickerFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
adapter = CountryPickerAdapter()
|
||||
binding.countryList.adapter = adapter
|
||||
|
||||
binding.countryList.setOnItemClickListener { _, _, position, _ ->
|
||||
if (position >= 0 && position < adapter.count) {
|
||||
val dialPlan = adapter.getItem(position)
|
||||
listener?.onCountryClicked(dialPlan)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.searchCountry.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
adapter.filter.filter(s)
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
|
||||
})
|
||||
|
||||
binding.setCancelClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
interface CountryPickedListener {
|
||||
fun onCountryClicked(dialPlan: DialPlan)
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.viewmodels.EchoCancellerCalibrationViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantEchoCancellerCalibrationFragmentBinding
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerCalibrationFragmentBinding>() {
|
||||
companion object {
|
||||
const val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 0
|
||||
}
|
||||
|
||||
private lateinit var viewModel: EchoCancellerCalibrationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_echo_canceller_calibration_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(this)[EchoCancellerCalibrationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.echoCalibrationTerminated.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
|
||||
if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
|
||||
Log.i("[Echo Canceller Calibration] Asking for RECORD_AUDIO permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), RECORD_AUDIO_PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
viewModel.startEchoCancellerCalibration()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
|
||||
val granted =
|
||||
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
|
||||
viewModel.startEchoCancellerCalibration()
|
||||
} else {
|
||||
Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied")
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToEmailAccountValidation
|
||||
import org.linphone.databinding.AssistantEmailAccountCreationFragmentBinding
|
||||
|
||||
class EmailAccountCreationFragment : GenericFragment<AssistantEmailAccountCreationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: EmailAccountCreationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_email_account_creation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, EmailAccountCreationViewModelFactory(sharedAssistantViewModel.getAccountCreator()))[EmailAccountCreationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.goToEmailValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
navigateToEmailAccountValidation()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.*
|
||||
import org.linphone.activities.navigateToAccountLinking
|
||||
import org.linphone.databinding.AssistantEmailAccountValidationFragmentBinding
|
||||
|
||||
class EmailAccountValidationFragment : GenericFragment<AssistantEmailAccountValidationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: EmailAccountValidationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_email_account_validation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, EmailAccountValidationViewModelFactory(sharedAssistantViewModel.getAccountCreator()))[EmailAccountValidationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
coreContext.newAccountConfigured(true)
|
||||
|
||||
if (!corePreferences.hideLinkPhoneNumber) {
|
||||
val args = Bundle()
|
||||
args.putBoolean("AllowSkip", true)
|
||||
args.putString("Username", viewModel.accountCreator.username)
|
||||
args.putString("Password", viewModel.accountCreator.password)
|
||||
navigateToAccountLinking(args)
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.GenericLoginViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.GenericLoginViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.databinding.AssistantGenericAccountLoginFragmentBinding
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class GenericAccountLoginFragment : GenericFragment<AssistantGenericAccountLoginFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: GenericLoginViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_generic_account_login_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, GenericLoginViewModelFactory(sharedAssistantViewModel.getAccountCreator(true)))[GenericLoginViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val isLinphoneAccount = viewModel.domain.value.orEmpty() == corePreferences.defaultDomain
|
||||
coreContext.newAccountConfigured(isLinphoneAccount)
|
||||
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.invalidCredentialsEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val dialogViewModel =
|
||||
DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
viewModel.removeInvalidProxyConfig()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialogViewModel.showDeleteButton(
|
||||
{
|
||||
viewModel.continueEvenIfInvalidCredentials()
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.assistant_continue_even_if_credentials_invalid)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2022 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.navigateToGenericLogin
|
||||
import org.linphone.databinding.AssistantGenericAccountWarningFragmentBinding
|
||||
|
||||
class GenericAccountWarningFragment : GenericFragment<AssistantGenericAccountWarningFragmentBinding>() {
|
||||
override fun getLayoutId(): Int = R.layout.assistant_generic_account_warning_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.setUnderstoodClickListener {
|
||||
navigateToGenericLogin()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||
import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding
|
||||
|
||||
class PhoneAccountCreationFragment :
|
||||
AbstractPhoneFragment<AssistantPhoneAccountCreationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
override lateinit var viewModel: PhoneAccountCreationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_phone_account_creation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
PhoneAccountCreationViewModelFactory(sharedAssistantViewModel.getAccountCreator())
|
||||
)[PhoneAccountCreationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
val countryPickerFragment = CountryPickerFragment()
|
||||
countryPickerFragment.listener = viewModel
|
||||
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsCreation", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
navigateToPhoneAccountValidation(args)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.viewmodels.*
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding
|
||||
|
||||
class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountLinkingFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
override lateinit var viewModel: PhoneAccountLinkingViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_phone_account_linking_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
val accountCreator = sharedAssistantViewModel.getAccountCreator()
|
||||
viewModel = ViewModelProvider(this, PhoneAccountLinkingViewModelFactory(accountCreator))[PhoneAccountLinkingViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
val username = arguments?.getString("Username")
|
||||
Log.i("[Phone Account Linking] username to link is $username")
|
||||
viewModel.username.value = username
|
||||
|
||||
val password = arguments?.getString("Password")
|
||||
accountCreator.password = password
|
||||
|
||||
val ha1 = arguments?.getString("HA1")
|
||||
accountCreator.ha1 = ha1
|
||||
|
||||
val allowSkip = arguments?.getBoolean("AllowSkip", false)
|
||||
viewModel.allowSkip.value = allowSkip
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
val countryPickerFragment = CountryPickerFragment()
|
||||
countryPickerFragment.listener = viewModel
|
||||
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsLinking", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
navigateToPhoneAccountValidation(args)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as SnackBarActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToAccountSettings
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding
|
||||
|
||||
class PhoneAccountValidationFragment : GenericFragment<AssistantPhoneAccountValidationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: PhoneAccountValidationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_phone_account_validation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, PhoneAccountValidationViewModelFactory(sharedAssistantViewModel.getAccountCreator()))[PhoneAccountValidationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.phoneNumber.value = arguments?.getString("PhoneNumber")
|
||||
viewModel.isLogin.value = arguments?.getBoolean("IsLogin", false)
|
||||
viewModel.isCreation.value = arguments?.getBoolean("IsCreation", false)
|
||||
viewModel.isLinking.value = arguments?.getBoolean("IsLinking", false)
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
when {
|
||||
viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
|
||||
coreContext.newAccountConfigured(true)
|
||||
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
viewModel.isLinking.value == true -> {
|
||||
if (findNavController().graph.id == R.id.settings_nav_graph_xml) {
|
||||
val args = Bundle()
|
||||
args.putString(
|
||||
"Identity",
|
||||
"sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}"
|
||||
)
|
||||
navigateToAccountSettings(args)
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as SnackBarActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
// This won't work starting Android 10 as clipboard access is denied unless app has focus,
|
||||
// which won't be the case when the SMS arrives unless it is added into clipboard from a notification
|
||||
val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.addPrimaryClipChangedListener {
|
||||
val data = clipboard.primaryClip
|
||||
if (data != null && data.itemCount > 0) {
|
||||
val clip = data.getItemAt(0).text.toString()
|
||||
if (clip.length == 4) {
|
||||
Log.i("[Assistant] [Phone Account Validation] Found 4 digits as primary clip in clipboard, using it and clear it")
|
||||
viewModel.code.value = clip
|
||||
clipboard.clearPrimaryClip()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.viewmodels.QrCodeViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantQrCodeFragmentBinding
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
|
||||
companion object {
|
||||
const val CAMERA_PERMISSION_REQUEST_CODE = 0
|
||||
}
|
||||
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: QrCodeViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_qr_code_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this)[QrCodeViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.qrCodeFoundEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { url ->
|
||||
sharedAssistantViewModel.remoteProvisioningUrl.value = url
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
viewModel.setBackCamera()
|
||||
|
||||
if (!PermissionHelper.required(requireContext()).hasCameraPermission()) {
|
||||
Log.i("[QR Code] Asking for CAMERA permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture
|
||||
coreContext.core.isQrcodeVideoPreviewEnabled = true
|
||||
coreContext.core.isVideoPreviewEnabled = true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
coreContext.core.nativePreviewWindowId = null
|
||||
coreContext.core.isQrcodeVideoPreviewEnabled = false
|
||||
coreContext.core.isVideoPreviewEnabled = false
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
|
||||
val granted =
|
||||
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[QR Code] CAMERA permission granted")
|
||||
coreContext.core.reloadVideoDevices()
|
||||
viewModel.setBackCamera()
|
||||
} else {
|
||||
Log.w("[QR Code] CAMERA permission denied")
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.RemoteProvisioningViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.activities.navigateToQrCode
|
||||
import org.linphone.databinding.AssistantRemoteProvisioningFragmentBinding
|
||||
|
||||
class RemoteProvisioningFragment : GenericFragment<AssistantRemoteProvisioningFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: RemoteProvisioningViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_remote_provisioning_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this)[RemoteProvisioningViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setQrCodeClickListener {
|
||||
navigateToQrCode()
|
||||
}
|
||||
|
||||
viewModel.fetchSuccessfulEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { success ->
|
||||
if (success) {
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
} else {
|
||||
val activity = requireActivity() as AssistantActivity
|
||||
activity.showSnackBar(R.string.assistant_remote_provisioning_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.urlToFetch.value = sharedAssistantViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (::sharedAssistantViewModel.isInitialized) {
|
||||
sharedAssistantViewModel.remoteProvisioningUrl.value = null
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.databinding.AssistantTopBarFragmentBinding
|
||||
|
||||
class TopBarFragment : GenericFragment<AssistantTopBarFragmentBinding>() {
|
||||
override fun getLayoutId(): Int = R.layout.assistant_top_bar_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
useMaterialSharedAxisXForwardAnimation = false
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.util.regex.Pattern
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.*
|
||||
import org.linphone.activities.assistant.viewmodels.WelcomeViewModel
|
||||
import org.linphone.activities.navigateToAccountLogin
|
||||
import org.linphone.activities.navigateToEmailAccountCreation
|
||||
import org.linphone.activities.navigateToRemoteProvisioning
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantWelcomeFragmentBinding
|
||||
|
||||
class WelcomeFragment : GenericFragment<AssistantWelcomeFragmentBinding>() {
|
||||
private lateinit var viewModel: WelcomeViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_welcome_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(this)[WelcomeViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setCreateAccountClickListener {
|
||||
if (resources.getBoolean(R.bool.isTablet)) {
|
||||
navigateToEmailAccountCreation()
|
||||
} else {
|
||||
navigateToPhoneAccountCreation()
|
||||
}
|
||||
}
|
||||
|
||||
binding.setAccountLoginClickListener {
|
||||
navigateToAccountLogin()
|
||||
}
|
||||
|
||||
binding.setGenericAccountLoginClickListener {
|
||||
navigateToGenericLoginWarning()
|
||||
}
|
||||
|
||||
binding.setRemoteProvisioningClickListener {
|
||||
navigateToRemoteProvisioning()
|
||||
}
|
||||
|
||||
viewModel.termsAndPrivacyAccepted.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it) corePreferences.readAndAgreeTermsAndPrivacy = true
|
||||
}
|
||||
|
||||
setUpTermsAndPrivacyLinks()
|
||||
}
|
||||
|
||||
private fun setUpTermsAndPrivacyLinks() {
|
||||
val terms = getString(R.string.assistant_general_terms)
|
||||
val privacy = getString(R.string.assistant_privacy_policy)
|
||||
|
||||
val label = getString(
|
||||
R.string.assistant_read_and_agree_terms,
|
||||
terms,
|
||||
privacy
|
||||
)
|
||||
val spannable = SpannableString(label)
|
||||
|
||||
val termsMatcher = Pattern.compile(terms).matcher(label)
|
||||
if (termsMatcher.find()) {
|
||||
val clickableSpan: ClickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.assistant_general_terms_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e("[Welcome] Can't start activity: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
spannable.setSpan(clickableSpan, termsMatcher.start(0), termsMatcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
|
||||
val policyMatcher = Pattern.compile(privacy).matcher(label)
|
||||
if (policyMatcher.find()) {
|
||||
val clickableSpan: ClickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.assistant_privacy_policy_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e("[Welcome] Can't start activity: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
spannable.setSpan(clickableSpan, policyMatcher.start(0), policyMatcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
|
||||
binding.termsAndPrivacy.text = spannable
|
||||
binding.termsAndPrivacy.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.activities.assistant.fragments.CountryPickerFragment
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
abstract class AbstractPhoneViewModel(val accountCreator: AccountCreator) :
|
||||
ViewModel(),
|
||||
CountryPickerFragment.CountryPickedListener {
|
||||
|
||||
val prefix = MutableLiveData<String>()
|
||||
|
||||
val phoneNumber = MutableLiveData<String>()
|
||||
val phoneNumberError = MutableLiveData<String>()
|
||||
|
||||
val countryName: LiveData<String> = Transformations.switchMap(prefix) {
|
||||
getCountryNameFromPrefix(it)
|
||||
}
|
||||
|
||||
init {
|
||||
prefix.value = "+"
|
||||
}
|
||||
|
||||
override fun onCountryClicked(dialPlan: DialPlan) {
|
||||
prefix.value = "+${dialPlan.countryCallingCode}"
|
||||
}
|
||||
|
||||
fun isPhoneNumberOk(): Boolean {
|
||||
return countryName.value.orEmpty().isNotEmpty() && phoneNumber.value.orEmpty().isNotEmpty() && phoneNumberError.value.orEmpty().isEmpty()
|
||||
}
|
||||
|
||||
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
|
||||
val internationalPrefix = "+${dialPlan?.countryCallingCode}"
|
||||
if (dialPlan != null) {
|
||||
Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}")
|
||||
prefix.value = internationalPrefix
|
||||
}
|
||||
|
||||
if (number != null) {
|
||||
Log.i("[Assistant] Found phone number: $number")
|
||||
phoneNumber.value = if (number.startsWith(internationalPrefix)) {
|
||||
number.substring(internationalPrefix.length)
|
||||
} else {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCountryNameFromPrefix(prefix: String?): MutableLiveData<String> {
|
||||
val country = MutableLiveData<String>()
|
||||
country.value = ""
|
||||
|
||||
if (prefix != null && prefix.isNotEmpty()) {
|
||||
val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode)
|
||||
Log.i("[Assistant] Found dial plan $dialPlan from country code: $countryCode")
|
||||
country.value = dialPlan?.country
|
||||
}
|
||||
return country
|
||||
}
|
||||
}
|
@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return AccountLoginViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||
val loginWithUsernamePassword = MutableLiveData<Boolean>()
|
||||
|
||||
val username = MutableLiveData<String>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
val passwordError = MutableLiveData<String>()
|
||||
|
||||
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val leaveAssistantEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val invalidCredentialsEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val goToSmsValidationEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onRecoverAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Account Login] Recover account status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.RequestOk) {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var proxyConfigToCheck: ProxyConfig? = null
|
||||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRegistrationStateChanged(
|
||||
core: Core,
|
||||
cfg: ProxyConfig,
|
||||
state: RegistrationState,
|
||||
message: String
|
||||
) {
|
||||
if (cfg == proxyConfigToCheck) {
|
||||
Log.i("[Assistant] [Account Login] Registration state is $state: $message")
|
||||
if (state == RegistrationState.Ok) {
|
||||
waitForServerAnswer.value = false
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
} else if (state == RegistrationState.Failed) {
|
||||
waitForServerAnswer.value = false
|
||||
invalidCredentialsEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
loginWithUsernamePassword.value = coreContext.context.resources.getBoolean(R.bool.isTablet)
|
||||
|
||||
loginEnabled.value = false
|
||||
loginEnabled.addSource(prefix) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(phoneNumber) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(username) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(password) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(loginWithUsernamePassword) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(phoneNumberError) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun removeInvalidProxyConfig() {
|
||||
val cfg = proxyConfigToCheck
|
||||
cfg ?: return
|
||||
val authInfo = cfg.findAuthInfo()
|
||||
if (authInfo != null) coreContext.core.removeAuthInfo(authInfo)
|
||||
coreContext.core.removeProxyConfig(cfg)
|
||||
proxyConfigToCheck = null
|
||||
}
|
||||
|
||||
fun continueEvenIfInvalidCredentials() {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun login() {
|
||||
accountCreator.displayName = displayName.value
|
||||
|
||||
if (loginWithUsernamePassword.value == true) {
|
||||
val result = accountCreator.setUsername(username.value)
|
||||
if (result != AccountCreator.UsernameStatus.Ok) {
|
||||
Log.e("[Assistant] [Account Login] Error [${result.name}] setting the username: ${username.value}")
|
||||
usernameError.value = result.name
|
||||
return
|
||||
}
|
||||
Log.i("[Assistant] [Account Login] Username is ${accountCreator.username}")
|
||||
|
||||
val result2 = accountCreator.setPassword(password.value)
|
||||
if (result2 != AccountCreator.PasswordStatus.Ok) {
|
||||
Log.e("[Assistant] [Account Login] Error [${result2.name}] setting the password")
|
||||
passwordError.value = result2.name
|
||||
return
|
||||
}
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
coreContext.core.addListener(coreListener)
|
||||
if (!createProxyConfig()) {
|
||||
waitForServerAnswer.value = false
|
||||
coreContext.core.removeListener(coreListener)
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
}
|
||||
} else {
|
||||
val result = AccountCreator.PhoneNumberStatus.fromInt(accountCreator.setPhoneNumber(phoneNumber.value, prefix.value))
|
||||
if (result != AccountCreator.PhoneNumberStatus.Ok) {
|
||||
Log.e("[Assistant] [Account Login] Error [$result] setting the phone number: ${phoneNumber.value} with prefix: ${prefix.value}")
|
||||
phoneNumberError.value = result.name
|
||||
return
|
||||
}
|
||||
Log.i("[Assistant] [Account Login] Phone number is ${accountCreator.phoneNumber}")
|
||||
|
||||
val result2 = accountCreator.setUsername(accountCreator.phoneNumber)
|
||||
if (result2 != AccountCreator.UsernameStatus.Ok) {
|
||||
Log.e("[Assistant] [Account Login] Error [${result2.name}] setting the username: ${accountCreator.phoneNumber}")
|
||||
usernameError.value = result2.name
|
||||
return
|
||||
}
|
||||
Log.i("[Assistant] [Account Login] Username is ${accountCreator.username}")
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.recoverAccount()
|
||||
Log.i("[Assistant] [Account Login] Recover account returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLoginButtonEnabled(): Boolean {
|
||||
return if (loginWithUsernamePassword.value == true) {
|
||||
username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
|
||||
} else {
|
||||
isPhoneNumberOk()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProxyConfig(): Boolean {
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
proxyConfigToCheck = proxyConfig
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Account Login] Account creator couldn't create proxy config")
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
return false
|
||||
}
|
||||
|
||||
proxyConfig.isPushNotificationAllowed = true
|
||||
|
||||
if (proxyConfig.dialPrefix.isNullOrEmpty()) {
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(coreContext.context)
|
||||
if (dialPlan != null) {
|
||||
Log.i("[Assistant] [Account Login] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}")
|
||||
proxyConfig.edit()
|
||||
proxyConfig.dialPrefix = dialPlan.countryCallingCode
|
||||
proxyConfig.done()
|
||||
} else {
|
||||
Log.w("[Assistant] [Account Login] Failed to find dial plan")
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("[Assistant] [Account Login] Proxy config created")
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.EcCalibratorStatus
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EchoCancellerCalibrationViewModel : ViewModel() {
|
||||
val echoCalibrationTerminated = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onEcCalibrationResult(core: Core, status: EcCalibratorStatus, delayMs: Int) {
|
||||
if (status == EcCalibratorStatus.InProgress) return
|
||||
echoCancellerCalibrationFinished(status, delayMs)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
}
|
||||
|
||||
fun startEchoCancellerCalibration() {
|
||||
coreContext.core.startEchoCancellerCalibration()
|
||||
}
|
||||
|
||||
fun echoCancellerCalibrationFinished(status: EcCalibratorStatus, delay: Int) {
|
||||
coreContext.core.removeListener(listener)
|
||||
when (status) {
|
||||
EcCalibratorStatus.DoneNoEcho -> {
|
||||
Log.i("[Echo Canceller Calibration] Done, no echo")
|
||||
}
|
||||
EcCalibratorStatus.Done -> {
|
||||
Log.i("[Echo Canceller Calibration] Done, delay is ${delay}ms")
|
||||
}
|
||||
EcCalibratorStatus.Failed -> {
|
||||
Log.w("[Echo Canceller Calibration] Failed")
|
||||
}
|
||||
EcCalibratorStatus.InProgress -> {
|
||||
Log.i("[Echo Canceller Calibration] In progress")
|
||||
}
|
||||
}
|
||||
echoCalibrationTerminated.value = Event(true)
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EmailAccountCreationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return EmailAccountCreationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EmailAccountCreationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val username = MutableLiveData<String>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val email = MutableLiveData<String>()
|
||||
val emailError = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
val passwordError = MutableLiveData<String>()
|
||||
|
||||
val passwordConfirmation = MutableLiveData<String>()
|
||||
val passwordConfirmationError = MutableLiveData<String>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val createEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val goToEmailValidationEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountExist(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Account Creation] onIsAccountExist status is $status")
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
||||
waitForServerAnswer.value = false
|
||||
usernameError.value = AppUtils.getString(R.string.assistant_error_username_already_exists)
|
||||
}
|
||||
AccountCreator.Status.AccountNotExist -> {
|
||||
val createAccountStatus = creator.createAccount()
|
||||
if (createAccountStatus != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Account Creation] onCreateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountCreated -> {
|
||||
goToEmailValidationEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
createEnabled.value = false
|
||||
createEnabled.addSource(username) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(usernameError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(email) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(emailError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(password) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordConfirmation) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordConfirmationError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun create() {
|
||||
accountCreator.username = username.value
|
||||
accountCreator.password = password.value
|
||||
accountCreator.email = email.value
|
||||
accountCreator.displayName = displayName.value
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountExist
|
||||
Log.i("[Assistant] [Account Creation] Account exists returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCreateButtonEnabled(): Boolean {
|
||||
return username.value.orEmpty().isNotEmpty() &&
|
||||
email.value.orEmpty().isNotEmpty() &&
|
||||
password.value.orEmpty().isNotEmpty() &&
|
||||
passwordConfirmation.value.orEmpty().isNotEmpty() &&
|
||||
password.value == passwordConfirmation.value &&
|
||||
usernameError.value.orEmpty().isEmpty() &&
|
||||
emailError.value.orEmpty().isEmpty() &&
|
||||
passwordError.value.orEmpty().isEmpty() &&
|
||||
passwordConfirmationError.value.orEmpty().isEmpty()
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.ProxyConfig
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return EmailAccountValidationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val email = MutableLiveData<String>()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountActivated(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Account Validation] onIsAccountActivated status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountActivated -> {
|
||||
if (createProxyConfig()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AccountNotActivated -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
email.value = accountCreator.email
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountActivated
|
||||
Log.i("[Assistant] [Account Validation] Account exists returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProxyConfig(): Boolean {
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Account Validation] Account creator couldn't create proxy config")
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
return false
|
||||
}
|
||||
|
||||
proxyConfig.isPushNotificationAllowed = true
|
||||
|
||||
if (proxyConfig.dialPrefix.isNullOrEmpty()) {
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(LinphoneApplication.coreContext.context)
|
||||
if (dialPlan != null) {
|
||||
Log.i("[Assistant] [Account Validation] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}")
|
||||
proxyConfig.edit()
|
||||
proxyConfig.dialPrefix = dialPlan.countryCallingCode
|
||||
proxyConfig.done()
|
||||
} else {
|
||||
Log.w("[Assistant] [Account Validation] Failed to find dial plan")
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("[Assistant] [Account Validation] Proxy config created")
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class GenericLoginViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return GenericLoginViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewModel() {
|
||||
val username = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
|
||||
val domain = MutableLiveData<String>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val transport = MutableLiveData<TransportType>()
|
||||
|
||||
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val invalidCredentialsEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private var proxyConfigToCheck: ProxyConfig? = null
|
||||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRegistrationStateChanged(
|
||||
core: Core,
|
||||
cfg: ProxyConfig,
|
||||
state: RegistrationState,
|
||||
message: String
|
||||
) {
|
||||
if (cfg == proxyConfigToCheck) {
|
||||
Log.i("[Assistant] [Generic Login] Registration state is $state: $message")
|
||||
if (state == RegistrationState.Ok) {
|
||||
waitForServerAnswer.value = false
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
} else if (state == RegistrationState.Failed) {
|
||||
waitForServerAnswer.value = false
|
||||
invalidCredentialsEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
transport.value = TransportType.Tls
|
||||
|
||||
loginEnabled.value = false
|
||||
loginEnabled.addSource(username) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(password) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(domain) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTransport(transportType: TransportType) {
|
||||
transport.value = transportType
|
||||
}
|
||||
|
||||
fun removeInvalidProxyConfig() {
|
||||
val cfg = proxyConfigToCheck
|
||||
cfg ?: return
|
||||
val authInfo = cfg.findAuthInfo()
|
||||
if (authInfo != null) coreContext.core.removeAuthInfo(authInfo)
|
||||
coreContext.core.removeProxyConfig(cfg)
|
||||
proxyConfigToCheck = null
|
||||
}
|
||||
|
||||
fun continueEvenIfInvalidCredentials() {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun createProxyConfig() {
|
||||
waitForServerAnswer.value = true
|
||||
coreContext.core.addListener(coreListener)
|
||||
|
||||
accountCreator.username = username.value
|
||||
accountCreator.password = password.value
|
||||
accountCreator.domain = domain.value
|
||||
accountCreator.displayName = displayName.value
|
||||
accountCreator.transport = transport.value
|
||||
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
proxyConfigToCheck = proxyConfig
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Generic Login] Account creator couldn't create proxy config")
|
||||
coreContext.core.removeListener(coreListener)
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
waitForServerAnswer.value = false
|
||||
return
|
||||
}
|
||||
|
||||
Log.i("[Assistant] [Generic Login] Proxy config created")
|
||||
}
|
||||
|
||||
private fun isLoginButtonEnabled(): Boolean {
|
||||
return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountCreationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountCreationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||
val username = MutableLiveData<String>()
|
||||
val useUsername = MutableLiveData<Boolean>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val createEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val goToSmsValidationEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountExist(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Creation] onIsAccountExist status is $status")
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
||||
waitForServerAnswer.value = false
|
||||
if (useUsername.value == true) {
|
||||
usernameError.value = AppUtils.getString(R.string.assistant_error_username_already_exists)
|
||||
} else {
|
||||
phoneNumberError.value = AppUtils.getString(R.string.assistant_error_phone_number_already_exists)
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AccountNotExist -> {
|
||||
val createAccountStatus = creator.createAccount()
|
||||
Log.i("[Phone Account Creation] createAccount returned $createAccountStatus")
|
||||
if (createAccountStatus != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Creation] onCreateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountCreated -> {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
}
|
||||
AccountCreator.Status.AccountExistWithAlias -> {
|
||||
phoneNumberError.value = AppUtils.getString(R.string.assistant_error_phone_number_already_exists)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
useUsername.value = false
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
createEnabled.value = false
|
||||
createEnabled.addSource(prefix) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(phoneNumber) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(useUsername) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(username) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(usernameError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(phoneNumberError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun create() {
|
||||
accountCreator.displayName = displayName.value
|
||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||
if (useUsername.value == true) {
|
||||
accountCreator.username = username.value
|
||||
} else {
|
||||
accountCreator.username = accountCreator.phoneNumber
|
||||
}
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountExist
|
||||
Log.i("[Phone Account Creation] isAccountExist returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCreateButtonEnabled(): Boolean {
|
||||
val usernameRegexp = corePreferences.config.getString("assistant", "username_regex", "^[a-z0-9+_.\\-]*\$")
|
||||
return isPhoneNumberOk() && usernameRegexp != null &&
|
||||
(
|
||||
useUsername.value == false ||
|
||||
username.value.orEmpty().matches(Regex(usernameRegexp)) &&
|
||||
username.value.orEmpty().isNotEmpty() &&
|
||||
usernameError.value.orEmpty().isEmpty()
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountLinkingViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountLinkingViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||
val username = MutableLiveData<String>()
|
||||
|
||||
val allowSkip = MutableLiveData<Boolean>()
|
||||
|
||||
val linkEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val goToSmsValidationEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAliasUsed(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Linking] onIsAliasUsed status is $status")
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AliasNotExist -> {
|
||||
if (creator.linkAccount() != AccountCreator.Status.RequestOk) {
|
||||
Log.e("[Phone Account Linking] linkAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AliasExist, AccountCreator.Status.AliasIsAccount -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLinkAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Linking] onLinkAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.RequestOk -> {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
linkEnabled.value = false
|
||||
linkEnabled.addSource(prefix) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
linkEnabled.addSource(phoneNumber) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
linkEnabled.addSource(phoneNumberError) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun link() {
|
||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||
accountCreator.username = username.value
|
||||
Log.i("[Assistant] [Phone Account Linking] Phone number is ${accountCreator.phoneNumber}")
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status: AccountCreator.Status = accountCreator.isAliasUsed
|
||||
Log.i("[Phone Account Linking] isAliasUsed returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
fun skip() {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
private fun isLinkButtonEnabled(): Boolean {
|
||||
return isPhoneNumberOk()
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.ProxyConfig
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountValidationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val phoneNumber = MutableLiveData<String>()
|
||||
|
||||
val code = MutableLiveData<String>()
|
||||
|
||||
val isLogin = MutableLiveData<Boolean>()
|
||||
|
||||
val isCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val isLinking = MutableLiveData<Boolean>()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onLoginLinphoneAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onLoginLinphoneAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.RequestOk) {
|
||||
if (createProxyConfig()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
}
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivateAlias(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onActivateAlias status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountActivated -> {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onActivateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.AccountActivated) {
|
||||
if (createProxyConfig()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
}
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
accountCreator.activationCode = code.value.orEmpty()
|
||||
Log.i("[Assistant] [Phone Account Validation] Phone number is ${accountCreator.phoneNumber} and activation code is ${accountCreator.activationCode}")
|
||||
waitForServerAnswer.value = true
|
||||
|
||||
val status = when {
|
||||
isLogin.value == true -> accountCreator.loginLinphoneAccount()
|
||||
isCreation.value == true -> accountCreator.activateAccount()
|
||||
isLinking.value == true -> accountCreator.activateAlias()
|
||||
else -> AccountCreator.Status.UnexpectedError
|
||||
}
|
||||
Log.i("[Assistant] [Phone Account Validation] Code validation result is $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProxyConfig(): Boolean {
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Phone Account Validation] Account creator couldn't create proxy config")
|
||||
return false
|
||||
}
|
||||
|
||||
proxyConfig.isPushNotificationAllowed = true
|
||||
Log.i("[Assistant] [Phone Account Validation] Proxy config created")
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class QrCodeViewModel : ViewModel() {
|
||||
val qrCodeFoundEvent = MutableLiveData<Event<String>>()
|
||||
|
||||
val showSwitchCamera = MutableLiveData<Boolean>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onQrcodeFound(core: Core, result: String?) {
|
||||
Log.i("[QR Code] Found [$result]")
|
||||
if (result != null) qrCodeFoundEvent.postValue(Event(result))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
showSwitchCamera.value = coreContext.showSwitchCameraButton()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun setBackCamera() {
|
||||
showSwitchCamera.value = coreContext.showSwitchCameraButton()
|
||||
|
||||
for (camera in coreContext.core.videoDevicesList) {
|
||||
if (camera.contains("Back")) {
|
||||
Log.i("[QR Code] Found back facing camera: $camera")
|
||||
coreContext.core.videoDevice = camera
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val first = coreContext.core.videoDevicesList.firstOrNull()
|
||||
if (first != null) {
|
||||
Log.i("[QR Code] Using first camera found: $first")
|
||||
coreContext.core.videoDevice = first
|
||||
}
|
||||
}
|
||||
|
||||
fun switchCamera() {
|
||||
coreContext.switchCamera()
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.ConfiguringState
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class RemoteProvisioningViewModel : ViewModel() {
|
||||
val urlToFetch = MutableLiveData<String>()
|
||||
val urlError = MutableLiveData<String>()
|
||||
|
||||
val fetchEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
val fetchInProgress = MutableLiveData<Boolean>()
|
||||
val fetchSuccessfulEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onConfiguringStatus(core: Core, status: ConfiguringState, message: String?) {
|
||||
fetchInProgress.value = false
|
||||
when (status) {
|
||||
ConfiguringState.Successful -> {
|
||||
fetchSuccessfulEvent.value = Event(true)
|
||||
}
|
||||
ConfiguringState.Failed -> {
|
||||
fetchSuccessfulEvent.value = Event(false)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
fetchInProgress.value = false
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
fetchEnabled.value = false
|
||||
fetchEnabled.addSource(urlToFetch) {
|
||||
fetchEnabled.value = isFetchEnabled()
|
||||
}
|
||||
fetchEnabled.addSource(urlError) {
|
||||
fetchEnabled.value = isFetchEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun fetchAndApply() {
|
||||
val url = urlToFetch.value.orEmpty()
|
||||
coreContext.core.provisioningUri = url
|
||||
Log.w("[Remote Provisioning] Url set to [$url], restarting Core")
|
||||
fetchInProgress.value = true
|
||||
coreContext.core.stop()
|
||||
coreContext.core.start()
|
||||
}
|
||||
|
||||
private fun isFetchEnabled(): Boolean {
|
||||
return urlToFetch.value.orEmpty().isNotEmpty() && urlError.value.orEmpty().isEmpty()
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import java.util.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class SharedAssistantViewModel : ViewModel() {
|
||||
val remoteProvisioningUrl = MutableLiveData<String>()
|
||||
|
||||
private var accountCreator: AccountCreator
|
||||
private var useGenericSipAccount: Boolean = false
|
||||
|
||||
init {
|
||||
Log.i("[Assistant] Loading linphone default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||
accountCreator = coreContext.core.createAccountCreator(corePreferences.xmlRpcServerUrl)
|
||||
accountCreator.language = Locale.getDefault().language
|
||||
}
|
||||
|
||||
fun getAccountCreator(genericAccountCreator: Boolean = false): AccountCreator {
|
||||
if (genericAccountCreator != useGenericSipAccount) {
|
||||
accountCreator.reset()
|
||||
accountCreator.language = Locale.getDefault().language
|
||||
|
||||
if (genericAccountCreator) {
|
||||
Log.i("[Assistant] Loading default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.defaultValuesPath)
|
||||
} else {
|
||||
Log.i("[Assistant] Loading linphone default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||
}
|
||||
useGenericSipAccount = genericAccountCreator
|
||||
}
|
||||
return accountCreator
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
|
||||
class WelcomeViewModel : ViewModel() {
|
||||
val showCreateAccount: Boolean = corePreferences.showCreateAccount
|
||||
val showLinphoneLogin: Boolean = corePreferences.showLinphoneLogin
|
||||
val showGenericLogin: Boolean = corePreferences.showGenericLogin
|
||||
val showRemoteProvisioning: Boolean = corePreferences.showRemoteProvisioning
|
||||
|
||||
val termsAndPrivacyAccepted = MutableLiveData<Boolean>()
|
||||
|
||||
init {
|
||||
termsAndPrivacyAccepted.value = corePreferences.readAndAgreeTermsAndPrivacy
|
||||
}
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.chat_bubble
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.*
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomListenerStub
|
||||
import org.linphone.core.EventLog
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatBubbleActivityBinding
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class ChatBubbleActivity : GenericActivity() {
|
||||
private lateinit var binding: ChatBubbleActivityBinding
|
||||
private lateinit var viewModel: ChatRoomViewModel
|
||||
private lateinit var listViewModel: ChatMessagesListViewModel
|
||||
private lateinit var chatSendingViewModel: ChatMessageSendingViewModel
|
||||
private lateinit var adapter: ChatMessagesListAdapter
|
||||
|
||||
private val observer = object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (positionStart == adapter.itemCount - itemCount) {
|
||||
adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array<out EventLog>) {
|
||||
chatRoom.markAsRead()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.chat_bubble_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
val localSipUri = intent.getStringExtra("LocalSipUri")
|
||||
val remoteSipUri = intent.getStringExtra("RemoteSipUri")
|
||||
var chatRoom: ChatRoom? = null
|
||||
|
||||
if (localSipUri != null && remoteSipUri != null) {
|
||||
Log.i("[Chat Bubble] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments")
|
||||
val localAddress = Factory.instance().createAddress(localSipUri)
|
||||
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
|
||||
chatRoom = coreContext.core.searchChatRoom(
|
||||
null, localAddress, remoteSipAddress,
|
||||
arrayOfNulls(
|
||||
0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Chat Bubble] Chat room is null, aborting!")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatRoomViewModelFactory(chatRoom)
|
||||
)[ChatRoomViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
listViewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatMessagesListViewModelFactory(chatRoom)
|
||||
)[ChatMessagesListViewModel::class.java]
|
||||
|
||||
chatSendingViewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatMessageSendingViewModelFactory(chatRoom)
|
||||
)[ChatMessageSendingViewModel::class.java]
|
||||
binding.chatSendingViewModel = chatSendingViewModel
|
||||
|
||||
val listSelectionViewModel = ViewModelProvider(this)[ListTopBarViewModel::class.java]
|
||||
adapter = ChatMessagesListAdapter(listSelectionViewModel, this)
|
||||
// SubmitList is done on a background thread
|
||||
// We need this adapter data observer to know when to scroll
|
||||
binding.chatMessagesList.adapter = adapter
|
||||
adapter.registerAdapterDataObserver(observer)
|
||||
|
||||
// Disable context menu on each message
|
||||
adapter.disableAdvancedContextMenuOptions()
|
||||
|
||||
adapter.openContentEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume { content ->
|
||||
if (content.isFileEncrypted) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
R.string.chat_bubble_cant_open_enrypted_file,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
FileUtils.openFileInThirdPartyApp(this, content.filePath.orEmpty(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val layoutManager = LinearLayoutManager(this)
|
||||
layoutManager.stackFromEnd = true
|
||||
binding.chatMessagesList.layoutManager = layoutManager
|
||||
|
||||
listViewModel.events.observe(
|
||||
this
|
||||
) { events ->
|
||||
adapter.submitList(events)
|
||||
}
|
||||
|
||||
chatSendingViewModel.textToSend.observe(
|
||||
this
|
||||
) {
|
||||
chatSendingViewModel.onTextToSendChanged(it)
|
||||
}
|
||||
|
||||
binding.setOpenAppClickListener {
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, false)
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.putExtra("RemoteSipUri", remoteSipUri)
|
||||
intent.putExtra("LocalSipUri", localSipUri)
|
||||
intent.putExtra("Chat", true)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK and Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.setCloseBubbleClickListener {
|
||||
coreContext.notificationsManager.dismissChatNotification(viewModel.chatRoom)
|
||||
}
|
||||
|
||||
binding.setSendMessageClickListener {
|
||||
chatSendingViewModel.sendMessage()
|
||||
binding.message.text?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
viewModel.chatRoom.addListener(listener)
|
||||
|
||||
// Workaround for the removed notification when a chat room is marked as read
|
||||
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, true)
|
||||
viewModel.chatRoom.markAsRead()
|
||||
|
||||
val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly()
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress
|
||||
coreContext.notificationsManager.resetChatNotificationCounterForSipUri(peerAddress)
|
||||
|
||||
lifecycleScope.launch {
|
||||
// Without the delay the scroll to bottom doesn't happen...
|
||||
delay(100)
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
viewModel.chatRoom.removeListener(listener)
|
||||
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, false)
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun scrollToBottom() {
|
||||
if (adapter.itemCount > 0) {
|
||||
binding.chatMessagesList.scrollToPosition(adapter.itemCount - 1)
|
||||
}
|
||||
}
|
||||
}
|
542
app/src/main/java/org/linphone/activities/main/MainActivity.kt
Normal file
542
app/src/main/java/org/linphone/activities/main/MainActivity.kt
Normal file
@ -0,0 +1,542 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main
|
||||
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.window.layout.FoldingFeature
|
||||
import coil.imageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLDecoder
|
||||
import kotlin.math.abs
|
||||
import kotlinx.coroutines.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.*
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.main.viewmodels.CallOverlayViewModel
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.activities.navigateToDialer
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.CorePreferences
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.MainActivityBinding
|
||||
import org.linphone.utils.*
|
||||
|
||||
class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestinationChangedListener {
|
||||
private lateinit var binding: MainActivityBinding
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
private lateinit var callOverlayViewModel: CallOverlayViewModel
|
||||
|
||||
private val listener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
Log.i("[Main Activity] Contact(s) updated, update shortcuts")
|
||||
if (corePreferences.contactsShortcuts) {
|
||||
ShortcutsHelper.createShortcutsToContacts(this@MainActivity)
|
||||
} else if (corePreferences.chatRoomShortcuts) {
|
||||
ShortcutsHelper.createShortcutsToChatRooms(this@MainActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var tabsFragment: FragmentContainerView
|
||||
private lateinit var statusFragment: FragmentContainerView
|
||||
|
||||
private var overlayX = 0f
|
||||
private var overlayY = 0f
|
||||
private var initPosX = 0f
|
||||
private var initPosY = 0f
|
||||
private var overlay: View? = null
|
||||
|
||||
private val componentCallbacks = object : ComponentCallbacks2 {
|
||||
override fun onConfigurationChanged(newConfig: Configuration) { }
|
||||
|
||||
override fun onLowMemory() {
|
||||
Log.w("[Main Activity] onLowMemory !")
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
Log.w("[Main Activity] onTrimMemory called with level $level !")
|
||||
applicationContext.imageLoader.memoryCache?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
|
||||
sharedViewModel.layoutChangedEvent.value = Event(true)
|
||||
}
|
||||
|
||||
private var shouldTabsBeVisibleDependingOnDestination = true
|
||||
private var shouldTabsBeVisibleDueToOrientationAndKeyboard = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Must be done before the setContentView
|
||||
installSplashScreen()
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = ViewModelProvider(this)[SharedMainViewModel::class.java]
|
||||
binding.viewModel = sharedViewModel
|
||||
|
||||
callOverlayViewModel = ViewModelProvider(this)[CallOverlayViewModel::class.java]
|
||||
binding.callOverlayViewModel = callOverlayViewModel
|
||||
|
||||
sharedViewModel.toggleDrawerEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume {
|
||||
if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) {
|
||||
binding.sideMenu.closeDrawer(binding.sideMenuContent, true)
|
||||
} else {
|
||||
binding.sideMenu.openDrawer(binding.sideMenuContent, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coreContext.callErrorMessageResourceId.observe(
|
||||
this
|
||||
) {
|
||||
it.consume { message ->
|
||||
showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
if (coreContext.core.accountList.isEmpty()) {
|
||||
if (corePreferences.firstStart) {
|
||||
startActivity(Intent(this, AssistantActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
tabsFragment = findViewById(R.id.tabs_fragment)
|
||||
statusFragment = findViewById(R.id.status_fragment)
|
||||
|
||||
binding.root.doOnAttach {
|
||||
Log.i("[Main Activity] Report UI has been fully drawn (TTFD)")
|
||||
try {
|
||||
reportFullyDrawn()
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[Main Activity] Security exception when doing reportFullyDrawn(): $se")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
if (intent != null) {
|
||||
Log.d("[Main Activity] Found new intent")
|
||||
handleIntentParams(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
coreContext.contactsManager.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
coreContext.contactsManager.removeListener(listener)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int) {
|
||||
Snackbar.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int, action: Int, listener: () -> Unit) {
|
||||
Snackbar
|
||||
.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG)
|
||||
.setAction(action) {
|
||||
Log.i("[Snack Bar] Action listener triggered")
|
||||
listener()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(message: String) {
|
||||
Snackbar.make(findViewById(R.id.coordinator), message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
|
||||
registerComponentCallbacks(componentCallbacks)
|
||||
findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this)
|
||||
|
||||
binding.rootCoordinatorLayout.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
||||
val keyboardVisible = ViewCompat.getRootWindowInsets(binding.rootCoordinatorLayout)
|
||||
?.isVisible(WindowInsetsCompat.Type.ime()) == true
|
||||
Log.d("[Tabs Fragment] Keyboard is ${if (keyboardVisible) "visible" else "invisible"}")
|
||||
shouldTabsBeVisibleDueToOrientationAndKeyboard = !portraitOrientation || !keyboardVisible
|
||||
updateTabsFragmentVisibility()
|
||||
}
|
||||
|
||||
initOverlay()
|
||||
|
||||
if (intent != null) {
|
||||
Log.d("[Main Activity] Found post create intent")
|
||||
handleIntentParams(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
findNavController(R.id.nav_host_fragment).removeOnDestinationChangedListener(this)
|
||||
unregisterComponentCallbacks(componentCallbacks)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDestinationChanged(
|
||||
controller: NavController,
|
||||
destination: NavDestination,
|
||||
arguments: Bundle?
|
||||
) {
|
||||
hideKeyboard()
|
||||
if (statusFragment.visibility == View.GONE) {
|
||||
statusFragment.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
shouldTabsBeVisibleDependingOnDestination = when (destination.id) {
|
||||
R.id.masterCallLogsFragment, R.id.masterContactsFragment, R.id.dialerFragment, R.id.masterChatRoomsFragment ->
|
||||
true
|
||||
else -> false
|
||||
}
|
||||
updateTabsFragmentVisibility()
|
||||
}
|
||||
|
||||
fun hideKeyboard() {
|
||||
currentFocus?.hideKeyboard()
|
||||
}
|
||||
|
||||
fun hideStatusFragment(hide: Boolean) {
|
||||
statusFragment.visibility = if (hide) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
private fun updateTabsFragmentVisibility() {
|
||||
tabsFragment.visibility = if (shouldTabsBeVisibleDependingOnDestination && shouldTabsBeVisibleDueToOrientationAndKeyboard) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun handleIntentParams(intent: Intent) {
|
||||
Log.i("[Main Activity] Handling intent with action [${intent.action}], type [${intent.type}] and data [${intent.data}]")
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_MAIN -> handleMainIntent(intent)
|
||||
Intent.ACTION_SEND, Intent.ACTION_SENDTO -> {
|
||||
if (intent.type == "text/plain") {
|
||||
handleSendText(intent)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
handleSendFile(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEND_MULTIPLE -> {
|
||||
lifecycleScope.launch {
|
||||
handleSendMultipleFiles(intent)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
if (
|
||||
intent.type == AppUtils.getString(R.string.linphone_address_mime_type) &&
|
||||
PermissionHelper.get().hasReadContactsPermission()
|
||||
) {
|
||||
val contactId =
|
||||
coreContext.contactsManager.getAndroidContactIdFromUri(uri)
|
||||
if (contactId != null) {
|
||||
Log.i("[Main Activity] Found contact URI parameter in intent: $uri")
|
||||
navigateToContact(contactId)
|
||||
}
|
||||
} else {
|
||||
handleTelOrSipUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_DIAL, Intent.ACTION_CALL -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
handleTelOrSipUri(uri)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW_LOCUS -> {
|
||||
if (corePreferences.disableChat) return
|
||||
val locus = Compatibility.extractLocusIdFromIntent(intent)
|
||||
if (locus != null) {
|
||||
Log.i("[Main Activity] Found chat room locus intent extra: $locus")
|
||||
handleLocusOrShortcut(locus)
|
||||
}
|
||||
}
|
||||
else -> handleMainIntent(intent)
|
||||
}
|
||||
|
||||
// Prevent this intent to be processed again
|
||||
intent.action = null
|
||||
intent.data = null
|
||||
intent.extras?.clear()
|
||||
}
|
||||
|
||||
private fun handleMainIntent(intent: Intent) {
|
||||
when {
|
||||
intent.hasExtra("ContactId") -> {
|
||||
val id = intent.getStringExtra("ContactId")
|
||||
Log.i("[Main Activity] Found contact ID in extras: $id")
|
||||
navigateToContact(id)
|
||||
}
|
||||
intent.hasExtra("Chat") -> {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
if (intent.hasExtra("RemoteSipUri") && intent.hasExtra("LocalSipUri")) {
|
||||
val peerAddress = intent.getStringExtra("RemoteSipUri")
|
||||
val localAddress = intent.getStringExtra("LocalSipUri")
|
||||
Log.i("[Main Activity] Found chat room intent extra: local SIP URI=[$localAddress], peer SIP URI=[$peerAddress]")
|
||||
navigateToChatRoom(localAddress, peerAddress)
|
||||
} else {
|
||||
Log.i("[Main Activity] Found chat intent extra, go to chat rooms list")
|
||||
navigateToChatRooms()
|
||||
}
|
||||
}
|
||||
intent.hasExtra("Dialer") -> {
|
||||
Log.i("[Main Activity] Found dialer intent extra, go to dialer")
|
||||
val args = Bundle()
|
||||
args.putBoolean("Transfer", intent.getBooleanExtra("Transfer", false))
|
||||
navigateToDialer(args)
|
||||
}
|
||||
else -> {
|
||||
val core = coreContext.core
|
||||
val call = core.currentCall ?: core.calls.firstOrNull()
|
||||
if (call != null) {
|
||||
Log.i("[Main Activity] Launcher clicked while there is at least one active call, go to CallActivity")
|
||||
val callIntent = Intent(this, org.linphone.activities.voip.CallActivity::class.java)
|
||||
callIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
startActivity(callIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTelOrSipUri(uri: Uri) {
|
||||
Log.i("[Main Activity] Found uri: $uri to call")
|
||||
val stringUri = uri.toString()
|
||||
var addressToCall: String = stringUri
|
||||
|
||||
when {
|
||||
addressToCall.startsWith("tel:") -> {
|
||||
Log.i("[Main Activity] Removing tel: prefix")
|
||||
addressToCall = addressToCall.substring("tel:".length)
|
||||
}
|
||||
addressToCall.startsWith("linphone:") -> {
|
||||
Log.i("[Main Activity] Removing linphone: prefix")
|
||||
addressToCall = addressToCall.substring("linphone:".length)
|
||||
}
|
||||
addressToCall.startsWith("sip-linphone:") -> {
|
||||
Log.i("[Main Activity] Removing linphone: sip-linphone")
|
||||
addressToCall = addressToCall.substring("sip-linphone:".length)
|
||||
}
|
||||
}
|
||||
|
||||
val address = coreContext.core.interpretUrl(addressToCall, LinphoneUtils.applyInternationalPrefix())
|
||||
if (address != null) {
|
||||
addressToCall = address.asStringUriOnly()
|
||||
}
|
||||
|
||||
Log.i("[Main Activity] Starting dialer with pre-filled URI $addressToCall")
|
||||
val args = Bundle()
|
||||
args.putString("URI", addressToCall)
|
||||
navigateToDialer(args)
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
sharedViewModel.textToShare.value = it
|
||||
}
|
||||
|
||||
handleSendChatRoom(intent)
|
||||
}
|
||||
|
||||
private suspend fun handleSendFile(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
Log.i("[Main Activity] Found single file to share with type ${intent.type}")
|
||||
|
||||
(intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
val list = arrayListOf<String>()
|
||||
coroutineScope {
|
||||
val deferred = async {
|
||||
FileUtils.getFilePath(this@MainActivity, it)
|
||||
}
|
||||
val path = deferred.await()
|
||||
if (path != null) {
|
||||
list.add(path)
|
||||
Log.i("[Main Activity] Found single file to share: $path")
|
||||
}
|
||||
}
|
||||
sharedViewModel.filesToShare.value = list
|
||||
}
|
||||
|
||||
// Check that the current fragment hasn't already handled the event on filesToShare
|
||||
// If it has, don't go further.
|
||||
// For example this may happen when picking a GIF from the keyboard while inside a chat room
|
||||
if (!sharedViewModel.filesToShare.value.isNullOrEmpty()) {
|
||||
handleSendChatRoom(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleSendMultipleFiles(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM)?.let {
|
||||
val list = arrayListOf<String>()
|
||||
coroutineScope {
|
||||
val deferred = arrayListOf<Deferred<String?>>()
|
||||
for (parcelable in it) {
|
||||
val uri = parcelable as Uri
|
||||
deferred.add(async { FileUtils.getFilePath(this@MainActivity, uri) })
|
||||
}
|
||||
val paths = deferred.awaitAll()
|
||||
for (path in paths) {
|
||||
Log.i("[Main Activity] Found file to share: $path")
|
||||
if (path != null) list.add(path)
|
||||
}
|
||||
}
|
||||
sharedViewModel.filesToShare.value = list
|
||||
}
|
||||
|
||||
handleSendChatRoom(intent)
|
||||
}
|
||||
|
||||
private fun handleSendChatRoom(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
Log.i("[Main Activity] Found uri: $uri to send a message to")
|
||||
val stringUri = uri.toString()
|
||||
var addressToIM: String = stringUri
|
||||
try {
|
||||
addressToIM = URLDecoder.decode(stringUri, "UTF-8")
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
Log.e("[Main Activity] UnsupportedEncodingException: $e")
|
||||
}
|
||||
|
||||
when {
|
||||
addressToIM.startsWith("sms:") ->
|
||||
addressToIM = addressToIM.substring("sms:".length)
|
||||
addressToIM.startsWith("smsto:") ->
|
||||
addressToIM = addressToIM.substring("smsto:".length)
|
||||
addressToIM.startsWith("mms:") ->
|
||||
addressToIM = addressToIM.substring("mms:".length)
|
||||
addressToIM.startsWith("mmsto:") ->
|
||||
addressToIM = addressToIM.substring("mmsto:".length)
|
||||
}
|
||||
|
||||
val localAddress =
|
||||
coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
|
||||
val peerAddress = coreContext.core.interpretUrl(addressToIM, LinphoneUtils.applyInternationalPrefix())?.asStringUriOnly()
|
||||
Log.i("[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses")
|
||||
navigateToChatRoom(localAddress, peerAddress)
|
||||
} else {
|
||||
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
|
||||
if (shortcutId != null) {
|
||||
Log.i("[Main Activity] Found shortcut ID: $shortcutId")
|
||||
handleLocusOrShortcut(shortcutId)
|
||||
} else {
|
||||
Log.i("[Main Activity] Going into chat rooms list")
|
||||
navigateToChatRooms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLocusOrShortcut(id: String) {
|
||||
val split = id.split("~")
|
||||
if (split.size == 2) {
|
||||
val localAddress = split[0]
|
||||
val peerAddress = split[1]
|
||||
Log.i("[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses, computed from shortcut/locus id")
|
||||
navigateToChatRoom(localAddress, peerAddress)
|
||||
} else {
|
||||
Log.e("[Main Activity] Failed to parse shortcut/locus id: $id, going to chat rooms list")
|
||||
navigateToChatRooms()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initOverlay() {
|
||||
overlay = binding.root.findViewById(R.id.call_overlay)
|
||||
val callOverlay = overlay
|
||||
callOverlay ?: return
|
||||
|
||||
callOverlay.setOnTouchListener { view, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
overlayX = view.x - event.rawX
|
||||
overlayY = view.y - event.rawY
|
||||
initPosX = view.x
|
||||
initPosY = view.y
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
view.animate()
|
||||
.x(event.rawX + overlayX)
|
||||
.y(event.rawY + overlayY)
|
||||
.setDuration(0)
|
||||
.start()
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (abs(initPosX - view.x) < CorePreferences.OVERLAY_CLICK_SENSITIVITY &&
|
||||
abs(initPosY - view.y) < CorePreferences.OVERLAY_CLICK_SENSITIVITY
|
||||
) {
|
||||
view.performClick()
|
||||
}
|
||||
}
|
||||
else -> return@setOnTouchListener false
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
callOverlay.setOnClickListener {
|
||||
coreContext.onCallOverlayClick()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.about
|
||||
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AboutFragmentBinding
|
||||
|
||||
class AboutFragment : SecureFragment<AboutFragmentBinding>() {
|
||||
private lateinit var viewModel: AboutViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.about_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(this)[AboutViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setPrivacyPolicyClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_privacy_policy_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[About] Failed to start browser intent, $se")
|
||||
}
|
||||
}
|
||||
|
||||
binding.setLicenseClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_license_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[About] Failed to start browser intent, $se")
|
||||
}
|
||||
}
|
||||
|
||||
binding.setWeblateClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_weblate_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[About] Failed to start browser intent, $se")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.about
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
|
||||
class AboutViewModel : ViewModel() {
|
||||
val appVersion: String = coreContext.appVersion
|
||||
|
||||
val sdkVersion: String = coreContext.sdkVersion
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.adapters
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
|
||||
abstract class SelectionListAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
diff: DiffUtil.ItemCallback<T>
|
||||
) :
|
||||
ListAdapter<T, VH>(diff) {
|
||||
|
||||
private var _selectionViewModel: ListTopBarViewModel? = selectionVM
|
||||
protected val selectionViewModel get() = _selectionViewModel!!
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView)
|
||||
_selectionViewModel = null
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
internal abstract class ChatScrollListener(private val mLayoutManager: LinearLayoutManager) :
|
||||
RecyclerView.OnScrollListener() {
|
||||
// The total number of items in the data set after the last load
|
||||
private var previousTotalItemCount = 0
|
||||
// True if we are still waiting for the last set of data to load.
|
||||
private var loading = true
|
||||
|
||||
private var userHasScrolledUp: Boolean = false
|
||||
|
||||
// This happens many times a second during a scroll, so be wary of the code you place here.
|
||||
// We are given a few useful parameters to help us work out if we need to load some more data,
|
||||
// but first we check if we are waiting for the previous load to finish.
|
||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||
val totalItemCount = mLayoutManager.itemCount
|
||||
val firstVisibleItemPosition: Int = mLayoutManager.findFirstVisibleItemPosition()
|
||||
val lastVisibleItemPosition: Int = mLayoutManager.findLastVisibleItemPosition()
|
||||
|
||||
// If the total item count is zero and the previous isn't, assume the
|
||||
// list is invalidated and should be reset back to initial state
|
||||
if (totalItemCount < previousTotalItemCount) {
|
||||
previousTotalItemCount = totalItemCount
|
||||
if (totalItemCount == 0) {
|
||||
loading = true
|
||||
}
|
||||
}
|
||||
|
||||
// If it’s still loading, we check to see if the data set count has
|
||||
// changed, if so we conclude it has finished loading and update the current page
|
||||
// number and total item count.
|
||||
if (loading && totalItemCount > previousTotalItemCount) {
|
||||
loading = false
|
||||
previousTotalItemCount = totalItemCount
|
||||
}
|
||||
|
||||
userHasScrolledUp = lastVisibleItemPosition != totalItemCount - 1
|
||||
if (userHasScrolledUp) {
|
||||
onScrolledUp()
|
||||
} else {
|
||||
onScrolledToEnd()
|
||||
}
|
||||
|
||||
// If it isn’t currently loading, we check to see if we have breached
|
||||
// the mVisibleThreshold and need to reload more data.
|
||||
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
||||
// threshold should reflect how many total columns there are too
|
||||
if (!loading &&
|
||||
firstVisibleItemPosition < mVisibleThreshold &&
|
||||
firstVisibleItemPosition >= 0 &&
|
||||
lastVisibleItemPosition < totalItemCount - mVisibleThreshold
|
||||
) {
|
||||
onLoadMore(totalItemCount)
|
||||
loading = true
|
||||
}
|
||||
}
|
||||
|
||||
// Defines the process for actually loading more data based on page
|
||||
protected abstract fun onLoadMore(totalItemsCount: Int)
|
||||
|
||||
// Called when user has started to scroll up, opposed to onScrolledToEnd()
|
||||
protected abstract fun onScrolledUp()
|
||||
|
||||
// Called when user has scrolled and reached the end of the items
|
||||
protected abstract fun onScrolledToEnd()
|
||||
|
||||
companion object {
|
||||
// The minimum amount of items to have below your current scroll position
|
||||
// before loading more.
|
||||
private const val mVisibleThreshold = 5
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat
|
||||
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatRoomSecurityLevel
|
||||
|
||||
data class GroupChatRoomMember(
|
||||
val address: Address,
|
||||
var isAdmin: Boolean = false,
|
||||
val securityLevel: ChatRoomSecurityLevel = ChatRoomSecurityLevel.ClearText,
|
||||
val hasLimeX3DHCapability: Boolean = false,
|
||||
// A participant not yet added to a group can't be set admin at the same time it's added
|
||||
val canBeSetAdmin: Boolean = false
|
||||
)
|
@ -0,0 +1,519 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupWindow
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.adapters.SelectionListAdapter
|
||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||
import org.linphone.activities.main.chat.data.EventData
|
||||
import org.linphone.activities.main.chat.data.EventLogData
|
||||
import org.linphone.activities.main.chat.data.OnContentClickedListener
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatEventListCellBinding
|
||||
import org.linphone.databinding.ChatMessageListCellBinding
|
||||
import org.linphone.databinding.ChatMessageLongPressMenuBindingImpl
|
||||
import org.linphone.databinding.ChatUnreadMessagesListHeaderBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
|
||||
class ChatMessagesListAdapter(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : SelectionListAdapter<EventLogData, RecyclerView.ViewHolder>(selectionVM, ChatMessageDiffCallback()),
|
||||
HeaderAdapter {
|
||||
companion object {
|
||||
const val MAX_TIME_TO_GROUP_MESSAGES = 60 // 1 minute
|
||||
}
|
||||
|
||||
val resendMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val deleteMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val forwardMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val replyMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val showImdnForMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val addSipUriToContactEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val openContentEvent: MutableLiveData<Event<Content>> by lazy {
|
||||
MutableLiveData<Event<Content>>()
|
||||
}
|
||||
|
||||
val urlClickEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val sipUriClickedEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val callConferenceEvent: MutableLiveData<Event<Pair<String, String?>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String?>>>()
|
||||
}
|
||||
|
||||
val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val errorEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
private val contentClickedListener = object : OnContentClickedListener {
|
||||
override fun onContentClicked(content: Content) {
|
||||
openContentEvent.value = Event(content)
|
||||
}
|
||||
|
||||
override fun onWebUrlClicked(url: String) {
|
||||
if (popup?.isShowing == true) {
|
||||
Log.w("[Chat Message Data] Long press that displayed context menu detected, aborting click on URL [$url]")
|
||||
return
|
||||
}
|
||||
urlClickEvent.value = Event(url)
|
||||
}
|
||||
|
||||
override fun onSipAddressClicked(sipUri: String) {
|
||||
if (popup?.isShowing == true) {
|
||||
Log.w("[Chat Message Data] Long press that displayed context menu detected, aborting click on SIP URI [$sipUri]")
|
||||
return
|
||||
}
|
||||
sipUriClickedEvent.value = Event(sipUri)
|
||||
}
|
||||
|
||||
override fun onCallConference(address: String, subject: String?) {
|
||||
callConferenceEvent.value = Event(Pair(address, subject))
|
||||
}
|
||||
|
||||
override fun onError(messageId: Int) {
|
||||
errorEvent.value = Event(messageId)
|
||||
}
|
||||
}
|
||||
|
||||
private var advancedContextMenuOptionsDisabled: Boolean = false
|
||||
private var popup: PopupWindow? = null
|
||||
|
||||
private var unreadMessagesCount: Int = 0
|
||||
private var firstUnreadMessagePosition: Int = -1
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
EventLog.Type.ConferenceChatMessage.toInt() -> createChatMessageViewHolder(parent)
|
||||
else -> createEventViewHolder(parent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChatMessageViewHolder(parent: ViewGroup): ChatMessageViewHolder {
|
||||
val binding: ChatMessageListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_message_list_cell, parent, false
|
||||
)
|
||||
return ChatMessageViewHolder(binding)
|
||||
}
|
||||
|
||||
private fun createEventViewHolder(parent: ViewGroup): EventViewHolder {
|
||||
val binding: ChatEventListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_event_list_cell, parent, false
|
||||
)
|
||||
return EventViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val eventLog = getItem(position)
|
||||
when (holder) {
|
||||
is ChatMessageViewHolder -> holder.bind(eventLog)
|
||||
is EventViewHolder -> holder.bind(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val eventLog = getItem(position)
|
||||
return eventLog.eventLog.type.toInt()
|
||||
}
|
||||
|
||||
override fun onCurrentListChanged(
|
||||
previousList: MutableList<EventLogData>,
|
||||
currentList: MutableList<EventLogData>
|
||||
) {
|
||||
// Need to wait for messages to be added before computing new first unread message position
|
||||
firstUnreadMessagePosition = -1
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
if (unreadMessagesCount > 0 && firstUnreadMessagePosition == -1) {
|
||||
computeFirstUnreadMessagePosition()
|
||||
}
|
||||
return position == firstUnreadMessagePosition
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val binding: ChatUnreadMessagesListHeaderBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.chat_unread_messages_list_header, null, false
|
||||
)
|
||||
binding.title = AppUtils.getStringWithPlural(R.plurals.chat_room_unread_messages_event, unreadMessagesCount)
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun disableAdvancedContextMenuOptions() {
|
||||
advancedContextMenuOptionsDisabled = true
|
||||
}
|
||||
|
||||
fun setUnreadMessageCount(count: Int, forceUpdate: Boolean) {
|
||||
// Once list has been filled once, don't show the unread message header
|
||||
// when new messages are added to the history whilst it is visible
|
||||
unreadMessagesCount = if (itemCount == 0 || forceUpdate) count else 0
|
||||
firstUnreadMessagePosition = -1
|
||||
}
|
||||
|
||||
fun getFirstUnreadMessagePosition(): Int {
|
||||
return firstUnreadMessagePosition
|
||||
}
|
||||
|
||||
private fun computeFirstUnreadMessagePosition() {
|
||||
if (unreadMessagesCount > 0) {
|
||||
var messageCount = 0
|
||||
for (position in itemCount - 1 downTo 0) {
|
||||
val eventLog = getItem(position)
|
||||
val data = eventLog.data
|
||||
if (data is ChatMessageData) {
|
||||
messageCount += 1
|
||||
if (messageCount == unreadMessagesCount) {
|
||||
firstUnreadMessagePosition = position
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ChatMessageViewHolder(
|
||||
val binding: ChatMessageListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(eventLog: EventLogData) {
|
||||
with(binding) {
|
||||
if (eventLog.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessageViewModel = eventLog.data as ChatMessageData
|
||||
chatMessageViewModel.setContentClickListener(contentClickedListener)
|
||||
|
||||
val chatMessage = chatMessageViewModel.chatMessage
|
||||
data = chatMessageViewModel
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
setReplyClickListener {
|
||||
val reply = chatMessageViewModel.replyData.value?.chatMessage
|
||||
if (reply != null) {
|
||||
scrollToChatMessageEvent.value = Event(reply)
|
||||
}
|
||||
}
|
||||
|
||||
// Grouping
|
||||
var hasPrevious = false
|
||||
var hasNext = false
|
||||
|
||||
if (bindingAdapterPosition > 0) {
|
||||
val previousItem = getItem(bindingAdapterPosition - 1)
|
||||
if (previousItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val previousMessage = previousItem.eventLog.chatMessage
|
||||
if (previousMessage != null && previousMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
|
||||
if (chatMessage.time - previousMessage.time < MAX_TIME_TO_GROUP_MESSAGES) {
|
||||
hasPrevious = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bindingAdapterPosition >= 0 && bindingAdapterPosition < itemCount - 1) {
|
||||
val nextItem = getItem(bindingAdapterPosition + 1)
|
||||
if (nextItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val nextMessage = nextItem.eventLog.chatMessage
|
||||
if (nextMessage != null && nextMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
|
||||
if (nextMessage.time - chatMessage.time < MAX_TIME_TO_GROUP_MESSAGES) {
|
||||
hasNext = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatMessageViewModel.updateBubbleBackground(hasPrevious, hasNext)
|
||||
|
||||
executePendingBindings()
|
||||
|
||||
setContextMenuClickListener {
|
||||
val popupView: ChatMessageLongPressMenuBindingImpl = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(root.context),
|
||||
R.layout.chat_message_long_press_menu, null, false
|
||||
)
|
||||
|
||||
val itemSize = AppUtils.getDimension(R.dimen.chat_message_popup_item_height).toInt()
|
||||
var totalSize = itemSize * 7
|
||||
if (chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
|
||||
// No message id
|
||||
popupView.imdnHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.state != ChatMessage.State.NotDelivered) {
|
||||
popupView.resendHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.contents.find { content -> content.isText } == null) {
|
||||
popupView.copyTextHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.isOutgoing ||
|
||||
chatMessageViewModel.contact.value != null ||
|
||||
advancedContextMenuOptionsDisabled
|
||||
) {
|
||||
popupView.addToContactsHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.chatRoom.isReadOnly) {
|
||||
popupView.replyHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (advancedContextMenuOptionsDisabled) {
|
||||
popupView.forwardHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
|
||||
// When using WRAP_CONTENT instead of real size, fails to place the
|
||||
// popup window above if not enough space is available below
|
||||
val popupWindow = PopupWindow(
|
||||
popupView.root,
|
||||
AppUtils.getDimension(R.dimen.chat_message_popup_width).toInt(),
|
||||
totalSize,
|
||||
true
|
||||
)
|
||||
popup = popupWindow
|
||||
|
||||
// Elevation is for showing a shadow around the popup
|
||||
popupWindow.elevation = 20f
|
||||
|
||||
popupView.setResendClickListener {
|
||||
resendMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setCopyTextClickListener {
|
||||
copyTextToClipboard()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setForwardClickListener {
|
||||
forwardMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setReplyClickListener {
|
||||
replyMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setImdnClickListener {
|
||||
showImdnDeliveryFragment()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setAddToContactsClickListener {
|
||||
addSenderToContacts()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setDeleteClickListener {
|
||||
deleteMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
|
||||
val gravity = if (chatMessage.isOutgoing) Gravity.END else Gravity.START
|
||||
popupWindow.showAsDropDown(background, 0, 0, gravity or Gravity.TOP)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resendMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
chatMessage.userData = bindingAdapterPosition
|
||||
resendMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyTextToClipboard() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
val content = chatMessage.contents.find { content -> content.isText }
|
||||
if (content != null) {
|
||||
val clipboard: ClipboardManager =
|
||||
coreContext.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Message", content.utf8Text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun forwardMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
forwardMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun replyMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
replyMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showImdnDeliveryFragment() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
showImdnForMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
chatMessage.userData = bindingAdapterPosition
|
||||
deleteMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSenderToContacts() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
val copy = chatMessage.fromAddress.clone()
|
||||
copy.clean() // To remove gruu if any
|
||||
addSipUriToContactEvent.value = Event(copy.asStringUriOnly())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class EventViewHolder(
|
||||
private val binding: ChatEventListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(eventLog: EventLogData) {
|
||||
with(binding) {
|
||||
val eventViewModel = eventLog.data as EventData
|
||||
data = eventViewModel
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
binding.setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ChatMessageDiffCallback : DiffUtil.ItemCallback<EventLogData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: EventLogData,
|
||||
newItem: EventLogData
|
||||
): Boolean {
|
||||
return if (oldItem.type == EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.type == EventLog.Type.ConferenceChatMessage
|
||||
) {
|
||||
val oldData = (oldItem.data as ChatMessageData)
|
||||
val newData = (newItem.data as ChatMessageData)
|
||||
|
||||
oldData.time.value == newData.time.value &&
|
||||
oldData.isOutgoing == newData.isOutgoing
|
||||
} else oldItem.notifyId == newItem.notifyId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: EventLogData,
|
||||
newItem: EventLogData
|
||||
): Boolean {
|
||||
return if (oldItem.type == EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.type == EventLog.Type.ConferenceChatMessage
|
||||
) {
|
||||
val oldData = (oldItem.data as ChatMessageData)
|
||||
val newData = (newItem.data as ChatMessageData)
|
||||
|
||||
val previous = oldData.hasPreviousMessage == newData.hasPreviousMessage
|
||||
val next = oldData.hasNextMessage == newData.hasNextMessage
|
||||
val isDisplayed = newData.isDisplayed.value == true
|
||||
isDisplayed && previous && next
|
||||
} else {
|
||||
oldItem.type != EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.type != EventLog.Type.ConferenceChatMessage
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.adapters.SelectionListAdapter
|
||||
import org.linphone.activities.main.chat.data.ChatRoomData
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.databinding.ChatRoomListCellBinding
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class ChatRoomsListAdapter(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : SelectionListAdapter<ChatRoom, RecyclerView.ViewHolder>(selectionVM, ChatRoomDiffCallback()) {
|
||||
val selectedChatRoomEvent: MutableLiveData<Event<ChatRoom>> by lazy {
|
||||
MutableLiveData<Event<ChatRoom>>()
|
||||
}
|
||||
|
||||
private var isForwardPending = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_list_cell, parent, false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
fun forwardPending(pending: Boolean) {
|
||||
isForwardPending = pending
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ChatRoomListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(chatRoom: ChatRoom) {
|
||||
with(binding) {
|
||||
data = ChatRoomData(chatRoom)
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
forwardPending = isForwardPending
|
||||
|
||||
setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
} else {
|
||||
selectedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
setLongClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == false) {
|
||||
selectionViewModel.isEditionEnabled.value = true
|
||||
// Selection will be handled by click listener
|
||||
true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ChatRoomDiffCallback : DiffUtil.ItemCallback<ChatRoom>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ChatRoom,
|
||||
newItem: ChatRoom
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ChatRoom,
|
||||
newItem: ChatRoom
|
||||
): Boolean {
|
||||
return false // To force redraw
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.data.GroupInfoParticipantData
|
||||
import org.linphone.databinding.ChatRoomGroupInfoParticipantCellBinding
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class GroupInfoParticipantsAdapter(
|
||||
private val viewLifecycleOwner: LifecycleOwner,
|
||||
private val isEncryptionEnabled: Boolean
|
||||
) : ListAdapter<GroupInfoParticipantData, RecyclerView.ViewHolder>(ParticipantDiffCallback()) {
|
||||
private var showAdmin: Boolean = false
|
||||
|
||||
val participantRemovedEvent: MutableLiveData<Event<GroupChatRoomMember>> by lazy {
|
||||
MutableLiveData<Event<GroupChatRoomMember>>()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomGroupInfoParticipantCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_group_info_participant_cell, parent, false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
fun showAdminControls(show: Boolean) {
|
||||
showAdmin = show
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: ChatRoomGroupInfoParticipantCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(participantViewModel: GroupInfoParticipantData) {
|
||||
with(binding) {
|
||||
participantViewModel.showAdminControls.value = showAdmin
|
||||
data = participantViewModel
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
setRemoveClickListener {
|
||||
participantRemovedEvent.value = Event(participantViewModel.participant)
|
||||
}
|
||||
isEncrypted = isEncryptionEnabled
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ParticipantDiffCallback : DiffUtil.ItemCallback<GroupInfoParticipantData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: GroupInfoParticipantData,
|
||||
newItem: GroupInfoParticipantData
|
||||
): Boolean {
|
||||
return oldItem.sipUri == newItem.sipUri
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: GroupInfoParticipantData,
|
||||
newItem: GroupInfoParticipantData
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.ImdnParticipantData
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.databinding.ChatRoomImdnParticipantCellBinding
|
||||
import org.linphone.databinding.ImdnListHeaderBinding
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
|
||||
class ImdnAdapter(
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : ListAdapter<ImdnParticipantData, RecyclerView.ViewHolder>(ParticipantImdnStateDiffCallback()), HeaderAdapter {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomImdnParticipantCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_imdn_participant_cell, parent, false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: ChatRoomImdnParticipantCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(participantImdnData: ImdnParticipantData) {
|
||||
with(binding) {
|
||||
data = participantImdnData
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
if (position >= itemCount) return false
|
||||
val participantImdnState = getItem(position)
|
||||
val previousPosition = position - 1
|
||||
return if (previousPosition >= 0) {
|
||||
getItem(previousPosition).imdnState.state != participantImdnState.imdnState.state
|
||||
} else true
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val participantImdnState = getItem(position).imdnState
|
||||
val binding: ImdnListHeaderBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.imdn_list_header, null, false
|
||||
)
|
||||
when (participantImdnState.state) {
|
||||
ChatMessage.State.Displayed -> {
|
||||
binding.title = R.string.chat_message_imdn_displayed
|
||||
binding.textColor = R.color.imdn_read_color
|
||||
binding.icon = R.drawable.chat_read
|
||||
}
|
||||
ChatMessage.State.DeliveredToUser -> {
|
||||
binding.title = R.string.chat_message_imdn_delivered
|
||||
binding.textColor = R.color.grey_color
|
||||
binding.icon = R.drawable.chat_delivered
|
||||
}
|
||||
ChatMessage.State.Delivered -> {
|
||||
binding.title = R.string.chat_message_imdn_sent
|
||||
binding.textColor = R.color.grey_color
|
||||
binding.icon = R.drawable.chat_delivered
|
||||
}
|
||||
ChatMessage.State.NotDelivered -> {
|
||||
binding.title = R.string.chat_message_imdn_undelivered
|
||||
binding.textColor = R.color.red_color
|
||||
binding.icon = R.drawable.chat_error
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
|
||||
private class ParticipantImdnStateDiffCallback : DiffUtil.ItemCallback<ImdnParticipantData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ImdnParticipantData,
|
||||
newItem: ImdnParticipantData
|
||||
): Boolean {
|
||||
return oldItem.sipUri == newItem.sipUri
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ImdnParticipantData,
|
||||
newItem: ImdnParticipantData
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class ChatMessageAttachmentData(
|
||||
val path: String,
|
||||
private val deleteCallback: (attachment: ChatMessageAttachmentData) -> Unit
|
||||
) {
|
||||
val fileName: String = FileUtils.getNameFromFilePath(path)
|
||||
val isImage: Boolean = FileUtils.isExtensionImage(path)
|
||||
val isVideo: Boolean = FileUtils.isExtensionVideo(path)
|
||||
val isAudio: Boolean = FileUtils.isExtensionAudio(path)
|
||||
val isPdf: Boolean = FileUtils.isExtensionPdf(path)
|
||||
|
||||
fun delete() {
|
||||
deleteCallback(this)
|
||||
}
|
||||
}
|
@ -0,0 +1,491 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.UnderlineSpan
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileReader
|
||||
import java.lang.StringBuilder
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatMessageContentData(
|
||||
private val chatMessage: ChatMessage,
|
||||
private val contentIndex: Int,
|
||||
) {
|
||||
var listener: OnContentClickedListener? = null
|
||||
|
||||
val isOutgoing = chatMessage.isOutgoing
|
||||
|
||||
val isImage = MutableLiveData<Boolean>()
|
||||
val isVideo = MutableLiveData<Boolean>()
|
||||
val isAudio = MutableLiveData<Boolean>()
|
||||
val isPdf = MutableLiveData<Boolean>()
|
||||
val isGenericFile = MutableLiveData<Boolean>()
|
||||
val isVoiceRecording = MutableLiveData<Boolean>()
|
||||
val isConferenceSchedule = MutableLiveData<Boolean>()
|
||||
val isConferenceUpdated = MutableLiveData<Boolean>()
|
||||
val isConferenceCancelled = MutableLiveData<Boolean>()
|
||||
|
||||
val fileName = MutableLiveData<String>()
|
||||
val filePath = MutableLiveData<String>()
|
||||
|
||||
val downloadable = MutableLiveData<Boolean>()
|
||||
val fileTransferProgress = MutableLiveData<Boolean>()
|
||||
val fileTransferProgressInt = MutableLiveData<Int>()
|
||||
val downloadLabel = MutableLiveData<Spannable>()
|
||||
|
||||
val voiceRecordDuration = MutableLiveData<Int>()
|
||||
val formattedDuration = MutableLiveData<String>()
|
||||
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
||||
val isVoiceRecordPlaying = MutableLiveData<Boolean>()
|
||||
|
||||
val conferenceSubject = MutableLiveData<String>()
|
||||
val conferenceDescription = MutableLiveData<String>()
|
||||
val conferenceParticipantCount = MutableLiveData<String>()
|
||||
val conferenceDate = MutableLiveData<String>()
|
||||
val conferenceTime = MutableLiveData<String>()
|
||||
val conferenceDuration = MutableLiveData<String>()
|
||||
var conferenceAddress = MutableLiveData<String>()
|
||||
val showDuration = MutableLiveData<Boolean>()
|
||||
|
||||
val isAlone: Boolean
|
||||
get() {
|
||||
var count = 0
|
||||
for (content in chatMessage.contents) {
|
||||
if (content.isFileTransfer || content.isFile) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count == 1
|
||||
}
|
||||
|
||||
private var isFileEncrypted: Boolean = false
|
||||
|
||||
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
||||
|
||||
private lateinit var voiceRecordingPlayer: Player
|
||||
private val playerListener = PlayerListener {
|
||||
Log.i("[Voice Recording] End of file reached")
|
||||
stopVoiceRecording()
|
||||
}
|
||||
|
||||
private fun getContent(): Content {
|
||||
return chatMessage.contents[contentIndex]
|
||||
}
|
||||
|
||||
private val chatMessageListener: ChatMessageListenerStub = object : ChatMessageListenerStub() {
|
||||
override fun onFileTransferProgressIndication(
|
||||
message: ChatMessage,
|
||||
c: Content,
|
||||
offset: Int,
|
||||
total: Int
|
||||
) {
|
||||
if (c.filePath == getContent().filePath) {
|
||||
if (fileTransferProgress.value == false) {
|
||||
fileTransferProgress.value = true
|
||||
}
|
||||
val percent = ((offset * 100.0) / total).toInt() // Conversion from int to double and back to int is required
|
||||
Log.d("[Content] Transfer progress is: $offset / $total -> $percent%")
|
||||
fileTransferProgressInt.value = percent
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
|
||||
if (state == ChatMessage.State.FileTransferDone || state == ChatMessage.State.FileTransferError) {
|
||||
fileTransferProgress.value = false
|
||||
updateContent()
|
||||
|
||||
if (state == ChatMessage.State.FileTransferDone) {
|
||||
Log.i("[Chat Message] File transfer done")
|
||||
if (!message.isOutgoing && !message.isEphemeral) {
|
||||
Log.i("[Chat Message] Adding content to media store")
|
||||
coreContext.addContentToMediaStore(getContent())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
isVoiceRecordPlaying.value = false
|
||||
voiceRecordDuration.value = 0
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
fileTransferProgress.value = false
|
||||
fileTransferProgressInt.value = 0
|
||||
|
||||
updateContent()
|
||||
chatMessage.addListener(chatMessageListener)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
scope.cancel()
|
||||
|
||||
deletePlainFilePath()
|
||||
chatMessage.removeListener(chatMessageListener)
|
||||
|
||||
if (this::voiceRecordingPlayer.isInitialized) {
|
||||
Log.i("[Voice Recording] Destroying voice record")
|
||||
stopVoiceRecording()
|
||||
voiceRecordingPlayer.removeListener(playerListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun download() {
|
||||
if (chatMessage.isFileTransferInProgress) {
|
||||
Log.w("[Content] Another FileTransfer content for this message is currently being downloaded, can't start another one for now")
|
||||
listener?.onError(R.string.chat_message_download_already_in_progress)
|
||||
return
|
||||
}
|
||||
|
||||
val content = getContent()
|
||||
val filePath = content.filePath
|
||||
if (content.isFileTransfer) {
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
val contentName = content.name
|
||||
if (contentName != null) {
|
||||
val file = FileUtils.getFileStoragePath(contentName)
|
||||
content.filePath = file.path
|
||||
Log.i("[Content] Started downloading $contentName into ${content.filePath}")
|
||||
} else {
|
||||
Log.e("[Content] Content name is null, can't download it!")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Log.w("[Content] File path already set [$filePath] using it (auto download that failed probably)")
|
||||
}
|
||||
|
||||
if (!chatMessage.downloadContent(content)) {
|
||||
Log.e("[Content] Failed to start content download!")
|
||||
}
|
||||
} else {
|
||||
Log.e("[Content] Content is not a FileTransfer, can't download it!")
|
||||
}
|
||||
}
|
||||
|
||||
fun openFile() {
|
||||
listener?.onContentClicked(getContent())
|
||||
}
|
||||
|
||||
private fun deletePlainFilePath() {
|
||||
val path = filePath.value.orEmpty()
|
||||
if (path.isNotEmpty() && isFileEncrypted) {
|
||||
Log.i("[Content] Deleting file used for preview: $path")
|
||||
FileUtils.deleteFile(path)
|
||||
filePath.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateContent() {
|
||||
Log.i("[Content] Updating content")
|
||||
deletePlainFilePath()
|
||||
|
||||
val content = getContent()
|
||||
isFileEncrypted = content.isFileEncrypted
|
||||
Log.i("[Content] Is ${if (content.isFile) "file" else "file transfer"} content encrypted ? $isFileEncrypted")
|
||||
|
||||
filePath.value = ""
|
||||
fileName.value = if (content.name.isNullOrEmpty() && !content.filePath.isNullOrEmpty()) {
|
||||
FileUtils.getNameFromFilePath(content.filePath!!)
|
||||
} else {
|
||||
content.name
|
||||
}
|
||||
|
||||
// Display download size and underline text
|
||||
val fileSize = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
|
||||
val spannable = SpannableString("${AppUtils.getString(R.string.chat_message_download_file)} ($fileSize)")
|
||||
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
downloadLabel.value = spannable
|
||||
|
||||
isImage.value = false
|
||||
isVideo.value = false
|
||||
isAudio.value = false
|
||||
isPdf.value = false
|
||||
isVoiceRecording.value = false
|
||||
isConferenceSchedule.value = false
|
||||
isConferenceUpdated.value = false
|
||||
isConferenceCancelled.value = false
|
||||
|
||||
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
|
||||
val path = if (isFileEncrypted) {
|
||||
Log.i("[Content] Content is encrypted, requesting plain file path")
|
||||
content.exportPlainFile()
|
||||
} else {
|
||||
content.filePath ?: ""
|
||||
}
|
||||
downloadable.value = content.filePath.orEmpty().isEmpty()
|
||||
|
||||
val isVoiceRecord = content.isVoiceRecording
|
||||
isVoiceRecording.value = isVoiceRecord
|
||||
|
||||
val isConferenceIcs = content.isIcalendar
|
||||
isConferenceSchedule.value = isConferenceIcs
|
||||
|
||||
if (path.isNotEmpty()) {
|
||||
Log.i("[Content] Found displayable content: $path")
|
||||
filePath.value = path
|
||||
isImage.value = FileUtils.isExtensionImage(path)
|
||||
isVideo.value = FileUtils.isExtensionVideo(path) && !isVoiceRecord
|
||||
isAudio.value = FileUtils.isExtensionAudio(path) && !isVoiceRecord
|
||||
isPdf.value = FileUtils.isExtensionPdf(path)
|
||||
|
||||
if (isVoiceRecord) {
|
||||
val duration = content.fileDuration // duration is in ms
|
||||
voiceRecordDuration.value = duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)
|
||||
Log.i("[Content] Voice recording duration is ${voiceRecordDuration.value} ($duration)")
|
||||
} else if (isConferenceIcs) {
|
||||
parseConferenceInvite(content)
|
||||
}
|
||||
} else if (isConferenceIcs) {
|
||||
Log.i("[Content] Found content with icalendar file")
|
||||
parseConferenceInvite(content)
|
||||
} else {
|
||||
Log.w("[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path...")
|
||||
isImage.value = false
|
||||
isVideo.value = false
|
||||
isAudio.value = false
|
||||
isPdf.value = false
|
||||
isVoiceRecording.value = false
|
||||
isConferenceSchedule.value = false
|
||||
}
|
||||
} else if (content.isFileTransfer) {
|
||||
downloadable.value = true
|
||||
isImage.value = FileUtils.isExtensionImage(fileName.value!!)
|
||||
isVideo.value = FileUtils.isExtensionVideo(fileName.value!!)
|
||||
isAudio.value = FileUtils.isExtensionAudio(fileName.value!!)
|
||||
isPdf.value = FileUtils.isExtensionPdf(fileName.value!!)
|
||||
isVoiceRecording.value = false
|
||||
isConferenceSchedule.value = false
|
||||
} else if (content.isIcalendar) {
|
||||
Log.i("[Content] Found content with icalendar body")
|
||||
isConferenceSchedule.value = true
|
||||
parseConferenceInvite(content)
|
||||
} else {
|
||||
Log.w("[Content] Found content that's neither a file or a file transfer")
|
||||
}
|
||||
|
||||
isGenericFile.value = !isPdf.value!! && !isAudio.value!! && !isVideo.value!! && !isImage.value!! && !isVoiceRecording.value!! && !isConferenceSchedule.value!!
|
||||
}
|
||||
|
||||
private fun parseConferenceInvite(content: Content) {
|
||||
val conferenceInfo = Factory.instance().createConferenceInfoFromIcalendarContent(content)
|
||||
val conferenceUri = conferenceInfo?.uri?.asStringUriOnly()
|
||||
if (conferenceInfo != null && conferenceUri != null) {
|
||||
conferenceAddress.value = conferenceUri!!
|
||||
Log.i("[Content] Created conference info from ICS with address ${conferenceAddress.value}")
|
||||
conferenceSubject.value = conferenceInfo.subject
|
||||
conferenceDescription.value = conferenceInfo.description
|
||||
|
||||
val state = conferenceInfo.state
|
||||
isConferenceUpdated.value = state == ConferenceInfo.State.Updated
|
||||
isConferenceCancelled.value = state == ConferenceInfo.State.Cancelled
|
||||
|
||||
conferenceDate.value = TimestampUtils.dateToString(conferenceInfo.dateTime)
|
||||
conferenceTime.value = TimestampUtils.timeToString(conferenceInfo.dateTime)
|
||||
|
||||
val minutes = conferenceInfo.duration
|
||||
val hours = TimeUnit.MINUTES.toHours(minutes.toLong())
|
||||
val remainMinutes = minutes - TimeUnit.HOURS.toMinutes(hours).toInt()
|
||||
conferenceDuration.value = TimestampUtils.durationToString(hours.toInt(), remainMinutes)
|
||||
showDuration.value = minutes > 0
|
||||
|
||||
// Check if organizer is part of participants list
|
||||
var participantsCount = conferenceInfo.participants.size
|
||||
val organizer = conferenceInfo.organizer
|
||||
var organizerFound = false
|
||||
if (organizer != null) {
|
||||
for (participant in conferenceInfo.participants) {
|
||||
if (participant.weakEqual(organizer)) {
|
||||
organizerFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!organizerFound) participantsCount += 1 // +1 for organizer
|
||||
conferenceParticipantCount.value = String.format(AppUtils.getString(R.string.conference_invite_participants_count), participantsCount)
|
||||
} else if (conferenceInfo == null) {
|
||||
if (content.filePath != null) {
|
||||
try {
|
||||
val br = BufferedReader(FileReader(content.filePath))
|
||||
var line: String?
|
||||
val textBuilder = StringBuilder()
|
||||
while (br.readLine().also { line = it } != null) {
|
||||
textBuilder.append(line)
|
||||
textBuilder.append('\n')
|
||||
}
|
||||
br.close()
|
||||
Log.e("[Content] Failed to create conference info from ICS file [${content.filePath}]: $textBuilder")
|
||||
} catch (e: Exception) {
|
||||
Log.e("[Content] Failed to read content of ICS file [${content.filePath}]: $e")
|
||||
}
|
||||
} else {
|
||||
Log.e("[Content] Failed to create conference info from ICS: ${content.utf8Text}")
|
||||
}
|
||||
} else if (conferenceInfo.uri == null) {
|
||||
Log.e("[Content] Failed to find the conference URI in conference info [$conferenceInfo]")
|
||||
}
|
||||
}
|
||||
|
||||
fun callConferenceAddress() {
|
||||
val address = conferenceAddress.value
|
||||
if (address == null) {
|
||||
Log.e("[Content] Can't call null conference address!")
|
||||
return
|
||||
}
|
||||
listener?.onCallConference(address, conferenceSubject.value)
|
||||
}
|
||||
|
||||
/** Voice recording specifics */
|
||||
|
||||
fun playVoiceRecording() {
|
||||
Log.i("[Voice Recording] Playing voice record")
|
||||
if (isPlayerClosed()) {
|
||||
Log.w("[Voice Recording] Player closed, let's open it first")
|
||||
initVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
if (AppUtils.isMediaVolumeLow(coreContext.context)) {
|
||||
Toast.makeText(coreContext.context, R.string.chat_message_voice_recording_playback_low_volume, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AppUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
voiceRecordingPlayer.start()
|
||||
isVoiceRecordPlaying.value = true
|
||||
tickerFlow().onEach {
|
||||
withContext(Dispatchers.Main) {
|
||||
voiceRecordPlayingPosition.value = voiceRecordingPlayer.currentPosition
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun pauseVoiceRecording() {
|
||||
Log.i("[Voice Recording] Pausing voice record")
|
||||
if (!isPlayerClosed()) {
|
||||
voiceRecordingPlayer.pause()
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isVoiceRecordPlaying.value = false
|
||||
}
|
||||
|
||||
private fun tickerFlow() = flow {
|
||||
while (isVoiceRecordPlaying.value == true) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initVoiceRecordPlayer() {
|
||||
Log.i("[Voice Recording] Creating player for voice record")
|
||||
// In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
|
||||
// If none are available, default one will be used
|
||||
var headphonesCard: String? = null
|
||||
var speakerCard: String? = null
|
||||
var earpieceCard: String? = null
|
||||
for (device in coreContext.core.audioDevices) {
|
||||
if (device.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
|
||||
when (device.type) {
|
||||
AudioDevice.Type.Speaker -> {
|
||||
speakerCard = device.id
|
||||
}
|
||||
AudioDevice.Type.Earpiece -> {
|
||||
earpieceCard = device.id
|
||||
}
|
||||
AudioDevice.Type.Headphones, AudioDevice.Type.Headset -> {
|
||||
headphonesCard = device.id
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i("[Voice Recording] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
|
||||
|
||||
val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
|
||||
if (localPlayer != null) {
|
||||
voiceRecordingPlayer = localPlayer
|
||||
} else {
|
||||
Log.e("[Voice Recording] Couldn't create local player!")
|
||||
return
|
||||
}
|
||||
voiceRecordingPlayer.addListener(playerListener)
|
||||
|
||||
val path = filePath.value
|
||||
voiceRecordingPlayer.open(path.orEmpty())
|
||||
voiceRecordDuration.value = voiceRecordingPlayer.duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(voiceRecordingPlayer.duration) // is already in milliseconds
|
||||
Log.i("[Voice Recording] Duration is ${voiceRecordDuration.value} (${voiceRecordingPlayer.duration})")
|
||||
}
|
||||
|
||||
private fun stopVoiceRecording() {
|
||||
if (!isPlayerClosed()) {
|
||||
Log.i("[Voice Recording] Stopping voice record")
|
||||
pauseVoiceRecording()
|
||||
voiceRecordingPlayer.seek(0)
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
voiceRecordingPlayer.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPlayerClosed(): Boolean {
|
||||
return !this::voiceRecordingPlayer.isInitialized || voiceRecordingPlayer.state == Player.State.Closed
|
||||
}
|
||||
}
|
||||
|
||||
interface OnContentClickedListener {
|
||||
fun onContentClicked(content: Content)
|
||||
|
||||
fun onSipAddressClicked(sipUri: String)
|
||||
|
||||
fun onWebUrlClicked(url: String)
|
||||
|
||||
fun onCallConference(address: String, subject: String?)
|
||||
|
||||
fun onError(messageId: Int)
|
||||
}
|
@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import android.text.Spannable
|
||||
import android.util.Patterns
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.util.regex.Pattern
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatMessageListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.PatternClickableSpan
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) {
|
||||
private var contentListener: OnContentClickedListener? = null
|
||||
|
||||
val sendInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val showImdn = MutableLiveData<Boolean>()
|
||||
|
||||
val imdnIcon = MutableLiveData<Int>()
|
||||
|
||||
val backgroundRes = MutableLiveData<Int>()
|
||||
|
||||
val hideAvatar = MutableLiveData<Boolean>()
|
||||
|
||||
val hideTime = MutableLiveData<Boolean>()
|
||||
|
||||
val contents = MutableLiveData<ArrayList<ChatMessageContentData>>()
|
||||
|
||||
val time = MutableLiveData<String>()
|
||||
|
||||
val ephemeralLifetime = MutableLiveData<String>()
|
||||
|
||||
val text = MutableLiveData<Spannable>()
|
||||
|
||||
val replyData = MutableLiveData<ChatMessageData>()
|
||||
|
||||
val isDisplayed = MutableLiveData<Boolean>()
|
||||
|
||||
val isOutgoing = chatMessage.isOutgoing
|
||||
|
||||
var hasPreviousMessage = false
|
||||
var hasNextMessage = false
|
||||
|
||||
private var countDownTimer: CountDownTimer? = null
|
||||
|
||||
private val listener = object : ChatMessageListenerStub() {
|
||||
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
|
||||
time.value = TimestampUtils.toString(chatMessage.time)
|
||||
updateChatMessageState(state)
|
||||
}
|
||||
|
||||
override fun onEphemeralMessageTimerStarted(message: ChatMessage) {
|
||||
updateEphemeralTimer()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatMessage.addListener(listener)
|
||||
|
||||
backgroundRes.value = if (chatMessage.isOutgoing) R.drawable.chat_bubble_outgoing_full else R.drawable.chat_bubble_incoming_full
|
||||
hideAvatar.value = false
|
||||
|
||||
if (chatMessage.isReply) {
|
||||
val reply = chatMessage.replyMessage
|
||||
if (reply != null) {
|
||||
Log.i("[Chat Message Data] Message is a reply of message id [${chatMessage.replyMessageId}] sent by [${chatMessage.replyMessageSenderAddress?.asStringUriOnly()}]")
|
||||
replyData.value = ChatMessageData(reply)
|
||||
}
|
||||
}
|
||||
|
||||
time.value = TimestampUtils.toString(chatMessage.time)
|
||||
updateEphemeralTimer()
|
||||
|
||||
updateChatMessageState(chatMessage.state)
|
||||
updateContentsList()
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
super.destroy()
|
||||
|
||||
if (chatMessage.isReply) {
|
||||
replyData.value?.destroy()
|
||||
}
|
||||
|
||||
contents.value.orEmpty().forEach(ChatMessageContentData::destroy)
|
||||
chatMessage.removeListener(listener)
|
||||
contentListener = null
|
||||
}
|
||||
|
||||
fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) {
|
||||
hasPreviousMessage = hasPrevious
|
||||
hasNextMessage = hasNext
|
||||
hideTime.value = false
|
||||
hideAvatar.value = false
|
||||
|
||||
if (hasPrevious) {
|
||||
hideTime.value = true
|
||||
}
|
||||
|
||||
if (chatMessage.isOutgoing) {
|
||||
if (hasNext && hasPrevious) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_2
|
||||
} else if (hasNext) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_1
|
||||
} else if (hasPrevious) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_3
|
||||
} else {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_full
|
||||
}
|
||||
} else {
|
||||
if (hasNext && hasPrevious) {
|
||||
hideAvatar.value = true
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_2
|
||||
} else if (hasNext) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_1
|
||||
} else if (hasPrevious) {
|
||||
hideAvatar.value = true
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_3
|
||||
} else {
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setContentClickListener(listener: OnContentClickedListener) {
|
||||
contentListener = listener
|
||||
|
||||
for (data in contents.value.orEmpty()) {
|
||||
data.listener = listener
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChatMessageState(state: ChatMessage.State) {
|
||||
sendInProgress.value = when (state) {
|
||||
ChatMessage.State.InProgress, ChatMessage.State.FileTransferInProgress, ChatMessage.State.FileTransferDone -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
showImdn.value = when (state) {
|
||||
ChatMessage.State.DeliveredToUser, ChatMessage.State.Displayed,
|
||||
ChatMessage.State.NotDelivered, ChatMessage.State.FileTransferError -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
imdnIcon.value = when (state) {
|
||||
ChatMessage.State.DeliveredToUser -> R.drawable.chat_delivered
|
||||
ChatMessage.State.Displayed -> R.drawable.chat_read
|
||||
ChatMessage.State.FileTransferError, ChatMessage.State.NotDelivered -> R.drawable.chat_error
|
||||
else -> R.drawable.chat_error
|
||||
}
|
||||
|
||||
isDisplayed.value = state == ChatMessage.State.Displayed
|
||||
}
|
||||
|
||||
private fun updateContentsList() {
|
||||
contents.value.orEmpty().forEach(ChatMessageContentData::destroy)
|
||||
val list = arrayListOf<ChatMessageContentData>()
|
||||
|
||||
val contentsList = chatMessage.contents
|
||||
for (index in contentsList.indices) {
|
||||
val content = contentsList[index]
|
||||
if (content.isFileTransfer || content.isFile || content.isIcalendar) {
|
||||
val data = ChatMessageContentData(chatMessage, index)
|
||||
data.listener = contentListener
|
||||
list.add(data)
|
||||
} else if (content.isText) {
|
||||
val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim())
|
||||
text.value = PatternClickableSpan()
|
||||
.add(
|
||||
Pattern.compile("(?:<?sips?:)?[a-zA-Z0-9+_.\\-]+(?:@([a-zA-Z0-9+_.\\-;=]+))+(>)?"),
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on SIP URI: $text")
|
||||
contentListener?.onSipAddressClicked(text)
|
||||
}
|
||||
}
|
||||
)
|
||||
.add(
|
||||
Patterns.WEB_URL,
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on web URL: $text")
|
||||
contentListener?.onWebUrlClicked(text)
|
||||
}
|
||||
}
|
||||
)
|
||||
.add(
|
||||
Patterns.PHONE,
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on phone number: $text")
|
||||
contentListener?.onSipAddressClicked(text)
|
||||
}
|
||||
}
|
||||
).build(spannable)
|
||||
} else {
|
||||
Log.e("[Chat Message Data] Unexpected content with type: ${content.type}/${content.subtype}")
|
||||
}
|
||||
}
|
||||
|
||||
contents.value = list
|
||||
}
|
||||
|
||||
private fun updateEphemeralTimer() {
|
||||
if (chatMessage.isEphemeral) {
|
||||
if (chatMessage.ephemeralExpireTime == 0L) {
|
||||
// This means the message hasn't been read by all participants yet, so the countdown hasn't started
|
||||
// In this case we simply display the configured value for lifetime
|
||||
ephemeralLifetime.value = formatLifetime(chatMessage.ephemeralLifetime)
|
||||
} else {
|
||||
// Countdown has started, display remaining time
|
||||
val remaining = chatMessage.ephemeralExpireTime - (System.currentTimeMillis() / 1000)
|
||||
ephemeralLifetime.value = formatLifetime(remaining)
|
||||
if (countDownTimer == null) {
|
||||
countDownTimer = object : CountDownTimer(remaining * 1000, 1000) {
|
||||
override fun onFinish() {}
|
||||
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
ephemeralLifetime.postValue(formatLifetime(millisUntilFinished / 1000))
|
||||
}
|
||||
}
|
||||
countDownTimer?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLifetime(seconds: Long): String {
|
||||
val days = seconds / 86400
|
||||
return when {
|
||||
days >= 1L -> AppUtils.getStringWithPlural(R.plurals.days, days.toInt())
|
||||
else -> String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, (seconds % 60))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2022 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.ContactDataInterface
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatRoomData(private val chatRoom: ChatRoom) : ContactDataInterface {
|
||||
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
|
||||
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
|
||||
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
|
||||
override val showGroupChatAvatar: Boolean
|
||||
get() = conferenceChatRoom && !oneToOneChatRoom
|
||||
override val coroutineScope: CoroutineScope = coreContext.coroutineScope
|
||||
|
||||
val unreadMessagesCount = MutableLiveData<Int>()
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val securityLevelIcon = MutableLiveData<Int>()
|
||||
|
||||
val securityLevelContentDescription = MutableLiveData<Int>()
|
||||
|
||||
val ephemeralEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val lastUpdate = MutableLiveData<String>()
|
||||
|
||||
val lastMessageText = MutableLiveData<SpannableStringBuilder>()
|
||||
|
||||
val notificationsMuted = MutableLiveData<Boolean>()
|
||||
|
||||
private val basicChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||
}
|
||||
|
||||
val oneToOneChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
|
||||
}
|
||||
|
||||
private val conferenceChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt())
|
||||
}
|
||||
|
||||
val encryptedChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())
|
||||
}
|
||||
|
||||
init {
|
||||
unreadMessagesCount.value = chatRoom.unreadMessagesCount
|
||||
|
||||
subject.value = chatRoom.subject
|
||||
updateSecurityIcon()
|
||||
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||
|
||||
contactLookup()
|
||||
formatLastMessage(chatRoom.lastMessageInHistory)
|
||||
|
||||
notificationsMuted.value = areNotificationsMuted()
|
||||
}
|
||||
|
||||
private fun updateSecurityIcon() {
|
||||
val level = chatRoom.securityLevel
|
||||
securityLevel.value = level
|
||||
|
||||
securityLevelIcon.value = when (level) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
securityLevelContentDescription.value = when (level) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
private fun contactLookup() {
|
||||
displayName.value = when {
|
||||
basicChatRoom -> LinphoneUtils.getDisplayName(
|
||||
chatRoom.peerAddress
|
||||
)
|
||||
oneToOneChatRoom -> LinphoneUtils.getDisplayName(
|
||||
chatRoom.participants.firstOrNull()?.address ?: chatRoom.peerAddress
|
||||
)
|
||||
conferenceChatRoom -> chatRoom.subject.orEmpty()
|
||||
else -> chatRoom.peerAddress.asStringUriOnly()
|
||||
}
|
||||
|
||||
if (oneToOneChatRoom) {
|
||||
searchMatchingContact()
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMatchingContact() {
|
||||
val remoteAddress = if (basicChatRoom) {
|
||||
chatRoom.peerAddress
|
||||
} else {
|
||||
if (chatRoom.participants.isNotEmpty()) {
|
||||
chatRoom.participants[0].address
|
||||
} else {
|
||||
Log.e("[Chat Room] ${chatRoom.peerAddress} doesn't have any participant (state ${chatRoom.state})!")
|
||||
null
|
||||
}
|
||||
}
|
||||
if (remoteAddress != null) {
|
||||
contact.value = coreContext.contactsManager.findContactByAddress(remoteAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLastMessage(msg: ChatMessage?) {
|
||||
val lastUpdateTime = chatRoom.lastUpdateTime
|
||||
lastUpdate.value = "00:00"
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
lastUpdate.postValue(TimestampUtils.toString(lastUpdateTime, true))
|
||||
}
|
||||
}
|
||||
|
||||
val builder = SpannableStringBuilder()
|
||||
if (msg == null) {
|
||||
lastMessageText.value = builder
|
||||
return
|
||||
}
|
||||
|
||||
val sender: String =
|
||||
coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.name
|
||||
?: LinphoneUtils.getDisplayName(msg.fromAddress)
|
||||
builder.append(sender)
|
||||
builder.append(": ")
|
||||
|
||||
for (content in msg.contents) {
|
||||
if (content.isIcalendar) {
|
||||
val body = AppUtils.getString(R.string.conference_invitation)
|
||||
builder.append(body)
|
||||
builder.setSpan(StyleSpan(Typeface.ITALIC), builder.length - body.length, builder.length, 0)
|
||||
} else if (content.isFile || content.isFileTransfer) {
|
||||
builder.append(content.name + " ")
|
||||
} else if (content.isText) {
|
||||
builder.append(content.utf8Text + " ")
|
||||
}
|
||||
}
|
||||
|
||||
builder.trim()
|
||||
lastMessageText.value = builder
|
||||
}
|
||||
|
||||
private fun areNotificationsMuted(): Boolean {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
|
||||
return corePreferences.chatRoomMuted(id)
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.ChatRoomSecurityLevel
|
||||
import org.linphone.core.ParticipantDevice
|
||||
|
||||
class DevicesListChildData(private val device: ParticipantDevice) {
|
||||
val deviceName: String = device.name.orEmpty()
|
||||
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (device.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityContentDescription: Int by lazy {
|
||||
when (device.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
fun onClick() {
|
||||
coreContext.startCall(device.address, forceZRTP = true)
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ChatRoomSecurityLevel
|
||||
import org.linphone.core.Participant
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class DevicesListGroupData(private val participant: Participant) : GenericContactData(participant.address) {
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (participant.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityLevelContentDescription: Int by lazy {
|
||||
when (participant.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(participant.address)
|
||||
|
||||
val isExpanded = MutableLiveData<Boolean>()
|
||||
|
||||
val devices = MutableLiveData<ArrayList<DevicesListChildData>>()
|
||||
|
||||
init {
|
||||
securityLevel.value = participant.securityLevel
|
||||
isExpanded.value = false
|
||||
|
||||
val list = arrayListOf<DevicesListChildData>()
|
||||
for (device in participant.devices) {
|
||||
list.add(DevicesListChildData((device)))
|
||||
}
|
||||
devices.value = list
|
||||
}
|
||||
|
||||
fun toggleExpanded() {
|
||||
isExpanded.value = isExpanded.value != true
|
||||
}
|
||||
|
||||
fun onClick() {
|
||||
val device = if (participant.devices.isEmpty()) null else participant.devices.first()
|
||||
if (device?.address != null) {
|
||||
coreContext.startCall(device.address, forceZRTP = true)
|
||||
} else {
|
||||
coreContext.startCall(participant.address, forceZRTP = true)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
class EphemeralDurationData(
|
||||
val textResource: Int,
|
||||
selectedDuration: Long,
|
||||
private val duration: Long,
|
||||
private val listener: DurationItemClicked
|
||||
) {
|
||||
val selected: Boolean = selectedDuration == duration
|
||||
|
||||
fun setSelected() {
|
||||
listener.onDurationValueChanged(duration)
|
||||
}
|
||||
}
|
||||
|
||||
interface DurationItemClicked {
|
||||
fun onDurationValueChanged(duration: Long)
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.EventLog
|
||||
|
||||
class EventData(private val eventLog: EventLog) : GenericContactData(
|
||||
if (eventLog.type == EventLog.Type.ConferenceSecurityEvent) {
|
||||
eventLog.securityEventFaultyDeviceAddress!!
|
||||
} else {
|
||||
if (eventLog.participantAddress == null) {
|
||||
eventLog.peerAddress!!
|
||||
} else {
|
||||
eventLog.participantAddress!!
|
||||
}
|
||||
}
|
||||
) {
|
||||
val text = MutableLiveData<String>()
|
||||
|
||||
val isSecurity: Boolean by lazy {
|
||||
when (eventLog.type) {
|
||||
EventLog.Type.ConferenceSecurityEvent -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val isGroupLeft: Boolean by lazy {
|
||||
when (eventLog.type) {
|
||||
EventLog.Type.ConferenceTerminated -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
updateEventText()
|
||||
}
|
||||
|
||||
private fun getName(): String {
|
||||
return contact.value?.name ?: displayName.value ?: ""
|
||||
}
|
||||
|
||||
private fun updateEventText() {
|
||||
val context: Context = coreContext.context
|
||||
|
||||
text.value = when (eventLog.type) {
|
||||
EventLog.Type.ConferenceCreated -> context.getString(R.string.chat_event_conference_created)
|
||||
EventLog.Type.ConferenceTerminated -> context.getString(R.string.chat_event_conference_destroyed)
|
||||
EventLog.Type.ConferenceParticipantAdded -> context.getString(R.string.chat_event_participant_added).format(getName())
|
||||
EventLog.Type.ConferenceParticipantRemoved -> context.getString(R.string.chat_event_participant_removed).format(getName())
|
||||
EventLog.Type.ConferenceSubjectChanged -> context.getString(R.string.chat_event_subject_changed).format(eventLog.subject)
|
||||
EventLog.Type.ConferenceParticipantSetAdmin -> context.getString(R.string.chat_event_admin_set).format(getName())
|
||||
EventLog.Type.ConferenceParticipantUnsetAdmin -> context.getString(R.string.chat_event_admin_unset).format(getName())
|
||||
EventLog.Type.ConferenceParticipantDeviceAdded -> context.getString(R.string.chat_event_device_added).format(getName())
|
||||
EventLog.Type.ConferenceParticipantDeviceRemoved -> context.getString(R.string.chat_event_device_removed).format(getName())
|
||||
EventLog.Type.ConferenceSecurityEvent -> {
|
||||
val name = getName()
|
||||
when (eventLog.securityEventType) {
|
||||
EventLog.SecurityEventType.EncryptionIdentityKeyChanged -> context.getString(R.string.chat_security_event_lime_identity_key_changed).format(name)
|
||||
EventLog.SecurityEventType.ManInTheMiddleDetected -> context.getString(R.string.chat_security_event_man_in_the_middle_detected).format(name)
|
||||
EventLog.SecurityEventType.SecurityLevelDowngraded -> context.getString(R.string.chat_security_event_security_level_downgraded).format(name)
|
||||
EventLog.SecurityEventType.ParticipantMaxDeviceCountExceeded -> context.getString(R.string.chat_security_event_participant_max_count_exceeded).format(name)
|
||||
else -> "Unexpected security event for $name: ${eventLog.securityEventType}"
|
||||
}
|
||||
}
|
||||
EventLog.Type.ConferenceEphemeralMessageDisabled -> context.getString(R.string.chat_event_ephemeral_disabled)
|
||||
EventLog.Type.ConferenceEphemeralMessageEnabled -> context.getString(R.string.chat_event_ephemeral_enabled).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime))
|
||||
EventLog.Type.ConferenceEphemeralMessageLifetimeChanged -> context.getString(R.string.chat_event_ephemeral_lifetime_changed).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime))
|
||||
else -> "Unexpected event: ${eventLog.type}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatEphemeralExpiration(context: Context, duration: Long): String {
|
||||
return when (duration) {
|
||||
0L -> context.getString(R.string.chat_room_ephemeral_message_disabled)
|
||||
60L -> context.getString(R.string.chat_room_ephemeral_message_one_minute)
|
||||
3600L -> context.getString(R.string.chat_room_ephemeral_message_one_hour)
|
||||
86400L -> context.getString(R.string.chat_room_ephemeral_message_one_day)
|
||||
259200L -> context.getString(R.string.chat_room_ephemeral_message_three_days)
|
||||
604800L -> context.getString(R.string.chat_room_ephemeral_message_one_week)
|
||||
else -> "Unexpected duration"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.EventLog
|
||||
|
||||
class EventLogData(val eventLog: EventLog) {
|
||||
val type: EventLog.Type = eventLog.type
|
||||
|
||||
val notifyId = eventLog.notifyId
|
||||
|
||||
val data: GenericContactData = if (type == EventLog.Type.ConferenceChatMessage) {
|
||||
ChatMessageData(eventLog.chatMessage!!)
|
||||
} else {
|
||||
EventData(eventLog)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
data.destroy()
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ChatRoomSecurityLevel
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class GroupInfoParticipantData(val participant: GroupChatRoomMember) : GenericContactData(participant.address) {
|
||||
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(participant.address)
|
||||
|
||||
val isAdmin = MutableLiveData<Boolean>()
|
||||
|
||||
val showAdminControls = MutableLiveData<Boolean>()
|
||||
|
||||
// A participant not yet added to a group can't be set admin at the same time it's added
|
||||
val canBeSetAdmin = MutableLiveData<Boolean>()
|
||||
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (participant.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityLevelContentDescription: Int by lazy {
|
||||
when (participant.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
securityLevel.value = participant.securityLevel
|
||||
isAdmin.value = participant.isAdmin
|
||||
showAdminControls.value = false
|
||||
canBeSetAdmin.value = participant.canBeSetAdmin
|
||||
}
|
||||
|
||||
fun setAdmin() {
|
||||
isAdmin.value = true
|
||||
participant.isAdmin = true
|
||||
}
|
||||
|
||||
fun unSetAdmin() {
|
||||
isAdmin.value = false
|
||||
participant.isAdmin = false
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.data
|
||||
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ParticipantImdnState
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ImdnParticipantData(val imdnState: ParticipantImdnState) : GenericContactData(imdnState.participant.address) {
|
||||
val sipUri: String = imdnState.participant.address.asStringUriOnly()
|
||||
|
||||
val time: String = TimestampUtils.toString(imdnState.stateChangeTime)
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationViewModel
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.activities.navigateToChatRoom
|
||||
import org.linphone.activities.navigateToGroupInfo
|
||||
import org.linphone.contact.ContactsSelectionAdapter
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomCreationFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>() {
|
||||
private lateinit var viewModel: ChatRoomCreationViewModel
|
||||
private lateinit var adapter: ContactsSelectionAdapter
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_creation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
|
||||
|
||||
val createGroup = arguments?.getBoolean("createGroup") ?: false
|
||||
|
||||
viewModel = ViewModelProvider(this)[ChatRoomCreationViewModel::class.java]
|
||||
viewModel.createGroupChat.value = createGroup
|
||||
|
||||
viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom
|
||||
|
||||
binding.viewModel = viewModel
|
||||
|
||||
adapter = ContactsSelectionAdapter(viewLifecycleOwner)
|
||||
adapter.setGroupChatCapabilityRequired(viewModel.createGroupChat.value == true)
|
||||
adapter.setLimeCapabilityRequired(viewModel.isEncrypted.value == true)
|
||||
binding.contactsList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.contactsList.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.contactsList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||
|
||||
binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
binding.setAllContactsToggleClickListener {
|
||||
viewModel.sipContactsSelected.value = false
|
||||
}
|
||||
|
||||
binding.setSipContactsToggleClickListener {
|
||||
viewModel.sipContactsSelected.value = true
|
||||
}
|
||||
|
||||
viewModel.contactsList.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
|
||||
viewModel.isEncrypted.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.setLimeCapabilityRequired(it)
|
||||
}
|
||||
|
||||
viewModel.sipContactsSelected.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
viewModel.applyFilter()
|
||||
}
|
||||
|
||||
viewModel.selectedAddresses.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.updateSelectedAddresses(it)
|
||||
}
|
||||
|
||||
viewModel.chatRoomCreatedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.filter.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
viewModel.applyFilter()
|
||||
}
|
||||
|
||||
adapter.selectedContact.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { searchResult ->
|
||||
if (createGroup) {
|
||||
viewModel.toggleSelectionForSearchResult(searchResult)
|
||||
} else {
|
||||
viewModel.createOneToOneChat(searchResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addParticipantsFromSharedViewModel()
|
||||
|
||||
// Next button is only used to go to group chat info fragment
|
||||
binding.setNextClickListener {
|
||||
sharedViewModel.createEncryptedChatRoom = viewModel.isEncrypted.value == true
|
||||
sharedViewModel.chatRoomParticipants.value = viewModel.selectedAddresses.value
|
||||
navigateToGroupInfo()
|
||||
}
|
||||
|
||||
viewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
}
|
||||
|
||||
if (corePreferences.enableNativeAddressBookIntegration) {
|
||||
if (!PermissionHelper.get().hasReadContactsPermission()) {
|
||||
Log.i("[Chat Room Creation] Asking for READ_CONTACTS permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == 0) {
|
||||
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[Chat Room Creation] READ_CONTACTS permission granted")
|
||||
coreContext.fetchContacts()
|
||||
} else {
|
||||
Log.w("[Chat Room Creation] READ_CONTACTS permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
viewModel.secureChatAvailable.value = LinphoneUtils.isEndToEndEncryptedChatAvailable()
|
||||
}
|
||||
|
||||
private fun addParticipantsFromSharedViewModel() {
|
||||
val participants = sharedViewModel.chatRoomParticipants.value
|
||||
if (participants != null && participants.size > 0) {
|
||||
viewModel.selectedAddresses.value = participants
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.viewmodels.DevicesListViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.DevicesListViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomDevicesFragmentBinding
|
||||
|
||||
class DevicesFragment : SecureFragment<ChatRoomDevicesFragmentBinding>() {
|
||||
private lateinit var listViewModel: DevicesListViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_devices_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Devices] Chat room is null, aborting!")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||
|
||||
listViewModel = ViewModelProvider(
|
||||
this,
|
||||
DevicesListViewModelFactory(chatRoom)
|
||||
)[DevicesListViewModel::class.java]
|
||||
binding.viewModel = listViewModel
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
listViewModel.updateParticipants()
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.viewmodels.EphemeralViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.EphemeralViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomEphemeralFragmentBinding
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EphemeralFragment : SecureFragment<ChatRoomEphemeralFragmentBinding>() {
|
||||
private lateinit var viewModel: EphemeralViewModel
|
||||
|
||||
override fun getLayoutId(): Int {
|
||||
return R.layout.chat_room_ephemeral_fragment
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
isSecure = true
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Ephemeral] Chat room is null, aborting!")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
EphemeralViewModelFactory(chatRoom)
|
||||
)[EphemeralViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setValidClickListener {
|
||||
viewModel.updateChatRoomEphemeralDuration()
|
||||
sharedViewModel.refreshChatRoomInListEvent.value = Event(true)
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.adapters.GroupInfoParticipantsAdapter
|
||||
import org.linphone.activities.main.chat.data.GroupInfoParticipantData
|
||||
import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToChatRoom
|
||||
import org.linphone.activities.navigateToChatRoomCreation
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomCapabilities
|
||||
import org.linphone.databinding.ChatRoomGroupInfoFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
|
||||
private lateinit var viewModel: GroupInfoViewModel
|
||||
private lateinit var adapter: GroupInfoParticipantsAdapter
|
||||
private var meAdminStatusChangedDialog: Dialog? = null
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_group_info_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value
|
||||
isSecure = chatRoom?.currentParams?.isEncryptionEnabled ?: false
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
GroupInfoViewModelFactory(chatRoom)
|
||||
)[GroupInfoViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom
|
||||
|
||||
adapter = GroupInfoParticipantsAdapter(
|
||||
viewLifecycleOwner,
|
||||
chatRoom?.hasCapability(ChatRoomCapabilities.Encrypted.toInt()) ?: (viewModel.isEncrypted.value == true)
|
||||
)
|
||||
binding.participants.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.participants.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.participants.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||
|
||||
viewModel.participants.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
|
||||
viewModel.isMeAdmin.observe(
|
||||
viewLifecycleOwner
|
||||
) { isMeAdmin ->
|
||||
adapter.showAdminControls(isMeAdmin && chatRoom != null)
|
||||
}
|
||||
|
||||
viewModel.meAdminChangedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { isMeAdmin ->
|
||||
showMeAdminStateChanged(isMeAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.participantRemovedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { participant ->
|
||||
viewModel.removeParticipant(participant)
|
||||
}
|
||||
}
|
||||
|
||||
addParticipantsFromSharedViewModel()
|
||||
|
||||
viewModel.createdChatRoomEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
goToChatRoom(chatRoom, true)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.updatedChatRoomEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
goToChatRoom(chatRoom, false)
|
||||
}
|
||||
}
|
||||
|
||||
binding.setNextClickListener {
|
||||
if (viewModel.chatRoom != null) {
|
||||
viewModel.updateRoom()
|
||||
} else {
|
||||
viewModel.createChatRoom()
|
||||
}
|
||||
}
|
||||
|
||||
binding.setParticipantsClickListener {
|
||||
sharedViewModel.createEncryptedChatRoom = corePreferences.forceEndToEndEncryptedChat || viewModel.isEncrypted.value == true
|
||||
|
||||
val list = arrayListOf<Address>()
|
||||
for (participant in viewModel.participants.value.orEmpty()) {
|
||||
list.add(participant.participant.address)
|
||||
}
|
||||
sharedViewModel.chatRoomParticipants.value = list
|
||||
sharedViewModel.chatRoomSubject = viewModel.subject.value.orEmpty()
|
||||
|
||||
val args = Bundle()
|
||||
args.putBoolean("createGroup", true)
|
||||
navigateToChatRoomCreation(args)
|
||||
}
|
||||
|
||||
binding.setLeaveClickListener {
|
||||
val dialogViewModel = DialogViewModel(getString(R.string.chat_room_group_info_leave_dialog_message))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showDeleteButton(
|
||||
{
|
||||
viewModel.leaveGroup()
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.chat_room_group_info_leave_dialog_button)
|
||||
)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
viewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addParticipantsFromSharedViewModel() {
|
||||
val participants = sharedViewModel.chatRoomParticipants.value
|
||||
if (participants != null && participants.size > 0) {
|
||||
val list = arrayListOf<GroupInfoParticipantData>()
|
||||
|
||||
for (address in participants) {
|
||||
val exists = viewModel.participants.value?.find {
|
||||
it.participant.address.weakEqual(address)
|
||||
}
|
||||
|
||||
if (exists != null) {
|
||||
list.add(exists)
|
||||
} else {
|
||||
list.add(
|
||||
GroupInfoParticipantData(
|
||||
GroupChatRoomMember(address, false, hasLimeX3DHCapability = viewModel.isEncrypted.value == true)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.participants.value = list
|
||||
}
|
||||
|
||||
if (sharedViewModel.chatRoomSubject.isNotEmpty()) {
|
||||
viewModel.subject.value = sharedViewModel.chatRoomSubject
|
||||
sharedViewModel.chatRoomSubject = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMeAdminStateChanged(isMeAdmin: Boolean) {
|
||||
meAdminStatusChangedDialog?.dismiss()
|
||||
|
||||
val message = if (isMeAdmin) {
|
||||
getString(R.string.chat_room_group_info_you_are_now_admin)
|
||||
} else {
|
||||
getString(R.string.chat_room_group_info_you_are_no_longer_admin)
|
||||
}
|
||||
val dialogViewModel = DialogViewModel(message)
|
||||
val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showOkButton({
|
||||
dialog.dismiss()
|
||||
})
|
||||
|
||||
dialog.show()
|
||||
meAdminStatusChangedDialog = dialog
|
||||
}
|
||||
|
||||
private fun goToChatRoom(chatRoom: ChatRoom, created: Boolean) {
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel), created)
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.adapters.ImdnAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.ImdnViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.ImdnViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomImdnFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.RecyclerViewHeaderDecoration
|
||||
|
||||
class ImdnFragment : SecureFragment<ChatRoomImdnFragmentBinding>() {
|
||||
private lateinit var viewModel: ImdnViewModel
|
||||
private lateinit var adapter: ImdnAdapter
|
||||
|
||||
override fun getLayoutId(): Int {
|
||||
return R.layout.chat_room_imdn_fragment
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[IMDN] Chat room is null, aborting!")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||
|
||||
if (arguments != null) {
|
||||
val messageId = arguments?.getString("MessageId")
|
||||
val message = if (messageId != null) chatRoom.findMessage(messageId) else null
|
||||
if (message != null) {
|
||||
Log.i("[IMDN] Found message $message with id $messageId")
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
ImdnViewModelFactory(message)
|
||||
)[ImdnViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
} else {
|
||||
Log.e("[IMDN] Couldn't find message with id $messageId in chat room $chatRoom")
|
||||
findNavController().popBackStack()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Log.e("[IMDN] Couldn't find message id in intent arguments")
|
||||
findNavController().popBackStack()
|
||||
return
|
||||
}
|
||||
|
||||
adapter = ImdnAdapter(viewLifecycleOwner)
|
||||
binding.participantsList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.participantsList.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.participantsList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||
|
||||
// Displays state header
|
||||
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
|
||||
binding.participantsList.addItemDecoration(headerItemDecoration)
|
||||
|
||||
viewModel.participants.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,399 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.clearDisplayedChatRoom
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.adapters.ChatRoomsListAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatRoomsListViewModel
|
||||
import org.linphone.activities.main.fragments.MasterFragment
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToChatRoom
|
||||
import org.linphone.activities.navigateToChatRoomCreation
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomMasterFragmentBinding
|
||||
import org.linphone.utils.*
|
||||
|
||||
class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, ChatRoomsListAdapter>() {
|
||||
override val dialogConfirmationMessageBeforeRemoval = R.plurals.chat_room_delete_dialog
|
||||
private lateinit var listViewModel: ChatRoomsListViewModel
|
||||
|
||||
private val observer = object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onChanged() {
|
||||
scrollToTop()
|
||||
}
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (positionStart == 0 && itemCount == 1) {
|
||||
scrollToTop()
|
||||
}
|
||||
}
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_master_fragment
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding.chatList.adapter = null
|
||||
adapter.unregisterAdapterDataObserver(observer)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
useMaterialSharedAxisXForwardAnimation = false
|
||||
if (corePreferences.enableAnimations) {
|
||||
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
||||
val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
|
||||
enterTransition = MaterialSharedAxis(axis, true)
|
||||
reenterTransition = MaterialSharedAxis(axis, true)
|
||||
returnTransition = MaterialSharedAxis(axis, false)
|
||||
exitTransition = MaterialSharedAxis(axis, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
isSecure = true
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
listViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[ChatRoomsListViewModel::class.java]
|
||||
}
|
||||
binding.viewModel = listViewModel
|
||||
|
||||
/* Shared view model & sliding pane related */
|
||||
|
||||
setUpSlidingPane(binding.slidingPane)
|
||||
|
||||
binding.slidingPane.addPanelSlideListener(object : SlidingPaneLayout.PanelSlideListener {
|
||||
override fun onPanelSlide(panel: View, slideOffset: Float) { }
|
||||
|
||||
override fun onPanelOpened(panel: View) { }
|
||||
|
||||
override fun onPanelClosed(panel: View) {
|
||||
// Conversation isn't visible anymore, any new message received in it will trigger a notification
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||
}
|
||||
})
|
||||
|
||||
// Chat room loading can take some time, so wait until it is ready before opening the pane
|
||||
sharedViewModel.chatRoomFragmentOpenedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
binding.slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.layoutChangedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
||||
if (binding.slidingPane.isSlideable) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
||||
if (navHostFragment.navController.currentDestination?.id == R.id.emptyChatFragment) {
|
||||
Log.i("[Chat] Foldable device has been folded, closing side pane with empty fragment")
|
||||
binding.slidingPane.closePane()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.refreshChatRoomInListEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom != null) {
|
||||
listViewModel.notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* End of shared view model & sliding pane related */
|
||||
|
||||
_adapter = ChatRoomsListAdapter(listSelectionViewModel, viewLifecycleOwner)
|
||||
// SubmitList is done on a background thread
|
||||
// We need this adapter data observer to know when to scroll
|
||||
adapter.registerAdapterDataObserver(observer)
|
||||
binding.chatList.setHasFixedSize(true)
|
||||
binding.chatList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.chatList.layoutManager = layoutManager
|
||||
|
||||
// Swipe action
|
||||
val swipeConfiguration = RecyclerViewSwipeConfiguration()
|
||||
val white = ContextCompat.getColor(requireContext(), R.color.white_color)
|
||||
|
||||
swipeConfiguration.rightToLeftAction = RecyclerViewSwipeConfiguration.Action(
|
||||
requireContext().getString(R.string.dialog_delete),
|
||||
white,
|
||||
ContextCompat.getColor(requireContext(), R.color.red_color)
|
||||
)
|
||||
swipeConfiguration.leftToRightAction = RecyclerViewSwipeConfiguration.Action(
|
||||
requireContext().getString(R.string.received_chat_notification_mark_as_read_label),
|
||||
white,
|
||||
ContextCompat.getColor(requireContext(), R.color.imdn_read_color)
|
||||
)
|
||||
val swipeListener = object : RecyclerViewSwipeListener {
|
||||
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||
val index = viewHolder.bindingAdapterPosition
|
||||
if (index < 0 || index >= adapter.currentList.size) {
|
||||
Log.e("[Chat] Index is out of bound, can't mark chat room as read")
|
||||
} else {
|
||||
val chatRoom = adapter.currentList[viewHolder.bindingAdapterPosition]
|
||||
chatRoom.markAsRead()
|
||||
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||
val viewModel = DialogViewModel(getString(R.string.chat_room_delete_one_dialog))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||
|
||||
val index = viewHolder.bindingAdapterPosition
|
||||
if (index < 0 || index >= adapter.currentList.size) {
|
||||
Log.e("[Chat] Index is out of bound, can't delete chat room")
|
||||
} else {
|
||||
viewModel.showCancelButton {
|
||||
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
viewModel.showDeleteButton(
|
||||
{
|
||||
val deletedChatRoom =
|
||||
adapter.currentList[index]
|
||||
listViewModel.deleteChatRoom(deletedChatRoom)
|
||||
if (!binding.slidingPane.isSlideable &&
|
||||
deletedChatRoom == sharedViewModel.selectedChatRoom.value
|
||||
) {
|
||||
Log.i("[Chat] Currently displayed chat room has been deleted, removing detail fragment")
|
||||
clearDisplayedChatRoom()
|
||||
}
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.dialog_delete)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
RecyclerViewSwipeUtils(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, swipeConfiguration, swipeListener)
|
||||
.attachToRecyclerView(binding.chatList)
|
||||
|
||||
// Divider between items
|
||||
binding.chatList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||
|
||||
listViewModel.chatRooms.observe(
|
||||
viewLifecycleOwner
|
||||
) { chatRooms ->
|
||||
adapter.submitList(chatRooms)
|
||||
}
|
||||
|
||||
listViewModel.chatRoomIndexUpdatedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { index ->
|
||||
adapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.selectedChatRoomEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
if ((requireActivity() as GenericActivity).isDestructionPending) {
|
||||
Log.w("[Chat] Activity is pending destruction, don't start navigating now!")
|
||||
sharedViewModel.destructionPendingChatRoom = chatRoom
|
||||
} else {
|
||||
if (chatRoom.peerAddress.asStringUriOnly() == coreContext.notificationsManager.currentlyDisplayedChatRoomAddress) {
|
||||
if (!binding.slidingPane.isOpen) {
|
||||
Log.w("[Chat] Chat room is displayed but sliding pane is closed...")
|
||||
if (!binding.slidingPane.openPane()) {
|
||||
Log.e("[Chat] Tried to open pane to workaround already displayed chat room issue, failed!")
|
||||
}
|
||||
} else {
|
||||
Log.w("[Chat] This chat room is already displayed!")
|
||||
}
|
||||
} else {
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
navigateToChatRoom(
|
||||
AppUtils.createBundleWithSharedTextAndFiles(
|
||||
sharedViewModel
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.setEditClickListener {
|
||||
listSelectionViewModel.isEditionEnabled.value = true
|
||||
}
|
||||
|
||||
binding.setCancelForwardClickListener {
|
||||
sharedViewModel.messageToForwardEvent.value?.consume {
|
||||
Log.i("[Chat] Cancelling message forward")
|
||||
}
|
||||
sharedViewModel.isPendingMessageForward.value = false
|
||||
}
|
||||
|
||||
binding.setCancelSharingClickListener {
|
||||
Log.i("[Chat] Cancelling text/files sharing")
|
||||
sharedViewModel.textToShare.value = ""
|
||||
sharedViewModel.filesToShare.value = arrayListOf()
|
||||
listViewModel.fileSharingPending.value = false
|
||||
listViewModel.textSharingPending.value = false
|
||||
}
|
||||
|
||||
binding.setNewOneToOneChatRoomClickListener {
|
||||
sharedViewModel.chatRoomParticipants.value = arrayListOf()
|
||||
navigateToChatRoomCreation(false, binding.slidingPane)
|
||||
}
|
||||
|
||||
binding.setNewGroupChatRoomClickListener {
|
||||
sharedViewModel.selectedGroupChatRoom.value = null
|
||||
sharedViewModel.chatRoomParticipants.value = arrayListOf()
|
||||
navigateToChatRoomCreation(true, binding.slidingPane)
|
||||
}
|
||||
|
||||
val pendingDestructionChatRoom = sharedViewModel.destructionPendingChatRoom
|
||||
if (pendingDestructionChatRoom != null) {
|
||||
Log.w("[Chat] Found pending chat room from before activity was recreated")
|
||||
sharedViewModel.destructionPendingChatRoom = null
|
||||
sharedViewModel.selectedChatRoom.value = pendingDestructionChatRoom
|
||||
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
|
||||
}
|
||||
|
||||
val localSipUri = arguments?.getString("LocalSipUri")
|
||||
val remoteSipUri = arguments?.getString("RemoteSipUri")
|
||||
if (localSipUri != null && remoteSipUri != null) {
|
||||
Log.i("[Chat] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments")
|
||||
arguments?.clear()
|
||||
val localAddress = Factory.instance().createAddress(localSipUri)
|
||||
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
|
||||
val chatRoom = coreContext.core.searchChatRoom(null, localAddress, remoteSipAddress, arrayOfNulls(0))
|
||||
if (chatRoom != null) {
|
||||
Log.i("[Chat] Found matching chat room $chatRoom")
|
||||
adapter.selectedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
} else {
|
||||
sharedViewModel.textToShare.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it.isNotEmpty()) {
|
||||
Log.i("[Chat] Found text to share")
|
||||
listViewModel.textSharingPending.value = true
|
||||
clearDisplayedChatRoom()
|
||||
} else {
|
||||
if (sharedViewModel.filesToShare.value.isNullOrEmpty()) {
|
||||
listViewModel.textSharingPending.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedViewModel.filesToShare.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it.isNotEmpty()) {
|
||||
Log.i("[Chat] Found ${it.size} files to share")
|
||||
listViewModel.fileSharingPending.value = true
|
||||
clearDisplayedChatRoom()
|
||||
} else {
|
||||
if (sharedViewModel.textToShare.value.isNullOrEmpty()) {
|
||||
listViewModel.fileSharingPending.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedViewModel.isPendingMessageForward.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
listViewModel.forwardPending.value = it
|
||||
adapter.forwardPending(it)
|
||||
if (it) {
|
||||
Log.i("[Chat] Found chat message to transfer")
|
||||
}
|
||||
}
|
||||
|
||||
listViewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
listViewModel.groupChatAvailable.value = LinphoneUtils.isGroupChatAvailable()
|
||||
}
|
||||
|
||||
override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) {
|
||||
val list = ArrayList<ChatRoom>()
|
||||
var closeSlidingPane = false
|
||||
for (index in indexesOfItemToDelete) {
|
||||
val chatRoom = adapter.currentList[index]
|
||||
list.add(chatRoom)
|
||||
|
||||
if (chatRoom == sharedViewModel.selectedChatRoom.value) {
|
||||
closeSlidingPane = true
|
||||
}
|
||||
}
|
||||
listViewModel.deleteChatRooms(list)
|
||||
|
||||
if (!binding.slidingPane.isSlideable && closeSlidingPane) {
|
||||
Log.i("[Chat] Currently displayed chat room has been deleted, removing detail fragment")
|
||||
clearDisplayedChatRoom()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
binding.chatList.scrollToPosition(0)
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.receivers
|
||||
|
||||
import android.content.ClipData
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.core.util.component1
|
||||
import androidx.core.util.component2
|
||||
import androidx.core.view.ContentInfoCompat
|
||||
import androidx.core.view.OnReceiveContentListener
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class RichContentReceiver(private val contentReceived: (uri: Uri) -> Unit) : OnReceiveContentListener {
|
||||
companion object {
|
||||
val MIME_TYPES = arrayOf("image/png", "image/gif", "image/jpeg")
|
||||
}
|
||||
|
||||
override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? {
|
||||
val (uriContent, remaining) = payload.partition { item -> item.uri != null }
|
||||
if (uriContent != null) {
|
||||
val clip: ClipData = uriContent.clip
|
||||
for (i in 0 until clip.itemCount) {
|
||||
val uri: Uri = clip.getItemAt(i).uri
|
||||
Log.i("[Content Receiver] Found URI: $uri")
|
||||
contentReceived(uri)
|
||||
}
|
||||
}
|
||||
// Return anything that your app didn't handle. This preserves the default platform
|
||||
// behavior for text and anything else that you aren't implementing custom handling for.
|
||||
return remaining
|
||||
}
|
||||
}
|
@ -0,0 +1,524 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlinx.coroutines.*
|
||||
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.activities.main.chat.data.ChatMessageAttachmentData
|
||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatMessageSendingViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ChatMessageSendingViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
var temporaryFileUploadPath: File? = null
|
||||
|
||||
val attachments = MutableLiveData<ArrayList<ChatMessageAttachmentData>>()
|
||||
|
||||
val attachFileEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val sendMessageEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isReadOnly = MutableLiveData<Boolean>()
|
||||
|
||||
var textToSend = MutableLiveData<String>()
|
||||
|
||||
val isPendingAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
var pendingChatMessageToReplyTo = MutableLiveData<ChatMessageData>()
|
||||
|
||||
val requestRecordAudioPermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val messageSentEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val voiceRecordingProgressBarMax = 10000
|
||||
|
||||
val isPendingVoiceRecord = MutableLiveData<Boolean>()
|
||||
|
||||
val isVoiceRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val voiceRecordingDuration = MutableLiveData<Int>()
|
||||
|
||||
val formattedDuration = MutableLiveData<String>()
|
||||
|
||||
val isPlayingVoiceRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
||||
|
||||
val imeFlags: Int = if (chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) {
|
||||
// IME_FLAG_NO_PERSONALIZED_LEARNING is only available on Android 8 and newer
|
||||
Compatibility.getImeFlagsForSecureChatRoom()
|
||||
} else {
|
||||
EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
||||
}
|
||||
|
||||
private val recorder: Recorder
|
||||
|
||||
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
||||
|
||||
private lateinit var voiceRecordingPlayer: Player
|
||||
private val playerListener = PlayerListener {
|
||||
Log.i("[Chat Message Sending] End of file reached")
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
updateChatRoomReadOnlyState()
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateChatRoomReadOnlyState()
|
||||
}
|
||||
|
||||
override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateChatRoomReadOnlyState()
|
||||
}
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
|
||||
attachments.value = arrayListOf()
|
||||
|
||||
attachFileEnabled.value = true
|
||||
sendMessageEnabled.value = false
|
||||
updateChatRoomReadOnlyState()
|
||||
|
||||
val recorderParams = coreContext.core.createRecorderParams()
|
||||
if (corePreferences.voiceMessagesFormatMkv) {
|
||||
recorderParams.fileFormat = RecorderFileFormat.Mkv
|
||||
} else {
|
||||
recorderParams.fileFormat = RecorderFileFormat.Wav
|
||||
}
|
||||
recorder = coreContext.core.createRecorder(recorderParams)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
pendingChatMessageToReplyTo.value?.destroy()
|
||||
|
||||
if (recorder.state != RecorderState.Closed) {
|
||||
recorder.close()
|
||||
}
|
||||
|
||||
if (this::voiceRecordingPlayer.isInitialized) {
|
||||
stopVoiceRecordPlayer()
|
||||
voiceRecordingPlayer.removeListener(playerListener)
|
||||
}
|
||||
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
scope.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun onTextToSendChanged(value: String) {
|
||||
sendMessageEnabled.value = value.trim().isNotEmpty() || attachments.value?.isNotEmpty() == true || isPendingVoiceRecord.value == true
|
||||
if (value.isNotEmpty()) {
|
||||
if (attachFileEnabled.value == true && !corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = false
|
||||
}
|
||||
chatRoom.compose()
|
||||
} else {
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = attachments.value?.isEmpty() ?: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAttachment(path: String) {
|
||||
val list = arrayListOf<ChatMessageAttachmentData>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
list.add(
|
||||
ChatMessageAttachmentData(path) {
|
||||
removeAttachment(it)
|
||||
}
|
||||
)
|
||||
attachments.value = list
|
||||
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().trim().isNotEmpty() || list.isNotEmpty() || isPendingVoiceRecord.value == true
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAttachment(attachment: ChatMessageAttachmentData) {
|
||||
val list = arrayListOf<ChatMessageAttachmentData>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
list.remove(attachment)
|
||||
attachments.value = list
|
||||
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().trim().isNotEmpty() || list.isNotEmpty() || isPendingVoiceRecord.value == true
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = list.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage() {
|
||||
if (!isPlayerClosed()) {
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
val pendingMessageToReplyTo = pendingChatMessageToReplyTo.value
|
||||
val message: ChatMessage = if (isPendingAnswer.value == true && pendingMessageToReplyTo != null)
|
||||
chatRoom.createReplyMessage(pendingMessageToReplyTo.chatMessage)
|
||||
else
|
||||
chatRoom.createEmptyMessage()
|
||||
val isBasicChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||
|
||||
var voiceRecord = false
|
||||
if (isPendingVoiceRecord.value == true && recorder.file != null) {
|
||||
val content = recorder.createContent()
|
||||
if (content != null) {
|
||||
Log.i("[Chat Message Sending] Voice recording content created, file name is ${content.name} and duration is ${content.fileDuration}")
|
||||
message.addContent(content)
|
||||
voiceRecord = true
|
||||
} else {
|
||||
Log.e("[Chat Message Sending] Voice recording content couldn't be created!")
|
||||
}
|
||||
|
||||
isPendingVoiceRecord.value = false
|
||||
isVoiceRecording.value = false
|
||||
}
|
||||
|
||||
val toSend = textToSend.value.orEmpty().trim()
|
||||
if (toSend.isNotEmpty()) {
|
||||
if (voiceRecord && isBasicChatRoom) {
|
||||
val textMessage: ChatMessage = chatRoom.createMessageFromUtf8(toSend)
|
||||
textMessage.send()
|
||||
} else {
|
||||
message.addUtf8TextContent(toSend)
|
||||
}
|
||||
}
|
||||
|
||||
var fileContent = false
|
||||
for (attachment in attachments.value.orEmpty()) {
|
||||
val content = Factory.instance().createContent()
|
||||
|
||||
if (attachment.isImage) {
|
||||
content.type = "image"
|
||||
} else {
|
||||
content.type = "file"
|
||||
}
|
||||
content.subtype = FileUtils.getExtensionFromFileName(attachment.fileName)
|
||||
content.name = attachment.fileName
|
||||
content.filePath = attachment.path // Let the file body handler take care of the upload
|
||||
|
||||
// Do not send file in the same message as the text in a BasicChatRoom
|
||||
// and don't send multiple files in the same message if setting says so
|
||||
if (isBasicChatRoom or (corePreferences.preventMoreThanOneFilePerMessage and (fileContent or voiceRecord))) {
|
||||
val fileMessage: ChatMessage = chatRoom.createFileTransferMessage(content)
|
||||
fileMessage.send()
|
||||
} else {
|
||||
message.addFileContent(content)
|
||||
fileContent = true
|
||||
}
|
||||
}
|
||||
|
||||
if (message.contents.isNotEmpty()) {
|
||||
message.send()
|
||||
}
|
||||
|
||||
cancelReply()
|
||||
attachments.value = arrayListOf()
|
||||
textToSend.value = ""
|
||||
|
||||
messageSentEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun transferMessage(chatMessage: ChatMessage) {
|
||||
val message = chatRoom.createForwardMessage(chatMessage)
|
||||
message.send()
|
||||
}
|
||||
|
||||
fun cancelReply() {
|
||||
pendingChatMessageToReplyTo.value?.destroy()
|
||||
isPendingAnswer.value = false
|
||||
}
|
||||
|
||||
private fun tickerFlowRecording() = flow {
|
||||
while (recorder.state == RecorderState.Running) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tickerFlowPlaying() = flow {
|
||||
while (voiceRecordingPlayer.state == Player.State.Playing) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleVoiceRecording() {
|
||||
if (corePreferences.holdToRecordVoiceMessage) {
|
||||
// Disables click listener just in case, touch listener will be used instead
|
||||
return
|
||||
}
|
||||
|
||||
if (isVoiceRecording.value == true) {
|
||||
stopVoiceRecording()
|
||||
} else {
|
||||
startVoiceRecording()
|
||||
}
|
||||
}
|
||||
|
||||
fun startVoiceRecording() {
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
requestRecordAudioPermissionEvent.value = Event(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AppUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
|
||||
when (recorder.state) {
|
||||
RecorderState.Running -> Log.w("[Chat Message Sending] Recorder is already recording")
|
||||
RecorderState.Paused -> {
|
||||
Log.w("[Chat Message Sending] Recorder isn't closed, resuming recording")
|
||||
recorder.start()
|
||||
}
|
||||
RecorderState.Closed -> {
|
||||
val extension = when (recorder.params.fileFormat) {
|
||||
RecorderFileFormat.Mkv -> "mkv"
|
||||
else -> "wav"
|
||||
}
|
||||
val tempFileName = "voice-recording-${System.currentTimeMillis()}.$extension"
|
||||
val file = FileUtils.getFileStoragePath(tempFileName)
|
||||
Log.w("[Chat Message Sending] Recorder is closed, starting recording in ${file.absoluteFile}")
|
||||
recorder.open(file.absolutePath)
|
||||
recorder.start()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
val duration = recorder.duration
|
||||
voiceRecordingDuration.value = duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration) // duration is in ms
|
||||
|
||||
isPendingVoiceRecord.value = true
|
||||
isVoiceRecording.value = true
|
||||
sendMessageEnabled.value = true
|
||||
|
||||
tickerFlowRecording().onEach {
|
||||
val duration = recorder.duration
|
||||
voiceRecordingDuration.postValue(recorder.duration % voiceRecordingProgressBarMax)
|
||||
formattedDuration.postValue(SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)) // duration is in ms
|
||||
|
||||
if (duration >= corePreferences.voiceRecordingMaxDuration) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Log.w("[Chat Message Sending] Max duration for voice recording exceeded (${corePreferences.voiceRecordingMaxDuration}ms), stopping.")
|
||||
stopVoiceRecording()
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun cancelVoiceRecording() {
|
||||
if (recorder.state != RecorderState.Closed) {
|
||||
Log.i("[Chat Message Sending] Closing voice recorder")
|
||||
recorder.close()
|
||||
|
||||
val path = recorder.file
|
||||
if (path != null) {
|
||||
Log.i("[Chat Message Sending] Deleting voice recording file: $path")
|
||||
FileUtils.deleteFile(path)
|
||||
}
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPendingVoiceRecord.value = false
|
||||
isVoiceRecording.value = false
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().trim().isNotEmpty() == true || attachments.value?.isNotEmpty() == true
|
||||
|
||||
if (!isPlayerClosed()) {
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
fun stopVoiceRecording() {
|
||||
if (recorder.state == RecorderState.Running) {
|
||||
Log.i("[Chat Message Sending] Pausing / closing voice recorder")
|
||||
recorder.pause()
|
||||
recorder.close()
|
||||
voiceRecordingDuration.value = recorder.duration
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isVoiceRecording.value = false
|
||||
if (corePreferences.sendVoiceRecordingRightAway) {
|
||||
Log.i("[Chat Message Sending] Sending voice recording right away")
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
fun playRecordedMessage() {
|
||||
if (isPlayerClosed()) {
|
||||
Log.w("[Chat Message Sending] Player closed, let's open it first")
|
||||
initVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
if (AppUtils.isMediaVolumeLow(coreContext.context)) {
|
||||
Toast.makeText(coreContext.context, R.string.chat_message_voice_recording_playback_low_volume, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AppUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
|
||||
voiceRecordingPlayer.start()
|
||||
isPlayingVoiceRecording.value = true
|
||||
|
||||
tickerFlowPlaying().onEach {
|
||||
voiceRecordPlayingPosition.postValue(voiceRecordingPlayer.currentPosition)
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun pauseRecordedMessage() {
|
||||
Log.i("[Chat Message Sending] Pausing voice record")
|
||||
if (!isPlayerClosed()) {
|
||||
voiceRecordingPlayer.pause()
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPlayingVoiceRecording.value = false
|
||||
}
|
||||
|
||||
private fun initVoiceRecordPlayer() {
|
||||
Log.i("[Chat Message Sending] Creating player for voice record")
|
||||
// In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
|
||||
// If none are available, default one will be used
|
||||
var headphonesCard: String? = null
|
||||
var speakerCard: String? = null
|
||||
var earpieceCard: String? = null
|
||||
for (device in coreContext.core.audioDevices) {
|
||||
if (device.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
|
||||
when (device.type) {
|
||||
AudioDevice.Type.Speaker -> {
|
||||
speakerCard = device.id
|
||||
}
|
||||
AudioDevice.Type.Earpiece -> {
|
||||
earpieceCard = device.id
|
||||
}
|
||||
AudioDevice.Type.Headphones, AudioDevice.Type.Headset -> {
|
||||
headphonesCard = device.id
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i("[Chat Message Sending] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
|
||||
|
||||
val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
|
||||
if (localPlayer != null) {
|
||||
voiceRecordingPlayer = localPlayer
|
||||
} else {
|
||||
Log.e("[Chat Message Sending] Couldn't create local player!")
|
||||
return
|
||||
}
|
||||
voiceRecordingPlayer.addListener(playerListener)
|
||||
|
||||
val path = recorder.file
|
||||
if (path != null) {
|
||||
voiceRecordingPlayer.open(path)
|
||||
// Update recording duration using player value to ensure proper progress bar animation
|
||||
voiceRecordingDuration.value = voiceRecordingPlayer.duration
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVoiceRecordPlayer() {
|
||||
if (!isPlayerClosed()) {
|
||||
Log.i("[Chat Message Sending] Stopping voice record")
|
||||
voiceRecordingPlayer.pause()
|
||||
voiceRecordingPlayer.seek(0)
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
voiceRecordingPlayer.close()
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPlayingVoiceRecording.value = false
|
||||
}
|
||||
|
||||
private fun isPlayerClosed(): Boolean {
|
||||
return !this::voiceRecordingPlayer.isInitialized || voiceRecordingPlayer.state == Player.State.Closed
|
||||
}
|
||||
|
||||
private fun updateChatRoomReadOnlyState() {
|
||||
isReadOnly.value = chatRoom.isReadOnly || (chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt()) && chatRoom.participants.isEmpty())
|
||||
}
|
||||
}
|
@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import org.linphone.activities.main.chat.data.EventLogData
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatMessagesListViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ChatMessagesListViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
companion object {
|
||||
private const val MESSAGES_PER_PAGE = 20
|
||||
}
|
||||
|
||||
val events = MutableLiveData<ArrayList<EventLogData>>()
|
||||
|
||||
val messageUpdatedEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
val requestWriteExternalStoragePermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array<out EventLog>) {
|
||||
for (eventLog in eventLogs) {
|
||||
addChatMessageEventLog(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChatMessageSending(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
val position = events.value.orEmpty().size
|
||||
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
chatMessage ?: return
|
||||
chatMessage.userData = position
|
||||
}
|
||||
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
if (!chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
if (!chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
Log.i("[Chat Messages] An ephemeral chat message has expired, removing it from event list")
|
||||
deleteEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun resendMessage(chatMessage: ChatMessage) {
|
||||
val position: Int = chatMessage.userData as Int
|
||||
chatMessage.send()
|
||||
messageUpdatedEvent.value = Event(position)
|
||||
}
|
||||
|
||||
fun deleteMessage(chatMessage: ChatMessage) {
|
||||
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
||||
chatRoom.deleteMessage(chatMessage)
|
||||
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
fun deleteEventLogs(listToDelete: ArrayList<EventLogData>) {
|
||||
for (eventLog in listToDelete) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog.eventLog)
|
||||
eventLog.eventLog.deleteFromDatabase()
|
||||
}
|
||||
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
fun loadMoreData(totalItemsCount: Int) {
|
||||
Log.i("[Chat Messages] Load more data, current total is $totalItemsCount")
|
||||
val maxSize: Int = chatRoom.historyEventsSize
|
||||
|
||||
if (totalItemsCount < maxSize) {
|
||||
var upperBound: Int = totalItemsCount + MESSAGES_PER_PAGE
|
||||
if (upperBound > maxSize) {
|
||||
upperBound = maxSize
|
||||
}
|
||||
|
||||
val history: Array<EventLog> = chatRoom.getHistoryRangeEvents(totalItemsCount, upperBound)
|
||||
val list = arrayListOf<EventLogData>()
|
||||
for (eventLog in history) {
|
||||
list.add(EventLogData(eventLog))
|
||||
}
|
||||
list.addAll(events.value.orEmpty())
|
||||
events.value = list
|
||||
}
|
||||
}
|
||||
|
||||
private fun addEvent(eventLog: EventLog) {
|
||||
val list = arrayListOf<EventLogData>()
|
||||
list.addAll(events.value.orEmpty())
|
||||
val found = list.find { data -> data.eventLog == eventLog }
|
||||
if (found == null) {
|
||||
list.add(EventLogData(eventLog))
|
||||
}
|
||||
events.value = list
|
||||
}
|
||||
|
||||
private fun getEvents(): ArrayList<EventLogData> {
|
||||
val list = arrayListOf<EventLogData>()
|
||||
val unreadCount = chatRoom.unreadMessagesCount
|
||||
var loadCount = max(MESSAGES_PER_PAGE, unreadCount)
|
||||
Log.i("[Chat Messages] $unreadCount unread messages in this chat room, loading $loadCount from history")
|
||||
|
||||
val history = chatRoom.getHistoryEvents(loadCount)
|
||||
var messageCount = 0
|
||||
for (eventLog in history) {
|
||||
list.add(EventLogData(eventLog))
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
messageCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Load enough events to have at least all unread messages
|
||||
while (unreadCount > 0 && messageCount < unreadCount) {
|
||||
Log.w("[Chat Messages] There is only $messageCount messages in the last $loadCount events, loading $MESSAGES_PER_PAGE more")
|
||||
val moreHistory = chatRoom.getHistoryRangeEvents(loadCount, loadCount + MESSAGES_PER_PAGE)
|
||||
loadCount += MESSAGES_PER_PAGE
|
||||
for (eventLog in moreHistory) {
|
||||
list.add(EventLogData(eventLog))
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
messageCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
private fun deleteEvent(eventLog: EventLog) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
if (chatMessage != null) {
|
||||
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
||||
chatRoom.deleteMessage(chatMessage)
|
||||
}
|
||||
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
private fun addChatMessageEventLog(eventLog: EventLog) {
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
chatMessage ?: return
|
||||
chatMessage.userData = events.value.orEmpty().size
|
||||
|
||||
val existingEvent = events.value.orEmpty().find { data ->
|
||||
data.eventLog.type == EventLog.Type.ConferenceChatMessage && data.eventLog.chatMessage?.messageId == chatMessage.messageId
|
||||
}
|
||||
if (existingEvent != null) {
|
||||
Log.w("[Chat Messages] Found already present chat message, don't add it it's probably the result of an auto download or an aggregated message received before but notified after the conversation was displayed")
|
||||
return
|
||||
}
|
||||
|
||||
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
||||
for (content in chatMessage.contents) {
|
||||
if (content.isFileTransfer) {
|
||||
Log.i("[Chat Messages] Android < 10 detected and WRITE_EXTERNAL_STORAGE permission isn't granted yet")
|
||||
requestWriteExternalStoragePermissionEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.ContactsSelectionViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomCreationViewModel : ContactsSelectionViewModel() {
|
||||
val chatRoomCreatedEvent: MutableLiveData<Event<ChatRoom>> by lazy {
|
||||
MutableLiveData<Event<ChatRoom>>()
|
||||
}
|
||||
|
||||
val createGroupChat = MutableLiveData<Boolean>()
|
||||
|
||||
val isEncrypted = MutableLiveData<Boolean>()
|
||||
|
||||
val waitForChatRoomCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val secureChatAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val secureChatMandatory: Boolean = corePreferences.forceEndToEndEncryptedChat
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(room: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
waitForChatRoomCreation.value = false
|
||||
Log.i("[Chat Room Creation] Chat room created")
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("[Chat Room Creation] Group chat room creation has failed !")
|
||||
waitForChatRoomCreation.value = false
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
createGroupChat.value = false
|
||||
isEncrypted.value = secureChatMandatory
|
||||
waitForChatRoomCreation.value = false
|
||||
secureChatAvailable.value = LinphoneUtils.isEndToEndEncryptedChatAvailable()
|
||||
}
|
||||
|
||||
fun updateEncryption(encrypted: Boolean) {
|
||||
if (!encrypted && secureChatMandatory) {
|
||||
Log.w("[Chat Room Creation] Something tries to force plain text chat room even if secureChatMandatory is enabled!")
|
||||
return
|
||||
}
|
||||
isEncrypted.value = encrypted
|
||||
}
|
||||
|
||||
fun createOneToOneChat(searchResult: SearchResult) {
|
||||
waitForChatRoomCreation.value = true
|
||||
val defaultAccount = coreContext.core.defaultAccount
|
||||
var room: ChatRoom?
|
||||
|
||||
val address = searchResult.address ?: coreContext.core.interpretUrl(searchResult.phoneNumber ?: "", LinphoneUtils.applyInternationalPrefix())
|
||||
if (address == null) {
|
||||
Log.e("[Chat Room Creation] Can't get a valid address from search result $searchResult")
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
waitForChatRoomCreation.value = false
|
||||
return
|
||||
}
|
||||
|
||||
val encrypted = secureChatMandatory || isEncrypted.value == true
|
||||
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
||||
params.backend = ChatRoomBackend.Basic
|
||||
params.isGroupEnabled = false
|
||||
if (encrypted) {
|
||||
params.isEncryptionEnabled = true
|
||||
params.backend = ChatRoomBackend.FlexisipChat
|
||||
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
|
||||
ChatRoomEphemeralMode.DeviceManaged
|
||||
else
|
||||
ChatRoomEphemeralMode.AdminManaged
|
||||
params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
Log.i("[Chat Room Creation] Ephemeral mode is ${params.ephemeralMode}, lifetime is ${params.ephemeralLifetime}")
|
||||
params.subject = AppUtils.getString(R.string.chat_room_dummy_subject)
|
||||
}
|
||||
|
||||
val participants = arrayOf(address)
|
||||
val localAddress: Address? = defaultAccount?.params?.identityAddress
|
||||
|
||||
room = coreContext.core.searchChatRoom(params, localAddress, null, participants)
|
||||
if (room == null) {
|
||||
Log.w("[Chat Room Creation] Couldn't find existing 1-1 chat room with remote ${address.asStringUriOnly()}, encryption=$encrypted and local identity ${localAddress?.asStringUriOnly()}")
|
||||
room = coreContext.core.createChatRoom(params, localAddress, participants)
|
||||
|
||||
if (room != null) {
|
||||
if (encrypted) {
|
||||
val state = room.state
|
||||
if (state == ChatRoom.State.Created) {
|
||||
Log.i("[Chat Room Creation] Found already created chat room, using it")
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
waitForChatRoomCreation.value = false
|
||||
} else {
|
||||
Log.i("[Chat Room Creation] Chat room creation is pending [$state], waiting for Created state")
|
||||
room.addListener(listener)
|
||||
}
|
||||
} else {
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
} else {
|
||||
Log.e("[Chat Room Creation] Couldn't create chat room with remote ${address.asStringUriOnly()} and local identity ${localAddress?.asStringUriOnly()}")
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
} else {
|
||||
Log.i("[Chat Room Creation] Found existing 1-1 chat room with remote ${address.asStringUriOnly()}, encryption=$encrypted and local identity ${localAddress?.asStringUriOnly()}")
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,382 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.animation.LinearInterpolator
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.ContactDataInterface
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ChatRoomViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterface {
|
||||
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
|
||||
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
|
||||
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
|
||||
override val showGroupChatAvatar: Boolean
|
||||
get() = conferenceChatRoom && !oneToOneChatRoom
|
||||
override val coroutineScope: CoroutineScope = viewModelScope
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val participants = MutableLiveData<String>()
|
||||
|
||||
val unreadMessagesCount = MutableLiveData<Int>()
|
||||
|
||||
val remoteIsComposing = MutableLiveData<Boolean>()
|
||||
|
||||
val composingList = MutableLiveData<String>()
|
||||
|
||||
val securityLevelIcon = MutableLiveData<Int>()
|
||||
|
||||
val securityLevelContentDescription = MutableLiveData<Int>()
|
||||
|
||||
val peerSipUri = MutableLiveData<String>()
|
||||
|
||||
val ephemeralEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val basicChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||
}
|
||||
|
||||
val oneToOneChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
|
||||
}
|
||||
|
||||
private val conferenceChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt())
|
||||
}
|
||||
|
||||
val encryptedChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())
|
||||
}
|
||||
|
||||
val ephemeralChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Ephemeral.toInt())
|
||||
}
|
||||
|
||||
val meAdmin: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
|
||||
val isUserScrollingUp = MutableLiveData<Boolean>()
|
||||
|
||||
var oneParticipantOneDevice: Boolean = false
|
||||
|
||||
var onlyParticipantOnlyDeviceAddress: Address? = null
|
||||
|
||||
val chatUnreadCountTranslateY = MutableLiveData<Float>()
|
||||
|
||||
val groupCallAvailable: Boolean
|
||||
get() = LinphoneUtils.isRemoteConferencingAvailable()
|
||||
|
||||
private var addressToCall: Address? = null
|
||||
|
||||
private val bounceAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset), 0f).apply {
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Float
|
||||
chatUnreadCountTranslateY.value = value
|
||||
}
|
||||
interpolator = LinearInterpolator()
|
||||
duration = 250
|
||||
repeatMode = ValueAnimator.REVERSE
|
||||
repeatCount = ValueAnimator.INFINITE
|
||||
}
|
||||
}
|
||||
|
||||
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
Log.d("[Chat Room] Contacts have changed")
|
||||
contactLookup()
|
||||
}
|
||||
}
|
||||
|
||||
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onChatRoomRead(core: Core, room: ChatRoom) {
|
||||
if (room == chatRoom) {
|
||||
updateUnreadMessageCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
Log.i("[Chat Room] $chatRoom state changed: $state")
|
||||
if (state == ChatRoom.State.Created) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
|
||||
override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array<out EventLog>) {
|
||||
updateUnreadMessageCount()
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onIsComposingReceived(
|
||||
chatRoom: ChatRoom,
|
||||
remoteAddr: Address,
|
||||
isComposing: Boolean
|
||||
) {
|
||||
updateRemotesComposing()
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
|
||||
override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
meAdmin.value = chatRoom.me?.isAdmin ?: false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.core.addListener(coreListener)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
coreContext.contactsManager.addListener(contactsUpdatedListener)
|
||||
|
||||
updateUnreadMessageCount()
|
||||
|
||||
subject.value = chatRoom.subject
|
||||
updateSecurityIcon()
|
||||
meAdmin.value = chatRoom.me?.isAdmin ?: false
|
||||
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||
|
||||
contactLookup()
|
||||
updateParticipants()
|
||||
|
||||
updateRemotesComposing()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.contactsManager.removeListener(contactsUpdatedListener)
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
chatRoom.core.removeListener(coreListener)
|
||||
if (corePreferences.enableAnimations) bounceAnimator.end()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun contactLookup() {
|
||||
displayName.value = when {
|
||||
basicChatRoom -> LinphoneUtils.getDisplayName(
|
||||
chatRoom.peerAddress
|
||||
)
|
||||
oneToOneChatRoom -> LinphoneUtils.getDisplayName(
|
||||
chatRoom.participants.firstOrNull()?.address ?: chatRoom.peerAddress
|
||||
)
|
||||
conferenceChatRoom -> chatRoom.subject.orEmpty()
|
||||
else -> chatRoom.peerAddress.asStringUriOnly()
|
||||
}
|
||||
|
||||
if (oneToOneChatRoom) {
|
||||
searchMatchingContact()
|
||||
} else {
|
||||
getParticipantsNames()
|
||||
}
|
||||
}
|
||||
|
||||
fun startCall() {
|
||||
val address = addressToCall
|
||||
if (address != null) {
|
||||
coreContext.startCall(address)
|
||||
}
|
||||
}
|
||||
|
||||
fun startGroupCall() {
|
||||
val conferenceScheduler = coreContext.core.createConferenceScheduler()
|
||||
val conferenceInfo = Factory.instance().createConferenceInfo()
|
||||
|
||||
val localAddress = chatRoom.localAddress.clone()
|
||||
localAddress.clean() // Remove GRUU
|
||||
val addresses = Array(chatRoom.participants.size) {
|
||||
index ->
|
||||
chatRoom.participants[index].address
|
||||
}
|
||||
val localAccount = coreContext.core.accountList.find {
|
||||
account ->
|
||||
account.params.identityAddress?.weakEqual(localAddress) ?: false
|
||||
}
|
||||
|
||||
conferenceInfo.organizer = localAddress
|
||||
conferenceInfo.subject = subject.value
|
||||
conferenceInfo.setParticipants(addresses)
|
||||
conferenceScheduler.account = localAccount
|
||||
// Will trigger the conference creation/update automatically
|
||||
conferenceScheduler.info = conferenceInfo
|
||||
}
|
||||
|
||||
fun areNotificationsMuted(): Boolean {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
|
||||
return corePreferences.chatRoomMuted(id)
|
||||
}
|
||||
|
||||
fun muteNotifications(mute: Boolean) {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
|
||||
corePreferences.muteChatRoom(id, mute)
|
||||
}
|
||||
|
||||
fun getRemoteAddress(): Address? {
|
||||
return if (basicChatRoom) {
|
||||
chatRoom.peerAddress
|
||||
} else {
|
||||
if (chatRoom.participants.isNotEmpty()) {
|
||||
chatRoom.participants[0].address
|
||||
} else {
|
||||
Log.e("[Chat Room] ${chatRoom.peerAddress} doesn't have any participant (state ${chatRoom.state})!")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMatchingContact() {
|
||||
val remoteAddress = getRemoteAddress()
|
||||
if (remoteAddress != null) {
|
||||
contact.value = coreContext.contactsManager.findContactByAddress(remoteAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getParticipantsNames() {
|
||||
if (oneToOneChatRoom) return
|
||||
|
||||
var participantsList = ""
|
||||
var index = 0
|
||||
for (participant in chatRoom.participants) {
|
||||
val contact = coreContext.contactsManager.findContactByAddress(participant.address)
|
||||
participantsList += contact?.name ?: LinphoneUtils.getDisplayName(participant.address)
|
||||
index++
|
||||
if (index != chatRoom.nbParticipants) participantsList += ", "
|
||||
}
|
||||
participants.value = participantsList
|
||||
}
|
||||
|
||||
private fun updateSecurityIcon() {
|
||||
val level = chatRoom.securityLevel
|
||||
securityLevel.value = level
|
||||
|
||||
securityLevelIcon.value = when (level) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
securityLevelContentDescription.value = when (level) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemotesComposing() {
|
||||
val isComposing = chatRoom.isRemoteComposing
|
||||
remoteIsComposing.value = isComposing
|
||||
if (!isComposing) return
|
||||
|
||||
var composing = ""
|
||||
for (address in chatRoom.composingAddresses) {
|
||||
val contact = coreContext.contactsManager.findContactByAddress(address)
|
||||
composing += if (composing.isNotEmpty()) ", " else ""
|
||||
composing += contact?.name ?: LinphoneUtils.getDisplayName(address)
|
||||
}
|
||||
composingList.value = AppUtils.getStringWithPlural(R.plurals.chat_room_remote_composing, chatRoom.composingAddresses.size, composing)
|
||||
}
|
||||
|
||||
private fun updateParticipants() {
|
||||
val participants = chatRoom.participants
|
||||
peerSipUri.value = if (oneToOneChatRoom && !basicChatRoom) {
|
||||
participants.firstOrNull()?.address?.asStringUriOnly()
|
||||
?: chatRoom.peerAddress.asStringUriOnly()
|
||||
} else {
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
}
|
||||
|
||||
oneParticipantOneDevice = oneToOneChatRoom &&
|
||||
chatRoom.me?.devices?.size == 1 &&
|
||||
participants.firstOrNull()?.devices?.size == 1
|
||||
|
||||
addressToCall = if (basicChatRoom)
|
||||
chatRoom.peerAddress
|
||||
else
|
||||
participants.firstOrNull()?.address
|
||||
|
||||
onlyParticipantOnlyDeviceAddress = participants.firstOrNull()?.devices?.firstOrNull()?.address
|
||||
}
|
||||
|
||||
private fun updateUnreadMessageCount() {
|
||||
val count = chatRoom.unreadMessagesCount
|
||||
unreadMessagesCount.value = count
|
||||
if (count > 0 && corePreferences.enableAnimations) bounceAnimator.start()
|
||||
else if (count == 0 && bounceAnimator.isStarted) bounceAnimator.end()
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomsListViewModel : MessageNotifierViewModel() {
|
||||
val chatRooms = MutableLiveData<ArrayList<ChatRoom>>()
|
||||
|
||||
val fileSharingPending = MutableLiveData<Boolean>()
|
||||
|
||||
val textSharingPending = MutableLiveData<Boolean>()
|
||||
|
||||
val forwardPending = MutableLiveData<Boolean>()
|
||||
|
||||
val groupChatAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val chatRoomIndexUpdatedEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onChatRoomStateChanged(core: Core, chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
updateChatRooms()
|
||||
} else if (state == ChatRoom.State.TerminationFailed) {
|
||||
Log.e("[Chat Rooms] Group chat room removal for address ${chatRoom.peerAddress.asStringUriOnly()} has failed !")
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_removal_failed_snack)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
|
||||
onChatRoomMessageEvent(chatRoom)
|
||||
}
|
||||
|
||||
override fun onMessagesReceived(
|
||||
core: Core,
|
||||
chatRoom: ChatRoom,
|
||||
messages: Array<out ChatMessage>
|
||||
) {
|
||||
onChatRoomMessageEvent(chatRoom)
|
||||
}
|
||||
|
||||
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
|
||||
notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
|
||||
override fun onChatRoomEphemeralMessageDeleted(core: Core, chatRoom: ChatRoom) {
|
||||
notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
|
||||
override fun onChatRoomSubjectChanged(core: Core, chatRoom: ChatRoom) {
|
||||
notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
private val chatRoomListener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State) {
|
||||
if (newState == ChatRoom.State.Deleted) {
|
||||
updateChatRooms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val contactsListener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
updateChatRooms()
|
||||
}
|
||||
}
|
||||
|
||||
private var chatRoomsToDeleteCount = 0
|
||||
|
||||
init {
|
||||
groupChatAvailable.value = LinphoneUtils.isGroupChatAvailable()
|
||||
updateChatRooms()
|
||||
coreContext.core.addListener(listener)
|
||||
coreContext.contactsManager.addListener(contactsListener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.contactsManager.removeListener(contactsListener)
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun deleteChatRoom(chatRoom: ChatRoom?) {
|
||||
for (eventLog in chatRoom?.getHistoryMessageEvents(0).orEmpty()) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||
}
|
||||
|
||||
chatRoomsToDeleteCount = 1
|
||||
if (chatRoom != null) {
|
||||
coreContext.notificationsManager.dismissChatNotification(chatRoom)
|
||||
Compatibility.removeChatRoomShortcut(coreContext.context, chatRoom)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
coreContext.core.deleteChatRoom(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteChatRooms(chatRooms: ArrayList<ChatRoom>) {
|
||||
chatRoomsToDeleteCount = chatRooms.size
|
||||
for (chatRoom in chatRooms) {
|
||||
for (eventLog in chatRoom.getHistoryMessageEvents(0)) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||
}
|
||||
|
||||
coreContext.notificationsManager.dismissChatNotification(chatRoom)
|
||||
Compatibility.removeChatRoomShortcut(coreContext.context, chatRoom)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
chatRoom.core.deleteChatRoom(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChatRooms() {
|
||||
val list = arrayListOf<ChatRoom>()
|
||||
list.addAll(coreContext.core.chatRooms)
|
||||
chatRooms.value = list
|
||||
}
|
||||
|
||||
fun notifyChatRoomUpdate(chatRoom: ChatRoom) {
|
||||
val index = findChatRoomIndex(chatRoom)
|
||||
if (index == -1) updateChatRooms()
|
||||
else chatRoomIndexUpdatedEvent.value = Event(index)
|
||||
}
|
||||
|
||||
private fun reorderChatRooms() {
|
||||
val list = arrayListOf<ChatRoom>()
|
||||
list.addAll(chatRooms.value.orEmpty())
|
||||
list.sortByDescending { chatRoom -> chatRoom.lastUpdateTime }
|
||||
chatRooms.value = list
|
||||
}
|
||||
|
||||
private fun findChatRoomIndex(chatRoom: ChatRoom): Int {
|
||||
for ((index, cr) in chatRooms.value.orEmpty().withIndex()) {
|
||||
if (LinphoneUtils.areChatRoomsTheSame(cr, chatRoom)) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun onChatRoomMessageEvent(chatRoom: ChatRoom) {
|
||||
when (findChatRoomIndex(chatRoom)) {
|
||||
-1 -> updateChatRooms()
|
||||
0 -> chatRoomIndexUpdatedEvent.value = Event(0)
|
||||
else -> reorderChatRooms()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.activities.main.chat.data.DevicesListGroupData
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomListenerStub
|
||||
import org.linphone.core.EventLog
|
||||
|
||||
class DevicesListViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return DevicesListViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class DevicesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
val participants = MutableLiveData<ArrayList<DevicesListGroupData>>()
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onParticipantDeviceAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
|
||||
chatRoom.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun updateParticipants() {
|
||||
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
|
||||
|
||||
val list = arrayListOf<DevicesListGroupData>()
|
||||
val me = chatRoom.me
|
||||
if (me != null) list.add(DevicesListGroupData(me))
|
||||
for (participant in chatRoom.participants) {
|
||||
list.add(DevicesListGroupData(participant))
|
||||
}
|
||||
|
||||
participants.value = list
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.DurationItemClicked
|
||||
import org.linphone.activities.main.chat.data.EphemeralDurationData
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class EphemeralViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return EphemeralViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
val durationsList = MutableLiveData<ArrayList<EphemeralDurationData>>()
|
||||
|
||||
var currentSelectedDuration: Long = 0
|
||||
|
||||
private val listener = object : DurationItemClicked {
|
||||
override fun onDurationValueChanged(duration: Long) {
|
||||
currentSelectedDuration = duration
|
||||
computeEphemeralDurationValues()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Log.i("[Ephemeral Messages] Current lifetime is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.isEphemeralEnabled}")
|
||||
currentSelectedDuration = if (chatRoom.isEphemeralEnabled) chatRoom.ephemeralLifetime else 0
|
||||
computeEphemeralDurationValues()
|
||||
}
|
||||
|
||||
fun updateChatRoomEphemeralDuration() {
|
||||
Log.i("[Ephemeral Messages] Selected value is $currentSelectedDuration")
|
||||
if (currentSelectedDuration > 0) {
|
||||
if (chatRoom.ephemeralLifetime != currentSelectedDuration) {
|
||||
Log.i("[Ephemeral Messages] Setting new lifetime for ephemeral messages to $currentSelectedDuration")
|
||||
chatRoom.ephemeralLifetime = currentSelectedDuration
|
||||
} else {
|
||||
Log.i("[Ephemeral Messages] Configured lifetime for ephemeral messages was already $currentSelectedDuration")
|
||||
}
|
||||
|
||||
if (!chatRoom.isEphemeralEnabled) {
|
||||
Log.i("[Ephemeral Messages] Ephemeral messages were disabled, enable them")
|
||||
chatRoom.isEphemeralEnabled = true
|
||||
}
|
||||
} else if (chatRoom.isEphemeralEnabled) {
|
||||
Log.i("[Ephemeral Messages] Ephemeral messages were enabled, disable them")
|
||||
chatRoom.isEphemeralEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeEphemeralDurationValues() {
|
||||
val list = arrayListOf<EphemeralDurationData>()
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_disabled, currentSelectedDuration, 0, listener))
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_one_minute, currentSelectedDuration, 60, listener))
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_one_hour, currentSelectedDuration, 3600, listener))
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_one_day, currentSelectedDuration, 86400, listener))
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_three_days, currentSelectedDuration, 259200, listener))
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_one_week, currentSelectedDuration, 604800, listener))
|
||||
durationsList.value = list
|
||||
}
|
||||
}
|
@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.data.GroupInfoParticipantData
|
||||
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class GroupInfoViewModelFactory(private val chatRoom: ChatRoom?) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return GroupInfoViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class GroupInfoViewModel(val chatRoom: ChatRoom?) : MessageNotifierViewModel() {
|
||||
val createdChatRoomEvent = MutableLiveData<Event<ChatRoom>>()
|
||||
val updatedChatRoomEvent = MutableLiveData<Event<ChatRoom>>()
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val participants = MutableLiveData<ArrayList<GroupInfoParticipantData>>()
|
||||
|
||||
val isEncrypted = MutableLiveData<Boolean>()
|
||||
|
||||
val isMeAdmin = MutableLiveData<Boolean>()
|
||||
|
||||
val canLeaveGroup = MutableLiveData<Boolean>()
|
||||
|
||||
val waitForChatRoomCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val meAdminChangedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
waitForChatRoomCreation.value = false
|
||||
createdChatRoomEvent.value = Event(chatRoom) // To trigger going to the chat room
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("[Chat Room Group Info] Group chat room creation has failed !")
|
||||
waitForChatRoomCreation.value = false
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
val admin = chatRoom.me?.isAdmin ?: false
|
||||
if (admin != isMeAdmin.value) {
|
||||
isMeAdmin.value = admin
|
||||
meAdminChangedEvent.value = Event(admin)
|
||||
}
|
||||
updateParticipants()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
subject.value = chatRoom?.subject
|
||||
isMeAdmin.value = chatRoom == null || (chatRoom.me?.isAdmin == true && !chatRoom.isReadOnly)
|
||||
canLeaveGroup.value = chatRoom != null && !chatRoom.isReadOnly
|
||||
isEncrypted.value = corePreferences.forceEndToEndEncryptedChat || chatRoom?.hasCapability(ChatRoomCapabilities.Encrypted.toInt()) == true
|
||||
|
||||
if (chatRoom != null) updateParticipants()
|
||||
|
||||
chatRoom?.addListener(listener)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
participants.value.orEmpty().forEach(GroupInfoParticipantData::destroy)
|
||||
chatRoom?.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun createChatRoom() {
|
||||
waitForChatRoomCreation.value = true
|
||||
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
||||
params.isEncryptionEnabled = corePreferences.forceEndToEndEncryptedChat || isEncrypted.value == true
|
||||
params.isGroupEnabled = true
|
||||
if (params.isEncryptionEnabled) {
|
||||
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
|
||||
ChatRoomEphemeralMode.DeviceManaged
|
||||
else
|
||||
ChatRoomEphemeralMode.AdminManaged
|
||||
}
|
||||
params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
Log.i("[Chat Room Group Info] Ephemeral mode is ${params.ephemeralMode}, lifetime is ${params.ephemeralLifetime}")
|
||||
params.subject = subject.value
|
||||
|
||||
val addresses = arrayOfNulls<Address>(participants.value.orEmpty().size)
|
||||
var index = 0
|
||||
for (participant in participants.value.orEmpty()) {
|
||||
addresses[index] = participant.participant.address
|
||||
Log.i("[Chat Room Group Info] Participant ${participant.sipUri} will be added to group")
|
||||
index += 1
|
||||
}
|
||||
|
||||
val chatRoom: ChatRoom? = coreContext.core.createChatRoom(params, coreContext.core.defaultAccount?.params?.identityAddress, addresses)
|
||||
chatRoom?.addListener(listener)
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Chat Room Group Info] Couldn't create chat room!")
|
||||
waitForChatRoomCreation.value = false
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateRoom() {
|
||||
if (chatRoom != null) {
|
||||
// Subject
|
||||
val newSubject = subject.value.orEmpty()
|
||||
if (newSubject.isNotEmpty() && newSubject != chatRoom.subject) {
|
||||
Log.i("[Chat Room Group Info] Subject changed to $newSubject")
|
||||
chatRoom.subject = newSubject
|
||||
}
|
||||
|
||||
// Removed participants
|
||||
val participantsToRemove = arrayListOf<Participant>()
|
||||
for (participant in chatRoom.participants) {
|
||||
val member = participants.value.orEmpty().find { member ->
|
||||
participant.address.weakEqual(member.participant.address)
|
||||
}
|
||||
if (member == null) {
|
||||
Log.w("[Chat Room Group Info] Participant ${participant.address.asStringUriOnly()} will be removed from group")
|
||||
participantsToRemove.add(participant)
|
||||
}
|
||||
}
|
||||
val toRemove = arrayOfNulls<Participant>(participantsToRemove.size)
|
||||
participantsToRemove.toArray(toRemove)
|
||||
chatRoom.removeParticipants(toRemove)
|
||||
|
||||
// Added participants & new admins
|
||||
val participantsToAdd = arrayListOf<Address>()
|
||||
for (member in participants.value.orEmpty()) {
|
||||
val participant = chatRoom.participants.find { participant ->
|
||||
participant.address.weakEqual(member.participant.address)
|
||||
}
|
||||
if (participant != null) {
|
||||
// Participant found, check if admin status needs to be updated
|
||||
if (member.participant.isAdmin != participant.isAdmin) {
|
||||
if (chatRoom.me?.isAdmin == true) {
|
||||
Log.i("[Chat Room Group Info] Participant ${member.sipUri} will be admin? ${member.isAdmin}")
|
||||
chatRoom.setParticipantAdminStatus(participant, member.participant.isAdmin)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.i("[Chat Room Group Info] Participant ${member.sipUri} will be added to group")
|
||||
participantsToAdd.add(member.participant.address)
|
||||
}
|
||||
}
|
||||
val toAdd = arrayOfNulls<Address>(participantsToAdd.size)
|
||||
participantsToAdd.toArray(toAdd)
|
||||
chatRoom.addParticipants(toAdd)
|
||||
|
||||
// Go back to chat room
|
||||
updatedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun leaveGroup() {
|
||||
if (chatRoom != null) {
|
||||
Log.w("[Chat Room Group Info] Leaving group")
|
||||
chatRoom.leave()
|
||||
updatedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeParticipant(participant: GroupChatRoomMember) {
|
||||
val list = arrayListOf<GroupInfoParticipantData>()
|
||||
for (data in participants.value.orEmpty()) {
|
||||
if (!data.participant.address.weakEqual(participant.address)) {
|
||||
list.add(data)
|
||||
}
|
||||
}
|
||||
participants.value = list
|
||||
}
|
||||
|
||||
private fun updateParticipants() {
|
||||
val list = arrayListOf<GroupInfoParticipantData>()
|
||||
|
||||
if (chatRoom != null) {
|
||||
for (participant in chatRoom.participants) {
|
||||
list.add(
|
||||
GroupInfoParticipantData(
|
||||
GroupChatRoomMember(participant.address, participant.isAdmin, participant.securityLevel, canBeSetAdmin = true)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
participants.value = list
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||
import org.linphone.activities.main.chat.data.ImdnParticipantData
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatMessageListenerStub
|
||||
import org.linphone.core.ParticipantImdnState
|
||||
|
||||
class ImdnViewModelFactory(private val chatMessage: ChatMessage) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ImdnViewModel(chatMessage) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ImdnViewModel(private val chatMessage: ChatMessage) : ViewModel() {
|
||||
val participants = MutableLiveData<ArrayList<ImdnParticipantData>>()
|
||||
|
||||
val chatMessageViewModel = ChatMessageData(chatMessage)
|
||||
|
||||
private val listener = object : ChatMessageListenerStub() {
|
||||
override fun onParticipantImdnStateChanged(
|
||||
message: ChatMessage,
|
||||
state: ParticipantImdnState
|
||||
) {
|
||||
updateParticipantsLists()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatMessage.addListener(listener)
|
||||
updateParticipantsLists()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
participants.value.orEmpty().forEach(ImdnParticipantData::destroy)
|
||||
chatMessage.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun updateParticipantsLists() {
|
||||
val list = arrayListOf<ImdnParticipantData>()
|
||||
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.Displayed)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.DeliveredToUser)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.Delivered)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.NotDelivered)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
|
||||
participants.value = list
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.views
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Layout
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* The purpose of this class is to have a TextView declared with wrap_content as width that won't
|
||||
* fill it's parent if it is multi line.
|
||||
*/
|
||||
class MultiLineWrapContentWidthTextView : AppCompatTextView {
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun setText(text: CharSequence?, type: BufferType?) {
|
||||
super.setText(text, type)
|
||||
// Required for PatternClickableSpan
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||
super.onMeasure(widthSpec, heightSpec)
|
||||
|
||||
if (layout != null && layout.lineCount >= 2) {
|
||||
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
|
||||
val uselessPaddingWidth = layout.width - maxLineWidth
|
||||
val width = measuredWidth - uselessPaddingWidth
|
||||
val height = measuredHeight
|
||||
setMeasuredDimension(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaxLineWidth(layout: Layout): Float {
|
||||
var maxWidth = 0.0f
|
||||
val lines = layout.lineCount
|
||||
for (i in 0 until lines) {
|
||||
if (layout.getLineWidth(i) > maxWidth) {
|
||||
maxWidth = layout.getLineWidth(i)
|
||||
}
|
||||
}
|
||||
return maxWidth
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.views
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.KeyEvent
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import org.linphone.activities.main.chat.receivers.RichContentReceiver
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
/**
|
||||
* Allows for image input inside an EditText, usefull for keyboards with gif support for example.
|
||||
*/
|
||||
class RichEditText : AppCompatEditText {
|
||||
private var controlPressed = false
|
||||
|
||||
private var sendListener: RichEditTextSendListener? = null
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
initReceiveContentListener()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
initReceiveContentListener()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
) {
|
||||
initReceiveContentListener()
|
||||
}
|
||||
|
||||
fun setControlEnterListener(listener: RichEditTextSendListener) {
|
||||
sendListener = listener
|
||||
}
|
||||
|
||||
private fun initReceiveContentListener() {
|
||||
ViewCompat.setOnReceiveContentListener(
|
||||
this, RichContentReceiver.MIME_TYPES,
|
||||
RichContentReceiver { uri ->
|
||||
Log.i("[Rich Edit Text] Received URI: $uri")
|
||||
val activity = context as Activity
|
||||
val sharedViewModel = activity.run {
|
||||
ViewModelProvider(activity as ViewModelStoreOwner)[SharedMainViewModel::class.java]
|
||||
}
|
||||
sharedViewModel.richContentUri.value = Event(uri)
|
||||
}
|
||||
)
|
||||
|
||||
setOnKeyListener { _, keyCode, event ->
|
||||
if (keyCode == KeyEvent.KEYCODE_CTRL_LEFT) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
controlPressed = true
|
||||
} else if (event.action == KeyEvent.ACTION_UP) {
|
||||
controlPressed = false
|
||||
}
|
||||
false
|
||||
} else if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP && controlPressed) {
|
||||
sendListener?.onControlEnterPressedAndReleased()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface RichEditTextSendListener {
|
||||
fun onControlEnterPressedAndReleased()
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.conference.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.adapters.SelectionListAdapter
|
||||
import org.linphone.activities.main.conference.data.ScheduledConferenceData
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.databinding.ConferenceScheduleCellBinding
|
||||
import org.linphone.databinding.ConferenceScheduleListHeaderBinding
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ScheduledConferencesAdapter(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : SelectionListAdapter<ScheduledConferenceData, RecyclerView.ViewHolder>(selectionVM, ConferenceInfoDiffCallback()),
|
||||
HeaderAdapter {
|
||||
val copyAddressToClipboardEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val joinConferenceEvent: MutableLiveData<Event<Pair<String, String?>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String?>>>()
|
||||
}
|
||||
|
||||
val editConferenceEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val deleteConferenceInfoEvent: MutableLiveData<Event<ScheduledConferenceData>> by lazy {
|
||||
MutableLiveData<Event<ScheduledConferenceData>>()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScheduledConferencesAdapter.ViewHolder {
|
||||
val binding: ConferenceScheduleCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.conference_schedule_cell, parent, false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ScheduledConferencesAdapter.ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
if (position >= itemCount) return false
|
||||
val conferenceInfo = getItem(position)
|
||||
val previousPosition = position - 1
|
||||
return if (previousPosition >= 0) {
|
||||
val previousItem = getItem(previousPosition)
|
||||
!TimestampUtils.isSameDay(previousItem.conferenceInfo.dateTime, conferenceInfo.conferenceInfo.dateTime)
|
||||
} else true
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val data = getItem(position)
|
||||
val binding: ConferenceScheduleListHeaderBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.conference_schedule_list_header, null, false
|
||||
)
|
||||
binding.title = formatDate(context, data.conferenceInfo.dateTime)
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun formatDate(context: Context, date: Long): String {
|
||||
if (TimestampUtils.isToday(date)) {
|
||||
return context.getString(R.string.today)
|
||||
}
|
||||
return TimestampUtils.toString(date, onlyDate = true, shortDate = false, hideYear = false)
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: ConferenceScheduleCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(conferenceData: ScheduledConferenceData) {
|
||||
with(binding) {
|
||||
data = conferenceData
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
} else {
|
||||
conferenceData.toggleExpand()
|
||||
}
|
||||
}
|
||||
|
||||
setLongClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == false) {
|
||||
selectionViewModel.isEditionEnabled.value = true
|
||||
// Selection will be handled by click listener
|
||||
true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
setCopyAddressClickListener {
|
||||
val address = conferenceData.getAddressAsString()
|
||||
if (address.isNotEmpty()) {
|
||||
copyAddressToClipboardEvent.value = Event(address)
|
||||
}
|
||||
}
|
||||
|
||||
setJoinConferenceClickListener {
|
||||
val address = conferenceData.conferenceInfo.uri
|
||||
if (address != null) {
|
||||
joinConferenceEvent.value = Event(Pair(address.asStringUriOnly(), conferenceData.conferenceInfo.subject))
|
||||
}
|
||||
}
|
||||
|
||||
setEditConferenceClickListener {
|
||||
val address = conferenceData.conferenceInfo.uri
|
||||
if (address != null) {
|
||||
editConferenceEvent.value = Event(address.asStringUriOnly())
|
||||
}
|
||||
}
|
||||
|
||||
setDeleteConferenceClickListener {
|
||||
deleteConferenceInfoEvent.value = Event(conferenceData)
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConferenceInfoDiffCallback : DiffUtil.ItemCallback<ScheduledConferenceData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ScheduledConferenceData,
|
||||
newItem: ScheduledConferenceData
|
||||
): Boolean {
|
||||
return oldItem.conferenceInfo == newItem.conferenceInfo
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ScheduledConferenceData,
|
||||
newItem: ScheduledConferenceData
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.conference.data
|
||||
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ConferenceSchedulingParticipantData(
|
||||
private val sipAddress: Address,
|
||||
val showLimeBadge: Boolean = false,
|
||||
val showDivider: Boolean = true
|
||||
) :
|
||||
GenericContactData(sipAddress) {
|
||||
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(sipAddress)
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.conference.data
|
||||
|
||||
class Duration(val value: Int, val display: String) : Comparable<Duration> {
|
||||
override fun toString(): String {
|
||||
return display
|
||||
}
|
||||
|
||||
override fun compareTo(other: Duration): Int {
|
||||
return value.compareTo(other.value)
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.conference.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.ConferenceInfo
|
||||
import org.linphone.core.ConferenceInfo.State
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ScheduledConferenceData(val conferenceInfo: ConferenceInfo, private val isFinished: Boolean) {
|
||||
val expanded = MutableLiveData<Boolean>()
|
||||
val backgroundResId = MutableLiveData<Int>()
|
||||
|
||||
val address = MutableLiveData<String>()
|
||||
val subject = MutableLiveData<String>()
|
||||
val description = MutableLiveData<String>()
|
||||
val time = MutableLiveData<String>()
|
||||
val date = MutableLiveData<String>()
|
||||
val duration = MutableLiveData<String>()
|
||||
val organizer = MutableLiveData<String>()
|
||||
val canEdit = MutableLiveData<Boolean>()
|
||||
val participantsShort = MutableLiveData<String>()
|
||||
val participantsExpanded = MutableLiveData<String>()
|
||||
val showDuration = MutableLiveData<Boolean>()
|
||||
val isConferenceCancelled = MutableLiveData<Boolean>()
|
||||
|
||||
init {
|
||||
expanded.value = false
|
||||
|
||||
address.value = conferenceInfo.uri?.asStringUriOnly()
|
||||
subject.value = conferenceInfo.subject
|
||||
description.value = conferenceInfo.description
|
||||
|
||||
time.value = TimestampUtils.timeToString(conferenceInfo.dateTime)
|
||||
date.value = TimestampUtils.toString(conferenceInfo.dateTime, onlyDate = true, shortDate = false, hideYear = false)
|
||||
isConferenceCancelled.value = conferenceInfo.state == State.Cancelled
|
||||
|
||||
val minutes = conferenceInfo.duration
|
||||
val hours = TimeUnit.MINUTES.toHours(minutes.toLong())
|
||||
val remainMinutes = minutes - TimeUnit.HOURS.toMinutes(hours).toInt()
|
||||
duration.value = TimestampUtils.durationToString(hours.toInt(), remainMinutes)
|
||||
showDuration.value = minutes > 0
|
||||
|
||||
val organizerAddress = conferenceInfo.organizer
|
||||
if (organizerAddress != null) {
|
||||
val localAccount = coreContext.core.accountList.find { account ->
|
||||
val address = account.params.identityAddress
|
||||
address != null && organizerAddress.weakEqual(address)
|
||||
}
|
||||
canEdit.value = localAccount != null
|
||||
|
||||
val contact = coreContext.contactsManager.findContactByAddress(organizerAddress)
|
||||
organizer.value = if (contact != null)
|
||||
contact.name
|
||||
else
|
||||
LinphoneUtils.getDisplayName(conferenceInfo.organizer)
|
||||
} else {
|
||||
canEdit.value = false
|
||||
Log.e("[Scheduled Conference] No organizer SIP URI found for: ${conferenceInfo.uri?.asStringUriOnly()}")
|
||||
}
|
||||
|
||||
computeBackgroundResId()
|
||||
computeParticipantsLists()
|
||||
}
|
||||
|
||||
fun destroy() {}
|
||||
|
||||
fun delete() {
|
||||
Log.w("[Scheduled Conference] Deleting conference info with URI: ${conferenceInfo.uri?.asStringUriOnly()}")
|
||||
coreContext.core.deleteConferenceInformation(conferenceInfo)
|
||||
}
|
||||
|
||||
fun toggleExpand() {
|
||||
expanded.value = expanded.value == false
|
||||
computeBackgroundResId()
|
||||
}
|
||||
|
||||
fun getAddressAsString(): String {
|
||||
val address = conferenceInfo.uri?.clone()
|
||||
if (address != null) {
|
||||
address.displayName = conferenceInfo.subject
|
||||
return address.asString()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun computeBackgroundResId() {
|
||||
backgroundResId.value = if (conferenceInfo.state == State.Cancelled) {
|
||||
if (expanded.value == true) {
|
||||
R.drawable.shape_round_red_background_with_orange_border
|
||||
} else {
|
||||
R.drawable.shape_round_red_background
|
||||
}
|
||||
} else if (isFinished) {
|
||||
if (expanded.value == true) {
|
||||
R.drawable.shape_round_dark_gray_background_with_orange_border
|
||||
} else {
|
||||
R.drawable.shape_round_dark_gray_background
|
||||
}
|
||||
} else {
|
||||
if (expanded.value == true) {
|
||||
R.drawable.shape_round_gray_background_with_orange_border
|
||||
} else {
|
||||
R.drawable.shape_round_gray_background
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeParticipantsLists() {
|
||||
var participantsListShort = ""
|
||||
var participantsListExpanded = ""
|
||||
|
||||
for (participant in conferenceInfo.participants) {
|
||||
val contact = coreContext.contactsManager.findContactByAddress(participant)
|
||||
val name = if (contact != null) contact.name else LinphoneUtils.getDisplayName(participant)
|
||||
val address = participant.asStringUriOnly()
|
||||
participantsListShort += "$name, "
|
||||
participantsListExpanded += "$name ($address)\n"
|
||||
}
|
||||
participantsListShort = participantsListShort.dropLast(2)
|
||||
participantsListExpanded = participantsListExpanded.dropLast(1)
|
||||
|
||||
participantsShort.value = participantsListShort
|
||||
participantsExpanded.value = participantsListExpanded
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user