Compare commits

...

2074 commits

Author SHA1 Message Date
Sylvain Berfini
8cb883a978 Workaround to prevent numpad from being briefly displayed when answering a video call 2026-01-20 17:19:45 +01:00
Sylvain Berfini
539439a055 Updated CHANGELOG with 6.0.22 release info 2026-01-20 16:51:29 +01:00
Sylvain Berfini
597581faa5 Fixed use of TelecomManager APIs 2026-01-20 14:23:43 +01:00
Sylvain Berfini
5391c12473 Bumped AGP to 9.0 (must keep using kapt instead of ksp because of databinding) and build using Java 21 + updated docker image 2026-01-20 14:04:33 +01:00
Sylvain Berfini
cea47a767b Reduced bottom bar height while in portrait to match conversation's send area height 2026-01-20 09:35:23 +01:00
Sylvain Berfini
e6f897cb39 Change layout to default one (instead of audion only) when joining a conference using a SIP URI 2026-01-15 15:22:45 +01:00
Sylvain Berfini
97606578a4 Updated gradle to 9.1.0 2026-01-15 11:13:26 +01:00
Sylvain Berfini
48c9b4d7af Updated dependencies 2026-01-15 09:15:03 +01:00
Sylvain Berfini
7d0d1a22ee Fixed text alignment 2026-01-14 19:54:54 +01:00
Sylvain Berfini
d467699388 Factorized code related to copying data into clipboard, added exception catch 2026-01-14 11:53:06 +01:00
Sylvain Berfini
5ad7b5da14 Update chat room shortcuts each time a notification is about to be created + report 'use' when opening a conversation 2026-01-14 09:56:03 +01:00
Sylvain Berfini
50c922b581 Small UI changes to improve look & feel 2026-01-14 09:39:46 +01:00
Sylvain Berfini
b574bf420c Fixed file viewer hover color 2026-01-13 10:20:52 +01:00
Sylvain Berfini
3df3c8d741 Fixed generated avatar size sometimes small 2026-01-12 16:44:25 +01:00
Sylvain Berfini
4039da9c8a Improved meeting layout 2026-01-12 16:19:33 +01:00
Sylvain Berfini
3cec19126d Fixed contacts not updated with LDAP results if latest query fails 2026-01-12 15:23:29 +01:00
Sylvain Berfini
a816a956b8 Added missing hover effects 2026-01-12 15:12:14 +01:00
Sylvain Berfini
cec3639b73 Try to workaround race condition on slow phones that may happen with foreground service notification not displayed in time 2026-01-09 10:53:07 +01:00
Sylvain Berfini
07cae7eb12 When opening delivery status bottom sheet select 'first' not empty tab automatically 2026-01-09 10:30:11 +01:00
Sylvain Berfini
3b561275a4 Close search bar when opening bottom sheet and vice versa 2026-01-08 12:17:24 +01:00
Sylvain Berfini
3f868e02fe Remove highlight from previous match when navigating between search results 2026-01-08 12:06:13 +01:00
Sylvain Berfini
57644a34de Changed mentions color in chat bubble to app's primary 2026-01-08 11:45:46 +01:00
Sylvain Berfini
e306c8c7fc Do not rely on chatMessageSending callback for scenario where message(s) are queued because of delayed subscribe 2026-01-08 09:54:25 +01:00
Sylvain Berfini
dd9190df07 Fixed layout broken by recyclerview id change 2026-01-06 17:29:15 +01:00
Sylvain Berfini
d90861b5f3 Cancel search when making reply/edit/forward action on a message 2026-01-06 16:20:00 +01:00
Sylvain Berfini
9151898a4d Disable search direction after latest matching result found 2026-01-06 16:09:47 +01:00
Sylvain Berfini
fe788caf0e Added label when no participant match user input after typing @ in a group conversation 2026-01-06 12:18:50 +01:00
Sylvain Berfini
7e0353cc91 Prevent participants list from blinking if it hasn't changed, using RecyclerView will also improve performances a bit for conversations with a lot of participants 2026-01-06 10:47:54 +01:00
Sylvain Berfini
a897c127e5 Using onFileTransferTerminated instead of relying on FileTransferDone chat message state 2026-01-05 15:58:18 +01:00
Sylvain Berfini
50aa053c19 Filter participants list using user input after '@' 2026-01-05 15:15:02 +01:00
Sylvain Berfini
b88b6a8093 Fixed hidden reply area when adding file to send area 2026-01-05 14:19:08 +01:00
Sylvain Berfini
24d808b1a7 Hide presence SIP addresses from contact details & editor views 2026-01-05 11:58:35 +01:00
Sylvain Berfini
6f09853424 Fixed attaching file to conversation from third party app using shortcut if matching conversation is already displayed 2026-01-05 10:40:35 +01:00
Sylvain Berfini
a7593e07fc Added a setting to edit native contacts in-app 2026-01-05 09:24:57 +01:00
Sylvain Berfini
844b182df2 Bumped version code 2025-12-18 15:37:12 +01:00
Sylvain Berfini
d299b0b129 Added RC flag to allow disabling add contact feature 2025-12-17 09:43:30 +00:00
Sylvain Berfini
965b159139 Using newly added MWI API to only show the notification bar for the concerned account + call voicemail when clicking on it 2025-12-17 08:09:22 +00:00
Sylvain Berfini
00b8e59ade Updated CHANGELOG with 6.0.21 release info 2025-12-16 16:23:11 +01:00
Sylvain Berfini
be47deeb40 Fixed self avatar not displayed in call views 2025-12-15 14:15:13 +01:00
Sylvain Berfini
e8c67fdd6f Don't use connected bluetooth audio device (if any) for recording a voice message 2025-12-15 11:21:44 +01:00
Sylvain Berfini
ff98c15840 Fixed crash due to empty routes & when setting an empty one in account params 2025-12-15 09:40:15 +01:00
Sylvain Berfini
3711fd749e Bumped AGP to 8.13.2 2025-12-12 10:15:00 +01:00
Sylvain Berfini
dce7095f74 Added workaround for linphone-config:// URIs 2025-12-11 11:09:25 +01:00
Sylvain Berfini
d2b12159af Allow linphone-config URIs in QR codes scanned inside Linphone 2025-12-11 10:25:46 +01:00
Sylvain Berfini
618be9ee7c Fixed contact not updated when company or job title was removed from native contact 2025-12-08 15:25:23 +01:00
Sylvain Berfini
e1abcc6dca Fixed ContactLoader not notifying app of changes when editing a native friend through another app 2025-12-08 15:04:43 +01:00
Sylvain Berfini
2a9ef440b7 Code small improvements 2025-12-05 11:28:41 +01:00
Sylvain Berfini
77b933c5a8 Migrated translations from release/6.0 branch 2025-12-05 11:19:36 +01:00
Sylvain Berfini
6b56165d4f Bumped version code to 6.01.001 and updated CHANGELOG 2025-12-05 10:33:38 +01:00
Sylvain Berfini
61c79a86f7 Added seek to recordings player & media player 2025-12-05 10:04:13 +01:00
Sylvain Berfini
40d195e06b Updated dependencies 2025-12-04 12:03:54 +00:00
Sylvain Berfini
e173e402c2 Added answer/decline keyboard shortcuts to CallActivity 2025-12-04 12:55:52 +01:00
Sylvain Berfini
7817e6603c Force front camera as default device when leaving QR code fragment 2025-12-03 14:19:49 +01:00
Sylvain Berfini
bf4b5a51f5 Added back largeHeap in Manifest XML file 2025-12-02 13:26:18 +01:00
Sylvain Berfini
3ffda24b82 Workaround missing audio focus requests & wrong audio manager mode when TelecomManager APIs aren't available on device 2025-12-02 11:05:11 +01:00
Sylvain Berfini
c99acbb5e1 Added missing update unread chat message count when a message has been retracted 2025-12-01 10:57:08 +01:00
Sylvain Berfini
cc1cc7d929 Fixed crash seen on Crashlytics due to clipboard data text being null 2025-12-01 08:38:09 +00:00
Sylvain Berfini
6bcce4ddbf Fixed call recording wrong indicator in case UPDATE isn't answered 2025-11-25 12:57:09 +01:00
Sylvain Berfini
696a593cbc Prevent replying to retracted message with swipe action 2025-11-24 15:20:48 +01:00
Sylvain Berfini
88e474533e Fixed reply preview when a message has been deleted (locally or remotely) 2025-11-24 14:11:03 +01:00
Sylvain Berfini
8e76c60a38 Added more info to startup listener, also log 3 previous startup reasons 2025-11-24 11:52:17 +01:00
Sylvain Berfini
85aa50d8d8 Fixed in-call media encryption alignment 2025-11-24 10:35:16 +01:00
Sylvain Berfini
c496545023 Added missing try/catch around some startActivity to prevent not found exceptions 2025-11-21 16:36:22 +01:00
Sylvain Berfini
1183a9e1c2 Updated CHANGELOG & version code from release/6.0 branch 2025-11-21 10:01:34 +01:00
Sylvain Berfini
170cd6fccc Cancel voice message recording when starting editing already sent text message 2025-11-20 15:50:56 +01:00
Sylvain Berfini
bc7ac8be64 Fixed issue with recording player fragment layout using lateinit property model 2025-11-18 17:12:44 +01:00
Sylvain Berfini
5ed68e0171 Remove AuthInfo when creating CardDAV entry if synchronization fails 2025-11-18 14:38:22 +01:00
Sylvain Berfini
41e6776b32 Use newly available API to properly remove account and all associated data 2025-11-18 11:38:42 +01:00
Sylvain Berfini
e290a8c4ea Added resources shrink to release build 2025-11-18 10:43:12 +01:00
Sylvain Berfini
93e26f6c10 Fixed missing error toast when starting a group call/meeting and there's an error after adding participants 2025-11-18 10:23:10 +01:00
Sylvain Berfini
3f22a596db Ignore Telecom Manager endpoints availability/requests, using our own preferred endpoint policy (to workaround device disconnect/reconnect not always notified) 2025-11-17 12:22:54 +01:00
Sylvain Berfini
d5c836b8b5 Should prevent crash that may happen after picking ringtone if settings fragment was destroyed while new ringtone was being picked 2025-11-17 10:06:35 +01:00
Sylvain Berfini
9afcb6db15 Updated AGP to 8.13.1 2025-11-17 09:47:56 +01:00
Sylvain Berfini
a6f568497d Bumped dependencies 2025-11-06 09:04:43 +01:00
Sylvain Berfini
209c0df091 Prevent voice message recording when editing an already sent message 2025-11-06 08:50:39 +01:00
Sylvain Berfini
7b0de4185c Prevent message edit to overlap reply and vice-versa 2025-11-05 14:47:13 +01:00
Sylvain Berfini
89458ed826 Bumped firebase BoM 2025-11-03 10:18:34 +01:00
Sylvain Berfini
a3f86fbac0 Fixed toggling favorite flag on contact not adding/removing it from favorites list 2025-10-31 14:37:00 +01:00
Sylvain Berfini
28cee7f539 Hide LDAP verbose mode toggle setting as it has no effect in SDK 2025-10-31 09:44:20 +01:00
Sylvain Berfini
daa2f10f7b Bumped Kotlin version 2025-10-30 12:39:43 +01:00
Sylvain Berfini
e14ea0ac68 Fixed emoji reaction not visible when long pressing an image on a device with a small screen 2025-10-30 09:31:36 +01:00
Sylvain Berfini
c3ad96cd1f Fixed issue in contact layout 2025-10-27 15:24:18 +01:00
Sylvain Berfini
c6a0f25041 Fixed generated avatars color when switching between light/dark modes 2025-10-27 15:17:18 +01:00
Sylvain Berfini
7ab7136a5b Fixed contacts list cell clipping 2025-10-27 14:16:22 +01:00
Sylvain Berfini
3698e1673e Removed delete contact option for native ones, will be re-imported at next restart anyway 2025-10-27 11:20:37 +01:00
Sylvain Berfini
bdd5c8766b Fixed missing bottom margin 2025-10-27 10:30:26 +01:00
Sylvain Berfini
ce2b794936 Fixed conversation avatar not updated when subject changes 2025-10-22 10:02:28 +02:00
Sylvain Berfini
e267f46fd7 Prevent blinking avatars in list in case they didn't change 2025-10-22 09:29:48 +02:00
Sylvain Berfini
ab6911dd11 Fixed adding/editing CardDAV synchronized contact picture 2025-10-21 16:58:38 +02:00
Sylvain Berfini
b0283043ee Save generated avatar as files in cache for faster re-user and lower memory footprint 2025-10-21 15:38:50 +02:00
Sylvain Berfini
0e71a726c1 Fixed infinite LDAP queries loop in case it returns a result that doesn't match the request 2025-10-20 11:29:48 +02:00
Sylvain Berfini
d74ccb523e Prevent LDAP password to be removed after editing existing config 2025-10-20 10:52:06 +02:00
Sylvain Berfini
4dc1b9a903 Disable camera button while in conference with audio_only layout 2025-10-20 10:00:53 +02:00
Sylvain Berfini
45c756cfd6 Updated CHANGELOG & version code from release/6.0 branch 2025-10-16 11:01:31 +02:00
Sylvain Berfini
069997d780 Fixed displayer screen sharing participant name 2025-10-15 17:03:01 +02:00
Sylvain Berfini
e2c9e1196f Force all LDAP fields to be filled, added verbose mode toggle 2025-10-15 12:12:32 +02:00
Sylvain Berfini
e8c642b9c6 Bumped firebase & gms version 2025-10-15 11:02:38 +02:00
Sylvain Berfini
d75c48cd34 Fixed misleading method name 2025-10-15 09:59:13 +02:00
Sylvain Berfini
d9ab840570 Prevent black screen when trying to scan a QR code in assistant right after granting the app the CAMERA permission (on some devices) 2025-10-14 14:23:41 +02:00
Sylvain Berfini
5ee3ba4ea9 Updated warning about conversations that aren't E2E encrypted 2025-10-13 11:55:33 +02:00
Sylvain Berfini
d694789d4b Trying to troubleshoot missing participant video when changing conference layout sometimes 2025-10-13 11:16:31 +02:00
Sylvain Berfini
b71249ea36 Fixed typos in French translation 2025-10-13 09:40:11 +02:00
Sylvain Berfini
7855d4e1db Updated lock open icon & warning color to increase contrast 2025-10-13 09:38:34 +02:00
Sylvain Berfini
7e2527c46c Fixed wrong label for LDAP form field 2025-10-06 16:19:43 +02:00
Sylvain Berfini
d16dbcf0fd Fixed proximity sensor not turned ON when call is answered from notification 2025-10-06 11:08:54 +02:00
Sylvain Berfini
1d28ce1846 Added logs to help troubleshoot contact matching issue 2025-10-06 10:28:23 +02:00
Sylvain Berfini
2ea38abdfe Reworked proxy/outbound proxy settings in account advanced settings 2025-10-02 16:12:00 +02:00
Sylvain Berfini
416cc6ea7f Only display missing permissions in assistant PermissionsFragment 2025-10-02 15:17:05 +02:00
Sylvain Berfini
6dc4790597 Simplified code using newly added API in SDK 2025-10-02 10:51:23 +02:00
Sylvain Berfini
f8556aa46b Hide suggestions SIP address domain if it matches default account SIP identity one + fixed suggestion avatar for phone numbers 2025-10-01 15:12:14 +02:00
Sylvain Berfini
df09bcad76 Hide SIP address field from contact editor when hide SIP addresses flag is set, and fixed issue where dialog with only 1 item would be displayed 2025-10-01 11:41:40 +02:00
Sylvain Berfini
0ca4eba63b Fixed contacts presence subscribe being only enabled for default domain account, added setting to disable presence 2025-10-01 10:01:48 +02:00
Sylvain Berfini
c556d14fb0 Fixed ConcurrentModificationException that could happen during contact edition 2025-09-29 13:59:50 +02:00
Sylvain Berfini
6cb78c8c59 Bumped dependencies 2025-09-26 11:57:52 +02:00
Sylvain Berfini
61517461dd Use account recovery token FlexiAPI endpoint 2025-09-23 20:32:13 +02:00
Sylvain Berfini
1fdc2bcc58 Fixed no suggestion flag not applied for filter text input in some screens (mostly participant pickers) 2025-09-22 16:28:10 +02:00
Sylvain Berfini
8f3415f6fa Reduce limit of attempts to change audio device in Android framework + return correct value if it failed 2025-09-22 14:58:44 +02:00
Sylvain Berfini
ae7a3c5bce Load contents by chunks instead of loading all of them at once 2025-09-17 16:30:41 +02:00
Sylvain Berfini
31e15ddfca Prevent app crash when trying to open a corrupted PDF sent/received by chat 2025-09-17 13:36:15 +02:00
Sylvain Berfini
808dc92cd7 Added PDF file preview in conversation (message bubble + documents list) 2025-09-17 12:18:17 +02:00
Sylvain Berfini
99936e8f75 Removed font padding on main fragments' titles 2025-09-17 09:22:10 +02:00
Sylvain Berfini
2ce07b5e89 Updated CHANGELOG & version code from release/6.0 branch 2025-09-15 14:22:44 +02:00
Sylvain Berfini
9d3ef9e8a5 Fix for empty fragment still opened after device rotation if user clicked on the empty part 2025-09-15 11:26:30 +02:00
Sylvain Berfini
5f17dd8534 Added menu icon in top bar next to current profile avatar + fixed layout icon while in conference 2025-09-15 09:29:22 +02:00
Sylvain Berfini
719b28f0ab Fixed account labelled as Disabled instead of Disconnected if network isn't reachable 2025-09-12 15:09:39 +02:00
Sylvain Berfini
6f1439756e Improved bodyless friendlist presence received processing 2025-09-11 11:14:31 +02:00
Sylvain Berfini
7fdbaf5fd6 Updated dependencies 2025-09-11 09:14:11 +02:00
Sylvain Berfini
4639e054bb Ask CallActivity to finish if no call found when trying to answer/hangup 2025-09-08 10:55:19 +02:00
Sylvain Berfini
504f6e2a2c Fixed missing conference subject when calling it's SIP URI without having the conference info 2025-09-05 15:16:56 +02:00
Sylvain Berfini
1e6f501dee Fixed mute mic / toggle speaker buttons background changing color when pressing the bottom bar empty space 2025-09-05 13:50:33 +02:00
Sylvain Berfini
633aee829a Updated numpad digits from cirlces to squircles to make them bigger 2025-09-05 12:08:39 +02:00
Sylvain Berfini
38ffac31b4 Added missing floating action button for dialpad on call transfer fragment 2025-09-05 11:59:19 +02:00
Sylvain Berfini
6e40e3f75f Updated AGP to 8.13.0 2025-09-03 13:28:21 +02:00
Sylvain Berfini
7d7900e081 Fixed media encryption label show end-to-end encrypted call while in conference instead of point-to-point 2025-09-03 08:42:02 +02:00
Sylvain Berfini
f0e899bb95 Updated CHANGELOG & version code from release/6.0 branch 2025-09-02 16:53:44 +02:00
Sylvain Berfini
db7ca6793b Added swipe/pull to refresh on contacts list when a CardDAV friend list is configured to force the synchronization 2025-09-02 10:39:37 +02:00
Sylvain Berfini
ac521557ce Bumped AGP & Firebase BoM dependencies 2025-09-01 09:56:05 +02:00
Sylvain Berfini
b5babae39a Updated telecom dependency 2025-08-28 18:29:11 +02:00
Sylvain Berfini
ce13d4c7d4 Reworked how meetings are cancelled/deleted from user perspective 2025-08-27 12:12:36 +02:00
Sylvain Berfini
e9cc03891b Remove update available alert for two dependencies 2025-08-26 15:31:22 +02:00
Sylvain Berfini
98bf3daed8 Use newly added SDK API to merge friend list 2025-08-25 16:59:00 +02:00
Sylvain Berfini
a5cee98a57 Updated CHANGELOG & version code from release/6.0 branch 2025-08-25 10:12:56 +02:00
Sylvain Berfini
26e391cbf8 Fixed contacts not displayed in app after being received through bodyless presence 2025-08-21 10:05:21 +02:00
Sylvain Berfini
0add60c628 Remove legacy shortcuts, follow Android guidelines and create chat room shortcuts when a message is sent or received in it, not at the start 2025-08-19 17:18:00 +02:00
Sylvain Berfini
fc90a95e94 Removed trash icon from retract message placeholder 2025-08-19 12:07:56 +02:00
Sylvain Berfini
c153b2d928 Hide reply/forward actions for retracted message 2025-08-19 11:48:27 +02:00
Sylvain Berfini
881e2c217b Reworked GenericAddressPicker related code, updated CorePreferences getters to be @AnyThread instead of @WorkerThread 2025-08-19 11:29:16 +02:00
Sylvain Berfini
77d744d020 Bumped dependencies 2025-08-19 10:46:43 +02:00
Sylvain Berfini
5a16761fdf Fixed methods called from wrong thread, other code cleanup / improvements 2025-08-19 10:38:54 +02:00
Sylvain Berfini
e0ff593f3d Prevent encrypted.pref file from being backed up, at best it won't work at worst it will prevent VFS from being turned on 2025-08-14 12:35:07 +02:00
Sylvain Berfini
71bb569fde Scroll to latest match during chat message search in case user scrolled away 2025-08-14 10:38:48 +02:00
Sylvain Berfini
9096225b45 Bumped dependencies 2025-08-14 09:41:08 +02:00
Sylvain Berfini
ad0037fe4c Do not show delete for everyone/delete locally choice dialog when removing received message 2025-08-14 09:09:55 +02:00
Sylvain Berfini
7eed9c06d3 Reworked some logs in ContactsManager, do not show ContactNumberOrAddressModel if phone number couldn't be turned into an Address 2025-08-11 14:58:17 +02:00
Sylvain Berfini
e2dabf5448 Updated CHANGELOG & version code from release/6.0 branch 2025-08-11 11:51:57 +02:00
Sylvain Berfini
08c72dbb8c Prevent fatal error due to changes in SDK preventing stopping Core from within iterate() loop, fixed leaving assistant after remote provisioning if no account was configured 2025-08-11 10:11:45 +02:00
Sylvain Berfini
f99b51d572 Use newly added Queued chat message state 2025-08-07 18:07:07 +02:00
Sylvain Berfini
8f0f6581b2 Use participant device display name in conference if device name is empty 2025-08-07 16:41:41 +02:00
Sylvain Berfini
fac6e42c22 Added edit/retract message features 2025-08-06 15:38:45 +00:00
Sylvain Berfini
e8d3c8750a Updated CHANGELOG & version code from release/6.0 branch 2025-08-06 17:21:11 +02:00
Sylvain Berfini
dee932da42 Reset forground service wait to stop flag when first call starts, just in case 2025-08-05 12:07:56 +00:00
Sylvain Berfini
2f4fd3da18 Fixed method called from main thread instead of core's one 2025-08-05 10:45:21 +02:00
Sylvain Berfini
22ae4e372f Fixed generated avatar for SIP URIs without username 2025-08-04 17:12:10 +02:00
Sylvain Berfini
95bd14bdd4 Quick workaround to fix missing favorites from address selection fragment 2025-08-04 14:41:45 +02:00
Sylvain Berfini
ffabd02f31 Fixed microphone not recording audio in background if SIP dialog doesn't reach Updating state 2025-08-04 13:51:00 +02:00
Sylvain Berfini
0bb7761db9 Fix outgoing call in full screen 2025-08-01 10:56:50 +02:00
Sylvain Berfini
50418f5dbb Updated AGP to 8.12.0 2025-08-01 09:37:37 +02:00
Sylvain Berfini
e800249445 Updated CHANGELOG & version code from release/6.0 branch 2025-07-31 23:58:11 +02:00
Sylvain Berfini
ec5b6e5707 Updated dependencies 2025-07-31 13:40:33 +02:00
Sylvain Berfini
97c6c0b553 Updated bell and bell_slash icons 2025-07-30 18:46:10 +02:00
Sylvain Berfini
95ce77e0e4 Prevent crash in message notification if person name is empty 2025-07-28 11:49:36 +02:00
Sylvain Berfini
243a6d8cb2 Hide numpad (if visible) from start call fragment when going back using gesture/click 2025-07-28 11:30:16 +02:00
Sylvain Berfini
e98318a23d Fixed numpad padding causing not centered # 2025-07-28 10:38:12 +02:00
Sylvain Berfini
ace0a3f61e Bumped dependencies 2025-07-26 11:57:59 +02:00
Sylvain Berfini
b3ac16052f Prevent exception causing crash if no Activity is available to create document 2025-07-26 11:45:39 +02:00
Sylvain Berfini
865216d717 Remove font padding when chat message only contains emoji 2025-07-26 10:41:40 +02:00
Sylvain Berfini
670eecf0d6 Fixed duplicated week label if 'no meeting today' is first of the week, improved layout a bit 2025-07-25 17:23:08 +02:00
Sylvain Berfini
8dcb18d059 Try to fix video issue when starting video call and video is declined when answered by remote end 2025-07-25 17:23:00 +02:00
Sylvain Berfini
92672bde0a Prevent favourites contacts from missing due to magic search limit 2025-07-25 17:22:51 +02:00
Sylvain Berfini
ada6f35d92 Fixed muted call if Telecom Manager requested a quick mute/unmute 2025-07-25 17:22:42 +02:00
Sylvain Berfini
332828dc7c Fixed french translation 2025-07-23 13:42:01 +02:00
Sylvain Berfini
595ff96d50 Use newly added chat room compose APIs to notify wether text or voice recording is being composed 2025-07-18 15:20:25 +00:00
Sylvain Berfini
6cdcdec373 Updated CHANGELOG & version code from release/6.0 branch 2025-07-18 15:17:51 +02:00
Sylvain Berfini
3098c3e68e Fixed file viewer opening in call activity's if there is one in PiP 2025-07-17 11:55:06 +02:00
Sylvain Berfini
2c9d627794 Updated dependencies 2025-07-17 07:54:16 +00:00
Sylvain Berfini
70df098ee4 Fixed crashes related to lateinit property used before being initialized found on Crashlytics 2025-07-17 07:43:01 +00:00
Sylvain Berfini
c32bac7b07 Added missing operation in progress dialog during account REGISTER in assistant 2025-07-15 16:32:40 +02:00
Sylvain Berfini
3d41a4d221 Added importantForAccessibility=no to all separator views 2025-07-15 14:32:10 +02:00
Sylvain Berfini
dfa87e4088 Improve accessibility by using labelFor item in XML layouts, fixed & uniformized margins in settings 2025-07-15 13:59:04 +02:00
Sylvain Berfini
d6b43c474b Prevent huge and pixelated emojis in picker when device is connected to an external screen 2025-07-15 10:22:43 +02:00
Sylvain Berfini
9d0f2cafc9 Show system notification when account registration is failed 2025-07-13 10:51:57 +02:00
Sylvain Berfini
98cc173d2e Fixed reactions list in bottom sheet not properly updated if opened when changes happen 2025-07-11 16:03:02 +02:00
Sylvain Berfini
4ae046a166 Updated CHANGELOG & version code from release/6.0 branch 2025-07-11 14:39:34 +02:00
Sylvain Berfini
62180140b7 Updated AGP to 8.11.1 2025-07-11 14:28:45 +02:00
Sylvain Berfini
899129d4bc Fixed CardDAV settings when it's read only 2025-07-10 16:22:10 +02:00
Sylvain Berfini
c4618702ab Fixed in-call top bar notification label when going from two calls to one 2025-07-10 13:47:49 +02:00
Sylvain Berfini
8c7c7b40c3 Show user when magic search didn't returned all available results 2025-07-10 11:25:52 +02:00
Sylvain Berfini
856f3e7f94 Fixed issues in French strings 2025-07-09 13:58:17 +02:00
Sylvain Berfini
f7be887984 Fixed wrong english string for password hint in auth requested dialog 2025-07-08 10:32:14 +02:00
Sylvain Berfini
028ece407c Add a way to go to Help page from assistant 2025-07-07 15:19:15 +02:00
Sylvain Berfini
bb81957aab Improved audio device name in advanced settings 2025-07-04 15:16:22 +02:00
Sylvain Berfini
da581c3737 Added enabled toggle for LDAP plugins allowing to disable it without deleting it + reduced max displayed contacts 2025-07-04 11:04:09 +02:00
Sylvain Berfini
461537aa9c Prevent auth dialog when failed to login in assistant and then successfully login another account 2025-07-04 10:15:50 +02:00
Sylvain Berfini
1a54746a80 Added support for HDMI devices 2025-07-03 17:10:32 +02:00
Sylvain Berfini
be24224f4c Bumped dependencies 2025-07-03 09:47:10 +02:00
Sylvain Berfini
f2cdb92858 Reworked LIME algo settings as you can set multiple and order matters 2025-07-02 16:18:08 +02:00
Sylvain Berfini
aa0255bcfd Use read-only information from FriendList if any to hide edit/delete/mark as favortie buttons in contact details & contacts list long press menu 2025-07-01 09:24:58 +02:00
Sylvain Berfini
ff425089c7 Reworked UI for incoming call screen when screen is locked 2025-07-01 08:49:32 +02:00
Sylvain Berfini
b4c2a52bf7 Added LIME algorithm dropdown in account advanced settings if developer mode is enabled 2025-06-30 17:04:26 +02:00
Sylvain Berfini
f397456879 Updated CHANGELOG & version code to match 6.0.10 release 2025-06-27 10:10:37 +02:00
Sylvain Berfini
5337ab6413 Make sure an account is set in CallParams when initiating a call 2025-06-26 13:06:36 +02:00
Sylvain Berfini
998f969c0f Increased single media max height for small screens in portrait orientation or large screens 2025-06-26 12:04:12 +02:00
Sylvain Berfini
58410ee112 Fixed mentions in chat message if there are more than one 2025-06-26 09:53:59 +02:00
Sylvain Berfini
6c97ee9176 Bumped dependencies 2025-06-25 16:57:52 +02:00
Sylvain Berfini
187946bf34 Fixed chat bubble when reply original message is missing (reply UI is hidden) 2025-06-24 16:45:57 +02:00
Sylvain Berfini
3c40bf3d6f Show event at top of conversation in case of unsecure conversation (like we do for e2e encrypted ones) + icon under conversation title 2025-06-23 11:42:07 +02:00
Sylvain Berfini
b7a9f4ba8e Fixed log + missing elevation for nav bar in landscape 2025-06-23 11:22:00 +02:00
Sylvain Berfini
4a1c5304b1 Fixed issue with encryption label being stuck in 'waiting for encryption' 2025-06-23 09:34:28 +02:00
Sylvain Berfini
67e3c51a84 Code cleanup, bumped dependencies & gradle 2025-06-20 13:54:47 +02:00
Sylvain Berfini
4d8ab32da7 Using a style for top bar icons to prevent duplicating code 2025-06-20 11:30:46 +02:00
Sylvain Berfini
62ff36e7a7 Reworked UI for dialogs buttons 2025-06-19 15:34:15 +02:00
Sylvain Berfini
6f80409086 Trying to prevent BT not being used automatically when it connects during a call + improved AudioUtils logs 2025-06-19 13:24:11 +02:00
Sylvain Berfini
fd3f746e3d Prevent race condition crash with VuMeter when call is ending 2025-06-19 12:03:24 +02:00
Sylvain Berfini
42fbbc51fd Improved NotificationsManager code, added call notification PendingIntent's ActivityOptions bundle, use PendingIntent in CoreContext to start CallActivity 2025-06-19 09:53:36 +02:00
Sylvain Berfini
60c74ee5b2 Fixed logs related to developer settings 2025-06-17 20:27:46 +02:00
Sylvain Berfini
79212a8757 Improved process to enable developer settings 2025-06-17 20:19:21 +02:00
Sylvain Berfini
1307ec5471 Added developer setting to change push notification compatible domains list 2025-06-17 17:38:15 +02:00
Sylvain Berfini
c62f549521 Added vu meter developer setting 2025-06-17 16:50:43 +02:00
Sylvain Berfini
9ba2684f31 Using contextClickListener to open bottom sheets/menus when mouse right button is clicked 2025-06-17 14:23:47 +02:00
Sylvain Berfini
4b631a19ef Fixed extra action visible under clear search field icon + fixed search field hint text color 2025-06-17 11:11:48 +02:00
Sylvain Berfini
496279d724 Added pressed/hover effect to icons that can be clicked 2025-06-17 10:40:34 +02:00
Sylvain Berfini
c25ed404dc Added missing hint to every top bar filter/search field, changed empty fragment background color to match the one on desktop app, removed useless title in call history details 2025-06-16 17:12:49 +02:00
Sylvain Berfini
a81973e4cf Using style to factorize popup menus attributes, added hover effect (same as pressed) & improved some layouts 2025-06-16 15:10:19 +02:00
Sylvain Berfini
654e790a6d Also handle text waiting to be shared in newly added top bar 2025-06-16 13:39:53 +02:00
Sylvain Berfini
6602c7692b Reworked MainActivity's top bar, split alerts & in-call notifications, added file sharing dedicated one 2025-06-16 08:48:56 +00:00
Sylvain Berfini
cc57244b56 Hide SIP address/phone number picker dialog if contact has exactly one SIP address matching the default domain and currently default account domain 2025-06-16 09:36:12 +02:00
Sylvain Berfini
19df3b07dc Increase text size when chat bubble only contains emoji character(s) 2025-06-13 16:22:44 +02:00
Sylvain Berfini
4ef9a2bdf3 Increase number of columns in conversation's media list when screen is large 2025-06-13 15:30:25 +02:00
Sylvain Berfini
61be1d21d5 Improved UI on large tablets 2025-06-13 15:00:55 +02:00
Sylvain Berfini
8148354901 Added enable/disable speaker to active call notification 2025-06-13 11:25:28 +02:00
Sylvain Berfini
ae39d79420 Various improvements 2025-06-13 09:20:01 +02:00
Sylvain Berfini
2bd0de4af1 Fixed group conversation creation if LIME server URL not set 2025-06-10 12:07:45 +02:00
Sylvain Berfini
cb27b35984 Updated changelog & version code/name from release/6.0 branch 2025-06-06 15:47:32 +02:00
Sylvain Berfini
316bc6698a Added user guide link in Help section, factorized code for URL opening, added missing ScrollView to help & debug layouts 2025-06-06 11:53:22 +02:00
Sylvain Berfini
fea1dbe5ca Display last message timestamp in conversations list instead of chat room last updated timestamp 2025-06-05 10:28:58 +02:00
Sylvain Berfini
85679dcc43 Added vu meter for playback volume + setting to enable vu meters (disabled by default) 2025-06-05 09:51:21 +02:00
Sylvain Berfini
32060f6830 Bumped dependencies 2025-06-05 09:39:23 +02:00
Sylvain Berfini
dfdc26a575 Prevent crash for call notification due to empty person name, using all identification fields from vCard (first & last names, organization, job title) 2025-06-02 14:08:56 +02:00
Sylvain Berfini
9e1d358f4e Check if outgoing early-media call is really doing video instead of assuming it is 2025-06-02 11:01:30 +02:00
Sylvain Berfini
90922568b5 Prevent port from being set in third party account SIP identity + update existing accounts to remove port from identity 2025-06-02 10:54:32 +02:00
Sylvain Berfini
d212b7b06e Added setting to hide contacts without SIP address nor phone number 2025-05-30 10:09:56 +02:00
Sylvain Berfini
4cb83980ba Bumped AGP 2025-05-30 09:42:33 +02:00
Sylvain Berfini
def52f69ad Only refresh conversation list cell when a message is deleted, prevents blinking 2025-05-29 11:40:42 +02:00
Sylvain Berfini
17ce34aba7 Updated changelog & version code/name from release/6.0 branch 2025-05-23 15:04:55 +02:00
Sylvain Berfini
1833b1985d Fixed sent files size missing 2025-05-22 14:45:31 +02:00
Sylvain Berfini
5256ee79c6 Use gray background for file preview in attachment area to increase contrast, improved remove file from attachments icon size and position 2025-05-22 10:49:13 +02:00
Sylvain Berfini
27e59a5f8b Using utils method to check whether call has video enabled or not 2025-05-21 13:41:24 +00:00
Sylvain Berfini
cea2d49778 Trying to workaround hearing aids issue in Telecom Manager 2025-05-21 15:19:42 +02:00
Sylvain Berfini
c0f67d01fe Prevent crash in MediaViewerFragment if media player wasn't initialized 2025-05-21 10:11:18 +02:00
Sylvain Berfini
b6279b03c0 Make sure that files grid in chat bubble is using at most 3 columns 2025-05-20 16:33:22 +02:00
Sylvain Berfini
21398c7b37 Added sliding button to answer/decline incoming call if device screen is locked 2025-05-20 15:53:21 +02:00
Sylvain Berfini
4cb7ea1965 Showing files in square area like media when more than one in a single chat message 2025-05-20 10:45:27 +02:00
Sylvain Berfini
7aae03f1f9 Fixed missing margin in media grid for audio files 2025-05-19 13:37:45 +02:00
Sylvain Berfini
25d13f44c7 Prevent 1-1 events for conference joined/left + temporary read only state 2025-05-16 15:51:28 +02:00
Sylvain Berfini
81d0da4241 Updated coil dependency 2025-05-16 12:14:37 +02:00
Sylvain Berfini
1556abc79e Fixed logs sharing server URL setting 2025-05-16 11:19:45 +02:00
Sylvain Berfini
28b6bd7e90 Updated changelog & version code/name from release/6.0 branch 2025-05-16 11:13:58 +02:00
Sylvain Berfini
1c3173b871 Moved call related advanced parameters into dedicated fragment 2025-05-16 09:51:32 +02:00
Sylvain Berfini
17588de5a9 Do not delete chat rooms when removing account, will cause leaving groups in case of multi device 2025-05-16 09:40:08 +02:00
Sylvain Berfini
502c6413ee Reworked bottom nav bar (in portrait) 2025-05-16 07:37:06 +00:00
Sylvain Berfini
02cbb45de9 Trying to prevent another race condition in notifications manager leading to foreground service not being started before being stopped 2025-05-16 09:11:38 +02:00
Sylvain Berfini
f5852a7b3e Prevent bottom nav bar titles from being cropped when font size is increased 2025-05-14 08:39:48 +02:00
Sylvain Berfini
cfec621787 Fixed group chat event icons 2025-05-13 16:35:51 +02:00
Sylvain Berfini
6847227f1a Reworked click on SIP URI in chat message to prevent long press on it from starting the call 2025-05-13 14:02:54 +02:00
Sylvain Berfini
f1fdb186ec Reworked unread count indicators 2025-05-13 13:48:15 +02:00
Sylvain Berfini
d822cbc827 Make sure after a remote provisioning a default account has been set 2025-05-13 10:15:42 +02:00
Sylvain Berfini
627f881364 Make sure speaker audio device is used if available when incoming early media call is ringing 2025-05-13 10:09:50 +02:00
Sylvain Berfini
244061c0b1 Fixed black thumbnails when joining conference without bundle mode 2025-05-12 16:23:51 +02:00
Sylvain Berfini
dc2b94ca4d Fixed broken link in README 2025-05-12 13:23:05 +02:00
Sylvain Berfini
7c78b021db Removed some debug logs, improved findContactByAddress performances a bit 2025-05-12 10:29:05 +02:00
Sylvain Berfini
d113797dfb Added auto answer with video in both directions advanced call setting 2025-05-12 09:56:38 +02:00
Sylvain Berfini
926b8d4dc1 Updated dependencies 2025-05-09 11:09:20 +02:00
Sylvain Berfini
85e24e25bf Added missing toast events observer 2025-05-07 16:12:34 +02:00
Sylvain Berfini
1c1729f3f0 Fixed meeting list yesterday item still displayed as today if list isn't reloaded in two days 2025-05-07 11:13:51 +02:00
Sylvain Berfini
bcce9a9ba1 Updated AGP to 8.10 2025-05-07 11:08:00 +02:00
Sylvain Berfini
a496e2bf56 Forgot to disable IMDN bottom sheet for incoming messages in groups 2025-05-06 14:33:19 +02:00
Sylvain Berfini
73237ee335 Added toast for meeting update error + fixed other toasts 2025-05-06 12:53:28 +02:00
Sylvain Berfini
b293bf7f2f Added back Weblate section to README 2025-05-05 15:42:36 +02:00
Sylvain Berfini
4689b7c7da Fixed app reloading lists too many times at startup when looking for friends in remote contact directories such as LDAP/CardDAV 2025-05-05 14:50:22 +02:00
Sylvain Berfini
e38040428b Improved empty lists (contacts & conversations) labels + added button to let user know it can change account 2025-05-05 13:52:28 +02:00
Sylvain Berfini
b740409642 Update conversations list after clearing conversation history 2025-05-05 12:01:23 +02:00
Sylvain Berfini
99a5ed23f6 Updated CHANGELOG & version code from release/6.0 branch 2025-05-02 14:37:23 +02:00
Sylvain Berfini
c9a3a01733 Improved empty SIP contacts list 2025-04-30 15:40:49 +02:00
Sylvain Berfini
966f713f19 Increased margin for clean/share logs button in troubleshooting fragment 2025-04-29 17:15:04 +02:00
Sylvain Berfini
344afdfcfa Moved print logs in logcat & file sharing URL settings to developper section, added logs upload file sharing server URL setting, added setting to disable crashlytics logs collection 2025-04-29 16:54:20 +02:00
Sylvain Berfini
2634945b8d Improved chat room lookup while in conference 2025-04-29 14:18:52 +02:00
Sylvain Berfini
2713c82ca3 Added content description french translation 2025-04-29 09:02:36 +00:00
Sylvain Berfini
1a813ee11e Prevent crash due to uncaught exception 2025-04-29 09:33:38 +02:00
Sylvain Berfini
e5cec2d45c Another attempt at fixing crashes related to in-call service never truly started as foreground before being stopped 2025-04-28 16:06:09 +02:00
Sylvain Berfini
6e9c6d1b33 Fixed group chat events icon 2025-04-28 11:26:35 +02:00
Sylvain Berfini
056abd629f Fixed newly created contact not appearing in contacts list 2025-04-24 10:10:15 +02:00
Sylvain Berfini
6c86af747b Improved VFS confirmation dialog message 2025-04-24 10:04:10 +02:00
Sylvain Berfini
f7790fbed7 Bumped AGP version & splashscreen dependency 2025-04-24 09:47:10 +02:00
Sylvain Berfini
90524da610 Fixed chat room lookup while in call 2025-04-23 10:18:25 +02:00
Sylvain Berfini
616b7bb70f Prevent crash & show error toast when trying to open a password protected PDF 2025-04-23 09:06:51 +02:00
Sylvain Berfini
985a304df9 Updated CHANGELOG & version code from release/6.0 branch 2025-04-18 11:06:19 +02:00
Sylvain Berfini
cd35f213c1 Fixed crash due to missing foreground service if OS denies call notification until foreground Service was started + fixed crash if call is ended before CoreInCallService was started and foreground service notification sent 2025-04-18 09:53:57 +02:00
Sylvain Berfini
dcbc837106 Apply workaround when making a call to a SIP URI having a phone number as username & IP as domain 2025-04-17 14:30:23 +02:00
Sylvain Berfini
5ef7eab0c5 Added microphone volume vu meter 2025-04-17 13:07:15 +02:00
Sylvain Berfini
c64bd5bc1c Fixed numpad dial button while transfering a call 2025-04-17 09:45:53 +02:00
Sylvain Berfini
afa041baf6 Hide account creation form when device doesn't support push notifications 2025-04-17 09:45:02 +02:00
Sylvain Berfini
94b6db6a08 Fixed build with latest SDK 2025-04-17 09:40:52 +02:00
Sylvain Berfini
6ba8760be7 Improved called account display 2025-04-15 11:41:21 +02:00
Sylvain Berfini
b1b1ab0d8a Updated CHANGELOG & version code from release/6.0 branch 2025-04-11 10:28:59 +02:00
Sylvain Berfini
518ecc1823 Added a list of domain for which to show push notification settings 2025-04-11 09:38:55 +02:00
Sylvain Berfini
e2dfd95857 Prevent crash in HelpViewModel if app is built without Firebase 2025-04-10 14:12:39 +02:00
Sylvain Berfini
51d725c757 Quick code cleanup 2025-04-10 13:24:10 +02:00
Sylvain Berfini
af3b1fa418 Bumped dependencies 2025-04-10 10:04:30 +02:00
Sylvain Berfini
bc9a6581b1 Added logs to call transfer (blind & attended) 2025-04-10 09:45:03 +02:00
Sylvain Berfini
26df085df3 Hide push notification setting in third party SIP accounts parameters, they won't work anyway + disable push for existing third party SIP accounts when migrating to 6.0.4 2025-04-09 10:28:23 +02:00
Sylvain Berfini
c08157b659 Removed code no longer needed, done by SDK now + prevent onContactsLoaded() callback to be triggered too many times when fetching multiple addresses from remote contacts directories 2025-04-07 09:54:14 +02:00
Sylvain Berfini
910527ef1b Updated CHANGELOG & version code from release/6.0 branch 2025-04-04 12:59:34 +02:00
Sylvain Berfini
8577571e67 Show operation in progress during contact search 2025-04-04 10:07:32 +02:00
Sylvain Berfini
dbca62bea9 Added hidden developer settings 2025-04-03 17:31:14 +02:00
Sylvain Berfini
5e9be7d10b Show alert when default account is disabled 2025-04-03 14:52:16 +02:00
Sylvain Berfini
836deaae99 Fixed no default account issue when removing currently default one 2025-04-03 13:06:49 +02:00
Sylvain Berfini
a2680028ce Keep attach file icon when keyboard is opened in chat instead of emoji picker 2025-04-03 11:36:50 +02:00
Sylvain Berfini
c8ff7262d4 Follow contacts list filter for every contact/address picker 2025-04-03 10:47:54 +02:00
Sylvain Berfini
06d8e903fc Revert "Trying to prevent bottom bar from disappearing sometimes", trying better fix instead
This reverts commit 317a7c4417.
2025-04-03 09:53:53 +02:00
Sylvain Berfini
a5872ef8de Set default values for notification channels, do not rely only on importance level 2025-04-02 15:15:09 +02:00
Sylvain Berfini
9255830fe2 Show copy SIP URI icon & do it on click in call history like in conversation details 2025-04-02 14:20:13 +02:00
Sylvain Berfini
80eaf08fbf Refresh lists content when going back from background after at least 1 hour (when keep alive service is enabled) 2025-04-01 13:25:16 +02:00
Sylvain Berfini
903aaad6fe Do not store friends map in ContactsLoader, might cause concurrent modification 2025-03-31 16:20:37 +02:00
Sylvain Berfini
bdb2615300 Targetting Android 16 Baklava (API level 36) 2025-03-31 14:15:26 +02:00
Sylvain Berfini
bab2acb75c Prevent meetings list display issue if source isn't sorted 2025-03-31 13:31:19 +02:00
Sylvain Berfini
bd52960749 Fixed behavior when video is disabled in settings, should not show incoming video calls as video nor route audio to speaker automatically 2025-03-31 12:07:06 +02:00
Sylvain Berfini
23810e41e5 Forgot to change some of POST_NOTIFICATIONS checks 2025-03-31 09:26:22 +02:00
Sylvain Berfini
3f3a229844 Updated Github issue template with 6.0 way of sharing logs 2025-03-28 20:05:43 +01:00
Sylvain Berfini
0eb659b633 Updated CHANGELOG from release/6.0 branch 2025-03-28 10:25:14 +01:00
Sylvain Berfini
c35a44b1a0 Fixed migration scenario where logs upload sharing server url might not be set 2025-03-28 10:22:24 +01:00
Sylvain Berfini
1cccf7d26b Apply same call history workaround for missed calls count 2025-03-28 10:22:18 +01:00
Sylvain Berfini
0fab732e89 Fixed LDAP/remote CardDAV results not always displayed when making a search in contacts list 2025-03-27 16:17:49 +00:00
Sylvain Berfini
317a7c4417 Trying to prevent bottom bar from disappearing sometimes 2025-03-27 15:28:57 +01:00
Sylvain Berfini
689665c475 Show floating action button to open numpad in outgoing early media call, prevent display name & SIP address being displayed twice if early media is audio only 2025-03-27 11:32:11 +01:00
Sylvain Berfini
18e15b60a4 Fixed toggle setting color when disabled & off, disable early media ringing toggle if early media ringing is disabled 2025-03-27 10:14:25 +01:00
Sylvain Berfini
7bead679ad Delete all data related to account being removed 2025-03-27 09:40:07 +01:00
Sylvain Berfini
f0ad67fb29 Fixed disabled handle color while outgoing call is ringing 2025-03-27 09:10:22 +01:00
Sylvain Berfini
2621eb306e Added content message to keep app alive foreground service notification 2025-03-26 16:21:55 +01:00
Sylvain Berfini
90bf20e50e Prevent meeting icons from being briefly visible when selected account has no videoconference factory URI set in it's params 2025-03-26 09:48:03 +01:00
Sylvain Berfini
1f45ba8bd0 Added back ring during early media setting 2025-03-26 09:19:37 +01:00
Sylvain Berfini
c528f0cdb8 Bumped dependencies 2025-03-25 17:14:06 +01:00
Sylvain Berfini
a0108776dd Keep newly created account disabled until SMS code validation is done 2025-03-25 16:04:36 +00:00
Peio Rigaux
a503ef06ee Now use docker to deploy. Allows multiple deploy at the same time 2025-03-25 15:55:13 +00:00
Sylvain Berfini
fbc19c7053 Fixes regarding contacts list filter when switching account 2025-03-25 16:45:56 +01:00
Sylvain Berfini
9c8c5f309e Added hidden setting to allow hiding SIP URIs, show device name instead of SIP full SIP URI when doing trust call from contact details 2025-03-25 16:28:00 +01:00
Sylvain Berfini
b40fbcad77 Log TelecomManager CallControl failed operations 2025-03-25 13:27:24 +00:00
Sylvain Berfini
8dda38a925 Parse friends in a coroutine scope, no need to do it on the Core's thread 2025-03-25 13:41:47 +01:00
Sylvain Berfini
d150027c24 Delay heavy tasks to prevent ServiceDidNotStartInTimeException (for example) 2025-03-25 13:16:18 +01:00
Sylvain Berfini
7018cd3442 Fixed crash when default device URI is null 2025-03-24 23:43:46 +01:00
Sylvain Berfini
d6494cd27c Ask for full screen intent if not granted 2025-03-24 17:33:52 +01:00
Sylvain Berfini
10f2d7cd78 Prevent today indicator in meetings list from blinking upon refresh 2025-03-24 14:03:43 +01:00
Sylvain Berfini
6767bc09f9 Fixed displayed SIP URI in call history details in case we find a matching contact that has a different SIP URI in addition to the one used for the call 2025-03-24 13:36:22 +01:00
Sylvain Berfini
b22ab7024e Fixed contact lookup if phone number starts by 00 instead of + 2025-03-24 13:34:10 +01:00
Sylvain Berfini
c6fa645f94 Fixed invisible conference notification icon 2025-03-24 11:39:03 +01:00
Sylvain Berfini
fb3feb0bc3 This should prevent crashes on Androids < 13 due to broken POST_NOTIFICATIONS permission check 2025-03-24 11:33:00 +01:00
Sylvain Berfini
77f61c1cfa Updated version code to match the one on release/6.0 branch 2025-03-24 10:17:31 +01:00
Sylvain Berfini
9ce803667b Do not send meeting message invitation when chat is disabled 2025-03-24 09:53:11 +01:00
Sylvain Berfini
50bd8f67d5 Check if lateinit conference was initialized 2025-03-24 09:44:47 +01:00
Sylvain Berfini
faac4111d9 Updated CHANGELOG from release/6.0 branch 2025-03-24 08:58:28 +01:00
Sylvain Berfini
6121cb41bf Fixed remove all call logs when workaround is active 2025-03-24 08:58:15 +01:00
Sylvain Berfini
2aed404167 Have automaticallyShowDialpad setting also work on new call/transfer call screens while already in call 2025-03-24 08:58:05 +01:00
Sylvain Berfini
2f9eb2f0ab Improved message when WiFi only mode is enabled & active network isn't Wifi nor Ethernet 2025-03-21 11:13:01 +01:00
Sylvain Berfini
1255d626af Fixed recordings order, now most recent on the top 2025-03-21 09:03:26 +01:00
Sylvain Berfini
a83f9d4424 Moved call related advanced settings to dedicated sub-section, added back auto answer 2025-03-21 08:54:17 +01:00
Sylvain Berfini
fecf067b50 Fixed wrong setting disabled when VFS is enabled 2025-03-21 08:41:24 +01:00
Sylvain Berfini
b194272f91 Use newly added chatRoom.getAccount() to dynamically filter conversations 2025-03-20 14:59:32 +01:00
Sylvain Berfini
cad90752db Fixed call logs details if it's not attached to any account 2025-03-20 14:55:49 +01:00
Sylvain Berfini
2abad0ab9a Fetch call history from Core instead of Account if only one of them 2025-03-20 13:00:10 +01:00
Sylvain Berfini
a0d74c8036 Added back apply prefix to phone numbers for calls & chat setting 2025-03-20 12:29:27 +01:00
Sylvain Berfini
2eb376fd2d Prevent schedule meeting button being pressed multiple times 2025-03-19 13:58:12 +01:00
Sylvain Berfini
1942ee8f85 Few tweaks trying to prevent jni global ref table overflow 2025-03-18 16:31:38 +00:00
Sylvain Berfini
488a0fd98c Added advanced setting letting user choose whether to record in MKV or SMFF 2025-03-18 16:46:12 +01:00
Sylvain Berfini
08412ef99a Prevent some crashes seen on Play Store console 2025-03-18 12:51:25 +01:00
Sylvain Berfini
e16e767d5a Fixed wrong deleted notification channel ID... 2025-03-18 12:34:22 +01:00
Sylvain Berfini
886be9e038 Fixed hearing aid icon not showing in bottom actions when selected + updated earpiece icon in device list to match 2025-03-18 11:58:02 +01:00
Sylvain Berfini
2a5b5d368c Revert using notification channel to play incoming calls ringtone 2025-03-18 10:50:53 +01:00
Sylvain Berfini
be5428aa08 Add generic exception handle for starting action_view activity for URL 2025-03-18 10:13:17 +01:00
Sylvain Berfini
d6c6de2b5e Wait for foreground service to be started before being stopped to try preventing ForegroundServiceDidNotStartInTimeException/RemoteServiceException due to Context.startForegroundService() did not then call Service.startForeground() 2025-03-17 17:37:43 +01:00
Sylvain Berfini
915a847083 Prevent crash due to service being started as foreground if post_notifications permission isn't granted 2025-03-17 17:37:43 +01:00
Sylvain Berfini
0d8397b914 Should fix hearing aids issue 2025-03-17 16:20:34 +00:00
Sylvain Berfini
b5a1e21f40 Should fix quit button visibility in drawer menu 2025-03-17 16:10:12 +01:00
Sylvain Berfini
9837a834d4 Added back "replace + by 00 when formatting phone numbers" account setting 2025-03-17 14:38:01 +01:00
Sylvain Berfini
8a4956e7c1 Hidden save/export buttons for call recordings until export feature will be added to SDK 2025-03-17 14:21:54 +01:00
Sylvain Berfini
052d7cc522 Added UI setting to have dialpad automatically opened when starting new call 2025-03-17 14:19:14 +01:00
Sylvain Berfini
6c6fb9eff3 Prevent call transfer if state is Ended, Error or Released 2025-03-17 13:10:38 +01:00
Sylvain Berfini
b23f52adec Prevent system call notification to be stuck if call was ended in Linphone SDK before being added to TelecomManager's 2025-03-17 11:58:02 +01:00
Sylvain Berfini
8769a47ed0 Adding back auto start setting 2025-03-17 11:47:29 +01:00
Sylvain Berfini
ebb7201701 Prevent crash if contacts listener triggers before chatRoom property is initialized 2025-03-17 09:23:44 +00:00
Sylvain Berfini
6e83b794b3 Prevent crash if not on contact fragment before navigating to editor 2025-03-17 09:14:33 +00:00
Sylvain Berfini
3045378eb0 Prevent crash if fails to go to outside activity because it doesn't exists or it isn't found 2025-03-17 09:14:33 +00:00
Sylvain Berfini
dc4619a7d7 Prevent use of recording ViewModel property not initialized yet 2025-03-17 09:06:15 +00:00
Sylvain Berfini
87b6c2deef Prevent crash clinking on link if no browser is installed on device 2025-03-17 08:44:33 +00:00
Sylvain Berfini
614ac7f9cf Prevent crash if DMTF setting doesn't exists (Samsung A51) 2025-03-16 20:34:29 +01:00
Sylvain Berfini
71e1734ca0 Fixed crash due to currentCall not being initialized 2025-03-13 15:28:11 +01:00
Sylvain Berfini
71b1cf8e7a Make sure Qr Code fragment doesn't use Static Picture camera device 2025-03-13 12:17:19 +01:00
Sylvain Berfini
dee684b364 Added setting to choose whether to sort contacts by first or last name 2025-03-13 11:54:46 +01:00
Sylvain Berfini
0b6805a73c Fixed color selector used when not needed, may cause crash on old devices 2025-03-13 10:10:47 +01:00
Sylvain Berfini
11795cded8 Prevent SecurityException when accessing bluetooth_name on some devices 2025-03-13 09:58:00 +01:00
Sylvain Berfini
cc5bfcf14d Use Account's onConferenceInformationUpdated callback to refresh meetings list 2025-03-13 09:45:19 +01:00
Sylvain Berfini
b3ab9601b2 Prevent user from connecting the same account multiple times 2025-03-12 13:32:11 +01:00
Sylvain Berfini
0e6d91a467 Fixed selecting participant in group conversation when typing '@' 2025-03-12 13:05:01 +01:00
Sylvain Berfini
fbf68db2dd Updated linphone version to use 5.5.0-alpha 2025-03-11 16:28:51 +01:00
Sylvain Berfini
0bf50f1495 Fixed wrong constraint layout reference 2025-03-11 16:23:32 +01:00
Sylvain Berfini
8363d41441 Merge branch 'release/6.0' 2025-03-11 16:01:38 +01:00
Sylvain Berfini
c1e20af56d 6.0.0 release, updated CHANGELOG 2025-03-11 13:28:07 +00:00
Sylvain Berfini
5d14d8bb7b Fixed SSO login from QRcode 2025-03-11 14:21:29 +01:00
Sylvain Berfini
a99067f701 Updated SSO ViewModel to allow for client ID to be changed through remote provisioning or to use client_id from URL query parameters 2025-03-11 10:56:18 +01:00
Sylvain Berfini
2ceebdcdda Use same max width for spinners than for text fields in settings, increased that value a bit for tablets 2025-03-11 10:22:24 +01:00
Sylvain Berfini
dca10c9b2a Keep SRTP as default media encryption for now, update active call notification channel importance from default to low 2025-03-11 10:12:39 +01:00
Sylvain Berfini
82e341ea8c Moved RLS URI out of linphonerc_factory 2025-03-10 17:45:03 +01:00
Sylvain Berfini
508f1154f5 Fixed broken scroll listener in conversation after the fragment is paused/resumed 2025-03-10 16:19:53 +01:00
Sylvain Berfini
07cb09128e Show participant SIP URI in reactions / delivery status / conversation participants lists when clicking on name 2025-03-10 16:06:46 +01:00
Sylvain Berfini
d6d705a975 Code cleanup 2025-03-10 11:47:30 +01:00
Sylvain Berfini
c709c720e4 Prevent being stuck while media list is processed 2025-03-10 10:44:05 +01:00
Sylvain Berfini
96a07fa8c6 Replaced Uri.parse() by .toUri() string extension 2025-03-05 15:17:29 +01:00
Sylvain Berfini
141d7b08a6 Improved answered call log 2025-03-05 15:08:09 +01:00
Sylvain Berfini
3c21044cf0 Renamed Android Auto related classes 2025-03-05 13:53:10 +01:00
Sylvain Berfini
e0e7032827 Updated AGP to 8.9.0 and gradle to 8.11.1 2025-03-05 10:46:29 +01:00
Sylvain Berfini
4cca59a39f Use newly added APIs to get real information about a content being related to an ephemeral chat message or not, and use that information to hide save/export buttons & prevent screenshots 2025-03-05 00:42:11 +00:00
Sylvain Berfini
4e852601fc Use new core.createChatRoom() that replaces the older version (API change was introduced in SDK release/5.4 branch commit ID dd4b83553fc72ca5449adb6ce72d9882f90ae320) 2025-03-04 16:48:25 +01:00
Sylvain Berfini
30364c48b0 Clear auth info password requested dialog when account is removed 2025-03-04 13:15:23 +00:00
Sylvain Berfini
354f39d76d Fixed media list fragment when VFS is enabled, the first time it was opened it wasn't possible to scroll to other files 2025-03-04 09:45:43 +01:00
Sylvain Berfini
64a2c5f455 Show green toast when VFS is successfully enabled 2025-03-04 09:01:27 +01:00
Sylvain Berfini
9ffe3b4d7f Hide incoming messages delivery status in groups if IMDN threshold is set to 1 2025-03-03 14:53:40 +01:00
Sylvain Berfini
fb323a4606 Do not play sound for currently displayed conversation if message is outgoing or read 2025-03-03 14:39:40 +01:00
Sylvain Berfini
7d6c50cf29 Renamed some layouts 2025-03-03 14:24:19 +01:00
Sylvain Berfini
8b9ceef6da Updated firebase BoM 2025-03-03 11:46:54 +01:00
Sylvain Berfini
12e6de52a8 Fixed inserting emoji/digit at the current cursor position instead of at the end 2025-03-03 10:26:17 +01:00
Sylvain Berfini
990549eb24 Prevent going back to operation in progress dialog after hanging up started group call, also go back to calls history 2025-02-27 14:48:28 +01:00
Sylvain Berfini
4afa2ebc93 Updated dependencies 2025-02-27 10:21:30 +01:00
Sylvain Berfini
e05d4cf94a Prevent crash when sharing file from native gallery due to chatRoom not being initialized yet (but it is not needed) 2025-02-27 09:58:54 +01:00
Sylvain Berfini
01c079440d Do not follow telecom manager audio routing requests if not connected to Android Auto and concerns Speaker or Earpiece 2025-02-26 11:21:14 +01:00
Sylvain Berfini
8eda500dae Prevent login issue if SIP identity is given instead of username in third party account login 2025-02-25 20:49:43 +01:00
Sylvain Berfini
a392cf3a6b Prevent crash first time app is installed due to chat message notification sound not installed yet 2025-02-25 19:27:16 +01:00
Sylvain Berfini
977fb63693 Add domain to third party SIP account auth info 2025-02-25 14:06:53 +01:00
Sylvain Berfini
fcd365ad81 Play incoming chat sound file when a message is being received in currently opened conversation 2025-02-25 11:29:23 +01:00
Sylvain Berfini
f1b23337e0 Prevent connexion in progress when going back to waiting room 2025-02-25 09:27:17 +01:00
Sylvain Berfini
296a324ba3 Prevent call in state IDLE to be added to TelecomManager, wait for IncomingReceived or OutgoingProgress 2025-02-24 12:16:05 +01:00
Sylvain Berfini
7e2a4c124d Improved UI of assitant & welcome pages, moving mountains at the bottom like on desktop 2025-02-19 15:25:28 +01:00
Sylvain Berfini
b8a6177f97 Fixed password text fields not using the right font when in visible mode & for hint text 2025-02-19 14:15:26 +01:00
Sylvain Berfini
f3991fb29d Small improvements for 7 inches screen in portrait 2025-02-19 13:51:27 +01:00
Sylvain Berfini
0bf6d60570 Prevent crash if MediaPlayer can't be instancianted 2025-02-18 17:00:52 +01:00
Sylvain Berfini
ef79475525 Fixed resend menu not visible when message goes into error state unless conversation is left & opened again 2025-02-18 16:34:27 +01:00
Sylvain Berfini
ec9c7bd070 Bumped AGP to 8.8.1 2025-02-18 12:02:57 +01:00
Sylvain Berfini
875198164d Added back auto export media to native gallery feature 2025-02-18 11:47:33 +01:00
Sylvain Berfini
17511a4c26 Refactored app to use chatRoom.identifier from SDK instead of computing it from local & peer address 2025-02-17 15:23:10 +00:00
Sylvain Berfini
ca08ed68be Updated coil to 3.1.0 2025-02-17 09:27:09 +01:00
Sylvain Berfini
e292b0d7e8 Fixed some scenario where account(s) reload isn't done 2025-02-13 14:17:28 +01:00
Sylvain Berfini
8c1889b181 Fixed duplicated listeners when doing remove provisioning 2025-02-13 11:59:59 +01:00
Sylvain Berfini
037cd71814 Improved logs 2025-02-13 11:46:43 +01:00
Sylvain Berfini
40e9dfc522 Fixed small UI issue if SIP URI is too long 2025-02-13 11:33:44 +01:00
Sylvain Berfini
7e0c6a23a9 Added hourglass icon for chat message in delivery pending state 2025-02-13 11:29:26 +01:00
Sylvain Berfini
7ccd42580d Handle remote provisioning failure from QR Code assistant 2025-02-13 11:01:00 +01:00
Sylvain Berfini
a238ae8db0 Bumped dependencies 2025-02-13 10:19:40 +01:00
Sylvain Berfini
a210ea67c1 Prevent choose address of phone number dialog not being dismissed once a user made a choice 2025-02-13 09:22:06 +01:00
Sylvain Berfini
dd454113e8 Prevent display issue if account has an empty display name 2025-02-12 10:13:23 +01:00
Sylvain Berfini
690140c2b8 Prevent attach file icon hidden when creating conversation using keyboard 2025-02-12 09:58:23 +01:00
Sylvain Berfini
ff323cea68 Also prevent issue if device has no speaker 2025-02-12 09:44:49 +01:00
Sylvain Berfini
fede808df1 Fixed bluetooth audio device switch while on tablet 2025-02-10 12:03:29 +01:00
Sylvain Berfini
ba5786fa0a Prevent sending multipart SIP message in basic chat rooms 2025-02-10 10:55:18 +01:00
Sylvain Berfini
22b447a67f Using supported tags for third party accounts 2025-02-10 10:23:33 +01:00
Sylvain Berfini
ea79e9243d Changed account login wording to ask for username instead of SIP URI 2025-02-10 09:35:30 +01:00
Sylvain Berfini
709f7dd3c5 Fixed missing chat room if you created one and sent a message in it 2025-02-07 17:12:18 +01:00
Sylvain Berfini
9381b459a0 Removed participant joining label in active speaker miniature (no room for it) 2025-02-07 16:31:07 +01:00
Sylvain Berfini
7bf9eb8394 Prevent starting a group call while already in call 2025-02-07 15:53:32 +01:00
Sylvain Berfini
30fc60c0ef Show video preview in calls list & conference participants list fragments 2025-02-07 15:34:10 +01:00
Sylvain Berfini
3e5a0c22f8 Forgot to do the same for permissions layout 2025-02-07 14:16:44 +01:00
Sylvain Berfini
2b75ecdaca Reworked assistant screens for tablets 2025-02-07 13:50:26 +01:00
Sylvain Berfini
6afd711539 Reworked welcome screens 2025-02-07 10:58:36 +01:00
Sylvain Berfini
bfc435c350 Ask user confirmation for cancelling meeting if it us the organizer, to delete it if it's only a participant 2025-02-07 09:29:52 +00:00
Sylvain Berfini
0f59e1a381 Prevent crash if for some reason today is not found 2025-02-06 17:16:24 +01:00
Sylvain Berfini
dd7546484b Fixed in-call bottom sheets not intercepting clicks 2025-02-06 09:44:45 +01:00
Sylvain Berfini
86b35354c3 Increased dialpad touch area 2025-02-06 09:34:57 +01:00
Sylvain Berfini
3e91f3e5ff Added advanced setting to choose between point to point and end to end encryption when creating a meeting or a group call 2025-02-04 15:43:57 +01:00
Sylvain Berfini
74394445c9 Improved toast API 2025-02-04 13:53:49 +01:00
Sylvain Berfini
536667cfe1 Prevent attach file button in conversation to be disabled after sending max number of attachments 2025-02-04 13:14:33 +01:00
Sylvain Berfini
b057163b43 Removed useless back button 2025-02-03 17:19:18 +01:00
Sylvain Berfini
63051ae58e Show who terminated a call in fragment title 2025-02-03 16:55:01 +01:00
Sylvain Berfini
00a7d34509 Fixed call history list filter not using resolved contact name 2025-02-03 16:33:11 +01:00
Sylvain Berfini
da385ee6e1 Added short toast when user starts recording a call 2025-02-03 16:06:15 +01:00
Sylvain Berfini
2952b2db01 Updated CHANGELOG 2025-02-03 14:13:34 +01:00
Sylvain Berfini
2ed4f44e50 Fixed group call missed notification & in-call alert titles 2025-02-03 14:01:30 +01:00
Sylvain Berfini
1c09388266 Disable username/password fields when account has been created (until SMS is sent), fixed default values not loaded 2025-02-03 13:41:54 +01:00
Sylvain Berfini
e5795ea05f Prevent crash if composing is sent when chat room not initialized yet 2025-02-03 09:43:17 +01:00
Sylvain Berfini
0625239477 Conference listener onActiveSpeakerParticipantDevice signature has changed 2025-01-30 10:59:21 +01:00
Sylvain Berfini
5e6186d115 Bumped dependencies 2025-01-30 09:48:51 +01:00
Sylvain Berfini
28998d4463 Fixed total unread message count if deleted chat room contained unread messages 2025-01-29 15:43:19 +01:00
Sylvain Berfini
6121040bda Fixed missing conversations until tab was left & opened again 2025-01-29 15:38:45 +01:00
Sylvain Berfini
0f3aea191f Improved Telecom Call Control Callback disconnect causes 2025-01-29 10:56:37 +01:00
Sylvain Berfini
e9385b5c07 Removed debug APK from GitLab artefacts and reduced artefacts expire 2025-01-29 09:27:34 +01:00
Sylvain Berfini
ec68e931c4 Fixed contacts not reladed when bodyless friendlist presence is received 2025-01-28 17:45:09 +01:00
Sylvain Berfini
37c23066f0 Fixed missing composing notifications 2025-01-28 16:55:49 +01:00
Sylvain Berfini
a5b8a8a683 Show git describe in startup logs 2025-01-27 17:03:47 +01:00
Sylvain Berfini
6d08625168 Fixed group call missed notification & in-call alert titles 2025-01-27 10:45:06 +01:00
Sylvain Berfini
b4329af83a Fixed operation in progress dialog not displayed when creating group call 2025-01-27 10:33:05 +01:00
Sylvain Berfini
de5f44ba04 Updated contacts when CardDAV sync is done 2025-01-27 09:23:42 +01:00
Sylvain Berfini
e686ed90e9 Updated participants label with count 2025-01-23 11:26:44 +01:00
Sylvain Berfini
f5ba48b9f0 No longer use conferenceScheduler for creating group calls 2025-01-23 09:22:20 +01:00
Sylvain Berfini
4dd92cddf0 Fixed issue with save/restore video preview position 2025-01-22 11:35:41 +01:00
Sylvain Berfini
1ead4d9218 Use call's startdate for system notification timestamp 2025-01-22 11:09:19 +01:00
Sylvain Berfini
6a41f7f67d Another attempt at preventing an empty fragment to be visible sometimes when going back from a conversation 2025-01-22 10:25:09 +01:00
Sylvain Berfini
febc6a85d3 Trying to prevent hidden bottom nav bar (very rare) 2025-01-22 10:12:34 +01:00
Sylvain Berfini
e1bcae703c Fixed meeting participants list not showing changes if joined right after 2025-01-22 09:53:17 +01:00
Sylvain Berfini
d05da148e7 Improved permission request in-app when it has already been denied once 2025-01-21 17:34:13 +01:00
Sylvain Berfini
ec9984c86b Show if audio codec is mono or stereo 2025-01-21 11:44:21 +01:00
Sylvain Berfini
456181609b Added shortcut to Android app settings in advanced params 2025-01-21 11:31:08 +01:00
Sylvain Berfini
f7d65b102e Fixed mistake in previous commit 2025-01-21 10:43:02 +01:00
Sylvain Berfini
06d1ae81b5 Prevent empty screen showing up sometimes when leaving a conversation 2025-01-21 10:02:51 +01:00
Sylvain Berfini
8477980011 Removed findNativeContact, it shouldn't be necessary and will prevent useless queries 2025-01-20 14:58:44 +01:00
Sylvain Berfini
e283b7b48e Prevent having two or more ZRTP SAS dialog stacked 2025-01-20 14:55:53 +01:00
Sylvain Berfini
fa9bcd3475 Show toast if record_audio or camera permission is missing while in call, if request permission fails open the android app settings 2025-01-20 12:28:02 +01:00
Sylvain Berfini
7f49a7756c Prevent crash in case of exception while creating the MasterKey in VFS 2025-01-20 09:50:02 +01:00
Sylvain Berfini
e7a4a24eaf Wait 500ms before reloading sound devices 2025-01-20 09:23:05 +01:00
Sylvain Berfini
e938105db0 Fixed settings french string 2025-01-18 19:58:32 +01:00
Sylvain Berfini
ca7035d84a Hide dynamically created empty chat rooms 2025-01-16 17:17:11 +01:00
Sylvain Berfini
27b4fa63f9 Prevent meeting schedule end time to change (when changing start time) if user set it manually 2025-01-16 14:58:11 +01:00
Sylvain Berfini
e464a49176 Fixed issue in current call conversation lookup 2025-01-16 14:26:07 +01:00
Sylvain Berfini
15f9d04747 Fixed back navigation when in conversation opened from meeting call history 2025-01-16 13:56:58 +01:00
Sylvain Berfini
9504c6d1ca Prevent show empty conversations as end to end encrypted and trusted 2025-01-16 13:49:19 +01:00
Sylvain Berfini
d0c733e81b Prevent huge list area when not required 2025-01-16 11:59:16 +01:00
Sylvain Berfini
e85c97837f Let user try to open file as plain text if no app on the device is registered to handle MIME type (useful for logs file without extension) 2025-01-16 11:08:00 +01:00
Sylvain Berfini
375c020b9b Added strings for conference security events 2025-01-16 09:56:24 +01:00
Sylvain Berfini
fee595cfdc Asking for EndToEnd encryption security level when scheduling a meeting/creating a group call if LIME is available 2025-01-16 08:25:05 +00:00
Sylvain Berfini
dbb1793ea0 Bumped dependencies 2025-01-15 20:52:35 +01:00
Sylvain Berfini
4b0fcd38d7 Reworked dialog confirmation model 2025-01-15 20:34:02 +01:00
Sylvain Berfini
3a3518b2a5 Do not disable file attachments in conversation attached to a call, added navigation to file viewer from in-call conversation 2025-01-15 18:36:11 +01:00
Sylvain Berfini
788dd338fb Fixed two meetings showing today's indicator in list 2025-01-15 16:18:09 +01:00
Sylvain Berfini
2e89f4d101 Show toast & un-toggle setting when VFS fails to be enabled + show dialog to confirm before turning it on 2025-01-15 16:05:54 +01:00
Sylvain Berfini
8b183fd347 Set LIME algo to c25519 2025-01-15 13:30:08 +01:00
Sylvain Berfini
815a2ed854 Fixed color issue 2025-01-15 11:31:08 +01:00
Sylvain Berfini
2ba54f085d Make sure foreground services notifications ID are re-set when service is destroyed 2025-01-15 10:22:27 +01:00
Sylvain Berfini
ae80e45e1c Fixed typo 2025-01-14 14:25:22 +01:00
Sylvain Berfini
7967a97b01 Updated ZRTP key agreements allowed algorithms 2025-01-14 11:18:12 +01:00
Sylvain Berfini
d5e20c02fe Fixed deadlock 2025-01-13 14:22:58 +01:00
Sylvain Berfini
00fbfcc490 Added early media advanced settings + added video views in incoming/outgoing call fragments to see/capture early media video if any 2025-01-13 12:29:34 +00:00
Sylvain Berfini
17e3622bdc Try to enter PiP mode when leaving CallActivity during a video call, either by back gesture or button, and remove PiP when call is ended 2025-01-13 12:08:19 +01:00
Sylvain Berfini
6b7591c971 Make link clickable 2025-01-13 10:12:49 +01:00
Sylvain Berfini
633fa07164 Fixed outline in landscape 2025-01-13 10:01:45 +01:00
Sylvain Berfini
fbe5885d08 Updated numpad to show letters 2025-01-11 14:15:12 +01:00
Sylvain Berfini
099350244a Added numpad floating action button 2025-01-11 10:54:29 +00:00
Sylvain Berfini
eac72a0e42 Do not show call ended fragment if accepted/declined on another device 2025-01-11 11:19:31 +01:00
Sylvain Berfini
de29fbc125 Updated gradle version 2025-01-11 10:50:46 +01:00
Sylvain Berfini
864677dab1 Do not use first address for global contact avatar model 2025-01-10 11:21:38 +01:00
Sylvain Berfini
c500761940 Keep screen on while playing media + fixed log file not opened if no extension is set for the file 2025-01-10 10:50:33 +01:00
Sylvain Berfini
e7f888ad78 Forgot to set the call notification color (setColorized is set to true) 2025-01-09 10:00:36 +01:00
Sylvain Berfini
32a3feef42 Improved tertiary button disabled style 2025-01-08 13:13:32 +01:00
Sylvain Berfini
e379833e18 Prevent race condition where service notification for FileTransferService would not be properly configured 2025-01-08 09:59:45 +01:00
Sylvain Berfini
486706299d Harmonized margins 2025-01-08 09:03:08 +01:00
Sylvain Berfini
b1a03db96f Added setting in CorePreferences to hide phone numbers from contacts (also disables contacts list filter) 2025-01-07 11:10:51 +01:00
Sylvain Berfini
5cee11c779 Show label when devices list in account profile is empty 2025-01-06 16:33:37 +01:00
Jonathan Bartet
a9736df3eb Update french file strings.xml 2025-01-06 15:41:37 +01:00
Sylvain Berfini
da848ecc61 Ignore unmute requests from telecom manager unless user is connected to Android Auto 2025-01-02 09:07:07 +00:00
Sylvain Berfini
942e78eede Fix chat message file transfer status when leaving & going back into conversation 2024-12-24 15:29:41 +01:00
Sylvain Berfini
b084807d18 Disable video toggle button and show indeterminate spinner while re-INVITE is in progress 2024-12-23 20:04:05 +01:00
Sylvain Berfini
d0dc42c67b Fixed text file colors in outgoing messages while in dark mode, fixed disabled in-call button color issue 2024-12-20 14:32:29 +01:00
Sylvain Berfini
83f249b1d9 Fixed contacts from remote LDAP/CardDAV not shown in contacts list when doing a search 2024-12-20 11:12:59 +01:00
Sylvain Berfini
8f52182959 Fixed file transfer in chat if file was picked from generic file picker 2024-12-19 16:56:10 +01:00
Sylvain Berfini
2c9a15a007 Removed track stop & gap from recording & media players 2024-12-19 15:39:08 +01:00
Sylvain Berfini
5d96ea3b56 Prevent 'media not found' error if it isn't found in conversation media list, in that case open it but you won't be able to swipe to other media files 2024-12-19 15:37:42 +01:00
Sylvain Berfini
e1c4fc2525 Fixed other accounts top bar notification background color in dark mode 2024-12-19 13:48:23 +01:00
Sylvain Berfini
049f63b61e Added icon next to last message in conversations list depending on it's content 2024-12-19 13:39:49 +01:00
Sylvain Berfini
eebc1bc91e Moved third party sip account login auth id field to advanced settings, added outbound proxy advanced setting 2024-12-19 12:12:03 +01:00
Sylvain Berfini
0d5bf5ed3e Fixed date picker in meeting scheduler 2024-12-19 08:42:25 +00:00
Sylvain Berfini
bad1621126 Updated jitpack.io maven repository configuration 2024-12-19 09:35:04 +01:00
Sylvain Berfini
8e93fefda3 Small improvements reported by Android Studio code inspector 2024-12-18 15:30:25 +01:00
Sylvain Berfini
ea92ca2220 Removed hardcoded text 2024-12-18 15:10:33 +01:00
Sylvain Berfini
3734c8ca90 Fixed issue when trying to add a participant to an existing group (meeting, conversation, etc...) 2024-12-18 14:20:16 +01:00
Sylvain Berfini
aad50669c4 Use Rejected instead of Error call disconnect cause because otherwise it triggers an exception on some devices 2024-12-18 13:56:40 +01:00
Sylvain Berfini
679f125870 Reworked meeting scheduler, now correctly handles selected date & time in local timezone to the one selected if different 2024-12-18 13:11:50 +01:00
Sylvain Berfini
a90dd53c9c Fixed dumb code 2024-12-18 09:07:06 +01:00
Sylvain Berfini
e664e1802c Fixed issue in chat bubble if more than 4 media files in single bubble 2024-12-17 16:37:43 +01:00
Sylvain Berfini
20fc177c7a Prevent changing timezone in meeting scheduler to update selected time 2024-12-17 10:40:16 +01:00
Sylvain Berfini
7c755bd080 Fixed issue with TelecomManager not disconnecting when call state is Error 2024-12-17 10:10:54 +01:00
Sylvain Berfini
372a16d3ab Fixed dark mode issue with participants list in conference + added menu to share meeting link 2024-12-17 10:10:27 +01:00
Sylvain Berfini
d4b0356bda Fixed deadlock causing ANR because SDK method was called directly in adapter 2024-12-16 16:39:26 +01:00
Sylvain Berfini
31b9836cc0 Reworked DTMFs dialpad in call to show letters 2024-12-16 15:52:41 +01:00
Sylvain Berfini
0571862142 Fixed today text color (in meetings) in dark mode 2024-12-16 15:37:21 +01:00
Sylvain Berfini
0497237630 More contrast improvements 2024-12-16 15:26:52 +01:00
Sylvain Berfini
75905c1321 Fixed tertiarty buttons disabled color 2024-12-16 15:12:37 +01:00
Sylvain Berfini
676a2c7710 Improved contrast for generated avatars in dark mode 2024-12-16 14:52:53 +01:00
Sylvain Berfini
967fb0563d Prevent outgoing calls to be routed on speaker while ringing 2024-12-16 14:36:52 +01:00
Sylvain Berfini
3fb5d8a97b Fixed circle buttons pressed color same as default in dark mode 2024-12-16 14:17:11 +01:00
Sylvain Berfini
4682a75049 Fixed wrong ID in layouts 2024-12-16 12:01:28 +01:00
Sylvain Berfini
466395a34b Added + icon under 0 in dialpad, changed background color in dark mode 2024-12-16 11:29:42 +01:00
Sylvain Berfini
2380c0fa6d Color improvements in dark mode for bottom sheets 2024-12-16 11:07:30 +01:00
Sylvain Berfini
137e8941cb Expand contact's devices for trust by default unless at 100% 2024-12-16 09:33:41 +00:00
Sylvain Berfini
409baab7c4 Fixed another typo 2024-12-16 09:14:38 +00:00
Sylvain Berfini
f068b5c9ce Prevent crash if identity address couldn't be parsed 2024-12-16 10:03:45 +01:00
Sylvain Berfini
3cdcc4bb77 Fixed splashscreen color in dark mode & window background color for bottom system bar, other color related improvements 2024-12-16 09:54:45 +01:00
Sylvain Berfini
a3ed13bc79 Show starred contacts first in lists 2024-12-12 15:58:53 +01:00
Sylvain Berfini
3a23c8813e Fixed conversation header invisible in landscape, updated missing separator color 2024-12-12 15:47:13 +01:00
Sylvain Berfini
9d58ced715 Set IMDN threshold to 1 2024-12-12 14:50:10 +01:00
Sylvain Berfini
86e2c731da Updated UI when contact no trusted device 2024-12-12 14:01:06 +01:00
Sylvain Berfini
5593e06cf5 Fixed typos 2024-12-12 12:00:33 +01:00
Sylvain Berfini
7bfed48b23 Updated some colors, no longer need to block material components version to an old release 2024-12-12 10:41:22 +01:00
Sylvain Berfini
821986b6a9 Prevent no default account being set, using first available account 2024-12-12 10:24:52 +01:00
Sylvain Berfini
b9065d3c7c Bumped dependencies, fixed back gesture when adding participants to a conversation or an existing meeting 2024-12-12 09:46:43 +01:00
Sylvain Berfini
47a308dcfc Fixed color issue in chat message forward fragment 2024-12-11 20:17:16 +01:00
Sylvain Berfini
9f6fb13e80 Another batch of fixes related to colors 2024-12-11 17:13:12 +01:00
Sylvain Berfini
6d3ef66995 More color fixes 2024-12-11 16:52:33 +01:00
Sylvain Berfini
e57ff021e9 Fixed chat message edit text field background color 2024-12-11 16:38:34 +01:00
Sylvain Berfini
d5ca4c29d3 Fixed broken avatar generation for notifications & shortcuts 2024-12-11 16:26:14 +01:00
Sylvain Berfini
b3da6ed347 Fixed issue with conference participants devices when admin status of one participant changes 2024-12-11 16:20:03 +01:00
Sylvain Berfini
b6e36272f7 Some fixes related to recent color/layout changes 2024-12-11 16:06:39 +01:00
Sylvain Berfini
41081f565a Reworked colors for better dark theme 2024-12-11 15:06:47 +01:00
Sylvain Berfini
b172396f0d Updated empty recordings list illustration 2024-12-11 11:51:16 +01:00
Sylvain Berfini
95b6a8e7a5 Improved some texts 2024-12-11 11:03:27 +01:00
Sylvain Berfini
d6ce9250fc Improved pressed effect in contact actions as well 2024-12-11 10:41:57 +01:00
Sylvain Berfini
0e057d8aa4 Improved press effect on conversation info actions 2024-12-11 10:39:06 +01:00
Sylvain Berfini
e0c58c0ac5 Added menu to go to shared media/documents from conversation details, only startPostponedEnterTransition() in doOnPreDraw for RecyclerView fragments 2024-12-10 20:28:19 +01:00
Sylvain Berfini
4304552bc8 Updated Ktlint 2024-12-10 12:20:52 +01:00
Sylvain Berfini
ab1d271b76 Updated coil from 2.7.0 to 3.0.4 2024-12-10 10:46:06 +01:00
Sylvain Berfini
b5b57405c6 Updated links that redirects to linphone.org new website 2024-12-10 10:06:09 +01:00
Sylvain Berfini
6476bb518d Added sub menu for file picker in conversation to allow picking files other than media 2024-12-09 16:44:02 +01:00
Sylvain Berfini
f9cf90fecd Show notification top bar when MWI messages are waiting 2024-12-09 14:37:58 +01:00
Sylvain Berfini
e5f227ba35 Do not list ourselves in group conversation participants if we have left, reworked disabled/encrypted conversation info box 2024-12-09 13:54:08 +01:00
Sylvain Berfini
d718cff486 Fixed wrong icon in account profile 2024-12-09 11:33:52 +01:00
Sylvain Berfini
3167544873 Added an option to copy conversation participant SIP URI from menu 2024-12-06 10:49:45 +01:00
Sylvain Berfini
af4ff25310 Fixed outgoing chat bubble background color in dark mode when using another theme than the default one 2024-12-06 09:05:35 +01:00
Sylvain Berfini
75bb28cb2f Recreate activity when theme is changed through remote provisioning, fixed keep alive service automatically enabled after remote provisioning 2024-12-05 16:52:20 +01:00
Sylvain Berfini
935d463896 Prevent last message sender name in front of composing label in chat rooms list cells + prevent network unreachable alert during core startup 2024-12-05 12:10:54 +01:00
Sylvain Berfini
c988504319 Clear previous grammar files from local storage 2024-12-05 11:45:57 +01:00
Sylvain Berfini
5bcf0e8ddb Added try/catch around some nav methods 2024-12-05 10:57:48 +01:00
Sylvain Berfini
a5f846a26d Added file sharing server URL in advanced settings 2024-12-04 13:49:54 +01:00
Sylvain Berfini
877565e516 Display media in square grid if exactly 4 of them 2024-12-04 11:23:52 +01:00
Sylvain Berfini
3a7265295e Fixed chat message file upload indicator 2024-12-03 22:16:35 +01:00
Sylvain Berfini
46dc7355b2 Replaced old URLs by new ones 2024-12-03 15:06:30 +01:00
Sylvain Berfini
d80b023918 Bumped dependencies 2024-12-03 14:47:23 +01:00
Sylvain Berfini
c4eb74ff0d Updated maven repository address 2024-12-03 14:42:13 +01:00
Sylvain Berfini
f48da167ab Protect navigate() calls 2024-12-03 14:38:44 +01:00
Sylvain Berfini
6b11e37b14 Make sure newly added account is marked as default in side menu 2024-12-02 11:25:30 +01:00
Sylvain Berfini
5a1487a691 Fixed call actions bar size & margins 2024-12-02 10:56:13 +01:00
Sylvain Berfini
badbbde183 Update call logs list when starting/receiving call while on call log details page 2024-12-02 10:31:04 +01:00
Sylvain Berfini
713ca4c1d5 Increased single media max height a bit in chat, renamed info message long press action and changed icon 2024-11-29 09:14:19 +01:00
Sylvain Berfini
d312141eda Fixed issue with media preview in chat bubble 2024-11-28 15:32:13 +01:00
Sylvain Berfini
4f0ca4adca Do not consider chat room as read-only if empty 2024-11-28 13:01:31 +01:00
Sylvain Berfini
a4c897b47d Fixes & improvements for remote contact directory search 2024-11-28 12:59:28 +01:00
Sylvain Berfini
711f2f4200 Fixed overlapped texts on narrow devices with increased text size 2024-11-27 10:07:35 +01:00
Sylvain Berfini
49d8381705 Make sure device name used in user-agent doesn't contains any apostrophe 2024-11-27 09:30:33 +01:00
Sylvain Berfini
0ce886cb56 Also apply foldable UI when in conference + prevent folded UI for non-tabletop orientation 2024-11-26 16:42:36 +01:00
Sylvain Berfini
40541d3316 Update contact when discovered later in call views, try to prevent incoming call notification to be visible when already in incoming call fragment 2024-11-26 13:34:00 +01:00
Sylvain Berfini
11e44d1fc4 Removed code not needed anymore, now done in SDK automatically 2024-11-26 11:14:08 +01:00
Sylvain Berfini
d864b5efa5 Added TURN NAT policy settings 2024-11-26 10:32:34 +01:00
Sylvain Berfini
0b0d7ce85a Updated chat message long press menu 2024-11-26 09:25:24 +01:00
Sylvain Berfini
363ce834fa Improved UI when flip/fold device is half-opened 2024-11-25 16:31:29 +01:00
Sylvain Berfini
8bb6b61edc Prevent crash in landscape on some extreme aspect ratio devices + added landscape version of numpad layout 2024-11-25 15:17:47 +01:00
Sylvain Berfini
2d2a5e26f6 Automatically play next message voice record, if any 2024-11-25 14:55:45 +01:00
Sylvain Berfini
213e62d125 Added remote contact directory lookup if friend isn't found locally 2024-11-25 10:11:52 +01:00
Sylvain Berfini
8d78f2b698 Added back spinner on incoming/outgoing call fragments, removed chronometer 2024-11-22 11:56:45 +01:00
Sylvain Berfini
f3a2020466 Improved call layouts for landscape orientation 2024-11-21 17:27:27 +01:00
Sylvain Berfini
167b42810f Added download progress percentage label 2024-11-21 15:47:02 +01:00
Sylvain Berfini
d71966e77d Search in remote CardDAV contacts if such server is configured 2024-11-20 15:05:25 +00:00
Sylvain Berfini
cf901a9c2b Try to prevent crash seen in Crashlytics 2024-11-20 14:58:16 +01:00
Sylvain Berfini
a743a0d2c6 Prevent crash in activity if dialog can't be shown 2024-11-20 10:33:51 +01:00
Sylvain Berfini
601aaf0b5c Updated README & CHANGELOG 2024-11-20 10:17:59 +01:00
Sylvain Berfini
f65666a996 Improved help & troubleshooting views 2024-11-18 11:38:14 +01:00
Sylvain Berfini
4f5ea3b5a4 Removed TODOs related to friends' refKey 2024-11-18 10:41:48 +01:00
Sylvain Berfini
90971224d5 Fixed ZRTP media encryption label in advanced settings not showing if post quantum or not 2024-11-15 16:11:40 +01:00
Sylvain Berfini
1f08682340 Fixed in-call/conf bottom sheet action label color not matching icon status 2024-11-14 17:30:15 +01:00
Sylvain Berfini
1e4066163f Prevent click on call bottom sheet background to collapse it and start full screen mode (by going through) 2024-11-14 16:27:09 +01:00
Sylvain Berfini
b7e7e08bbe Fixed conference chronometer having two different visibility binders 2024-11-14 13:19:09 +01:00
Sylvain Berfini
eb80a16202 Prevent being stuck in full screen mode without video 2024-11-14 12:44:43 +01:00
Sylvain Berfini
a7fb2ccfec Bumped dependencies 2024-11-14 11:00:23 +01:00
Sylvain Berfini
4afb10d82a Added meeting/group call conversation history in call log 2024-11-14 10:19:00 +01:00
Sylvain Berfini
f74976f563 Show forward icon in front of conversation last message if it has been forwarded 2024-11-13 17:17:50 +01:00
Sylvain Berfini
a96940b94a Fixed conversation subject not pre-filled when scheduling meeting 2024-11-13 16:55:48 +01:00
Sylvain Berfini
3fcbc9bf28 Fixed waiting room not displaying call progress when resumed 2024-11-13 14:21:32 +01:00
Sylvain Berfini
a2bf235656 Fixes issue in previous commit 2024-11-13 13:37:20 +01:00
Sylvain Berfini
40093d77cf Improved active speaker conference layout a bit 2024-11-13 13:22:40 +01:00
Sylvain Berfini
e205d0ef00 Trying to prevent bottom nav bar being gone sometimes 2024-11-13 10:39:43 +01:00
Sylvain Berfini
416f4ffef0 Use conference.getChatRoom() instead of searching it, enable text stream for scheduled meetings & group calls 2024-11-13 09:37:53 +01:00
Sylvain Berfini
68fe26e05f Keep bundle mode disabled by default for third party SIP accounts 2024-11-07 16:47:19 +01:00
Sylvain Berfini
cda5deb18d Clear single sign on cache file when removing account 2024-11-06 12:23:32 +01:00
Sylvain Berfini
5c8e4bcc22 Minor UI fix to add some space between end of meeting title and end of chat bubble 2024-11-06 12:07:40 +01:00
Sylvain Berfini
70b4a500d2 Lookup for conference's chat room if any, and allow navigating to it if found 2024-11-06 10:38:13 +01:00
Sylvain Berfini
135ca527ee Clicking on already selected contact will remove it from the selection 2024-11-05 17:01:28 +01:00
Sylvain Berfini
c88917ac68 Improved & fixed issues regarding incoming video call notification 2024-11-05 16:33:25 +01:00
Sylvain Berfini
d02ca882bd Disabled use auth info username as SSO login by default 2024-11-05 12:00:39 +01:00
Sylvain Berfini
807a36b54c Fixed UI not updated when kicked out group conversation 2024-11-04 15:25:46 +01:00
Sylvain Berfini
5f6c02e2ca Fixed minor UI issues 2024-11-04 15:04:09 +01:00
Sylvain Berfini
9a33def8a4 Bumped AGP to 8.7.2 2024-11-01 13:08:38 +01:00
Sylvain Berfini
88b4227bbc Auto close search bar when navigating from list into child details 2024-11-01 10:23:08 +01:00
Sylvain Berfini
5da87f598a Fixed MWI icon if no count is provided in the NOTIFY (only yes/no) 2024-10-31 16:32:00 +01:00
Sylvain Berfini
612c8b3301 Bumped dependencies 2024-10-31 10:32:46 +01:00
Sylvain Berfini
6ad5f7573b Fixed auto full screen issue in conference 2024-10-31 10:21:21 +01:00
Sylvain Berfini
2be4f691f2 Added participants count in conversation info 2024-10-29 16:37:43 +01:00
Sylvain Berfini
eeb19846cc Removed code related to all day meetings + fixed duration issue when only modifiying date 2024-10-24 15:18:00 +02:00
Sylvain Berfini
467e029599 Bumped dependencies 2024-10-24 10:18:43 +02:00
Sylvain Berfini
4ff8c7c7eb Added CCMP server URL setting in advanced account params 2024-10-23 23:05:36 +02:00
Sylvain Berfini
cc6ec98846 Reworked video preview in chat messages to prevent broken layout if thumbnail can't be extracted from video (or if picture is not displayable) 2024-10-23 12:53:39 +02:00
Sylvain Berfini
257352927d Migrated deprecated chat room & conference scheduler APIs 2024-10-22 17:00:05 +02:00
Sylvain Berfini
50cb162bd3 Fixed SSO sign-in when using remote provisioning without credentials information available 2024-10-22 16:41:08 +02:00
Sylvain Berfini
e562f1505d Added wake lock at startup 2024-10-22 11:33:36 +02:00
Sylvain Berfini
d5d3cc0bc2 Fixed issue in previous fix preventing opening a conversation from a chat message notification 2024-10-17 15:09:33 +02:00
Sylvain Berfini
d03f94d52a Prevent crash when conversation fragment resumes if viewmodel doesn't have a chatRoom stored 2024-10-17 09:48:00 +02:00
Sylvain Berfini
6edd20e214 Fixed issue in loss rates 2024-10-16 16:11:38 +02:00
Sylvain Berfini
0217ca78a0 Fixed layout issue in chat message ICS bubble for cancelled meeting with a description 2024-10-16 16:08:10 +02:00
Sylvain Berfini
c435852b32 Bumped AGP to 8.7.1 & Kotlin to 2.0.21, removed deprecated experimental k2 but enabled kapt k2 2024-10-16 12:42:43 +02:00
Sylvain Berfini
c6fe442b09 SDK will filter the chat rooms list itself, no need to do it in app 2024-10-15 15:47:12 +02:00
Sylvain Berfini
6d23402001 Added more call stats (loss rate & jitter buffer) 2024-10-15 14:13:03 +02:00
Sylvain Berfini
e3d356765d Merged blind/attended call transfer feature into one 2024-10-15 13:06:57 +02:00
Sylvain Berfini
d446e6d998 Updated behavior of alerts top bar 2024-10-14 12:18:26 +02:00
Sylvain Berfini
157ea2c847 Fixed missing week header in meetings list after deleting first meeting of the week 2024-10-14 10:53:59 +02:00
Sylvain Berfini
97f12b150a Fixed small issue in account settings layout 2024-10-10 15:59:24 +02:00
Sylvain Berfini
6826a51307 Dismiss chat message long press view if visible when doing a back gesture 2024-10-10 15:20:35 +02:00
Sylvain Berfini
8a39529fd1 Code cleanup 2024-10-10 11:18:41 +02:00
Sylvain Berfini
865f3b9692 Added setting to enable/disable logs printing in system logcat 2024-10-10 09:43:14 +02:00
Sylvain Berfini
b0dbfbcc3d Fixed layout 2024-10-08 16:12:59 +02:00
Sylvain Berfini
7d9a3edf31 Fixed positional arguments 2024-10-08 12:27:20 +00:00
Sylvain Berfini
d0ded694f8 Moved quit button to drawer menu, only visible if keep service alive setting is enabled 2024-10-08 14:01:14 +02:00
Sylvain Berfini
899a3ea374 Added voicemail icon below 1 in dialpad & voicemail URI setting in account params to be called when long pressing voicemail icon in dialpad 2024-10-08 10:47:04 +02:00
Sylvain Berfini
27b1cf90c6 Display configured ephemeral messages duration below conversation title (next to icon) 2024-10-07 11:46:38 +02:00
Sylvain Berfini
6d959d489b Updated AGP to 8.7.0 & Gradle to 8.9 2024-10-03 13:22:33 +02:00
Sylvain Berfini
9e08a5a506 Bumped navigation dependency version 2024-10-03 10:25:57 +02:00
Sylvain Berfini
86e09b5dce Actually do not disable earpiece, it works even without telecom manager support 2024-10-03 09:54:29 +02:00
Sylvain Berfini
248a06c8be For some reason, sometimes Telecom Manager Call Control Callback doesn't lists Earpiece as an available endpoint, disabling it when it happens 2024-10-02 16:51:45 +02:00
Sylvain Berfini
e8100e58da Updated firebase BoM version 2024-10-02 14:14:37 +02:00
Sylvain Berfini
10c68a2c28 Fixed another foreground service not started crash 2024-10-01 15:56:55 +02:00
Sylvain Berfini
cfe00d1b61 Added social app category to Manifest 2024-09-30 14:41:38 +02:00
Sylvain Berfini
20daa67ccf Fixed audio device java object instance in log 2024-09-30 13:46:59 +02:00
Sylvain Berfini
42cf8fd89e Fixed 'paused' label briefly visible at top of conference call UI when joining 2024-09-30 12:30:35 +02:00
Sylvain Berfini
bb4e9fdeb5 Hide call encryption in call ended fragment if call wasn't answered 2024-09-30 12:18:18 +02:00
Sylvain Berfini
cd8785855d Fixed quit app when keep alive service is enabled 2024-09-30 11:17:38 +02:00
Sylvain Berfini
fe2a074d0b Fixed foreground service notification lookup 2024-09-30 10:57:25 +02:00
Sylvain Berfini
63cb7d6630 Added confirmation dialog before starting a group call from a group conversation 2024-09-30 10:22:12 +02:00
Sylvain Berfini
eaa498f1ad Fix regarding in-call foreground service notification & other accounts notifications alert 2024-09-30 10:02:11 +02:00
Sylvain Berfini
f6d4f56bbc Fix voice record not playable if displayed before download terminated 2024-09-29 14:34:42 +02:00
Sylvain Berfini
31d92abcdf Update devices list when removing one 2024-09-29 14:16:27 +02:00
Sylvain Berfini
92ec1940e5 Prevent other accounts message notification header to be displayed when a message is received on the default account 2024-09-29 14:06:27 +02:00
Sylvain Berfini
5ebb4ee6ac Updated AGP, trying to fix crash due to disabled crashlytics with latest changes 2024-09-25 19:43:46 +02:00
Sylvain Berfini
a9d11543d8 Fixed build without google_services.json file 2024-09-24 14:59:46 +02:00
Sylvain Berfini
0e451dcc8a Added shortcuts in drawer menu if configured in remote prov 2024-09-19 16:54:37 +02:00
Sylvain Berfini
e74ebb0d81 Bumped dependencies + trigger voice message player more often to have a smoother progress bar 2024-09-19 14:14:05 +02:00
Sylvain Berfini
4f10a2d1fc Fixed Assistant started when setting & applying remote provisioning URI from advanced params 2024-09-19 13:40:10 +02:00
Sylvain Berfini
690f6fa4c2 Fixed rounded corners of main lists not properly applied on old Android versions 2024-09-19 10:13:47 +02:00
Sylvain Berfini
45cfac3ac6 Make shadow a bit lighter 2024-09-18 16:55:42 +02:00
Sylvain Berfini
db2ce910b9 Fixed pre-selected contacts not having the checkmark in participants list edition 2024-09-18 16:28:31 +02:00
Sylvain Berfini
0baceddf27 Fixed 'you have joined the group' chat event for created group icon not matching others + added new icon for admin rights granted event 2024-09-18 16:10:28 +02:00
Sylvain Berfini
fa7486ff36 Fixed issue with forward message 2024-09-18 10:40:08 +02:00
Sylvain Berfini
b2b55305d2 Forgot to change the label of sign out button in the confirmation dialog + hide delete account message for third party accounts 2024-09-17 17:15:28 +02:00
Sylvain Berfini
5eece24d68 Changed delete account into sign out (wording only) 2024-09-17 16:40:42 +02:00
Sylvain Berfini
51dd246971 Prevent external file sharing to attach file in already opened conversation if any 2024-09-17 12:53:18 +02:00
Sylvain Berfini
3190a1869d Added firebase project ID in troubleshooting fragment, updated icons 2024-09-16 11:53:54 +02:00
Sylvain Berfini
2c2beb5725 Fixed duplicated contacts in start call / conversation list 2024-09-16 10:43:50 +02:00
Sylvain Berfini
7b8b92706b Added very slight shadow effect at top of main lists 2024-09-16 09:50:39 +02:00
Sylvain Berfini
8fc3185278 Removed 'all day' switch in meeting scheduling/edit 2024-09-12 11:46:20 +02:00
Sylvain Berfini
96d85027c3 Workaround for first header not displayed when meetings list is initially drawn 2024-09-12 11:41:48 +02:00
Sylvain Berfini
41721e3994 Hide either IMDN or reactions bottom sheet with back gesture instead of directly going back 2024-09-12 10:13:20 +02:00
Sylvain Berfini
1a35a7048d Improved emoji reaction picker accessibility when font size is increased a lot at OS level 2024-09-11 16:20:39 +02:00
Sylvain Berfini
49059d6b3c Added back round corners to main lists in portrait mode (keep round top bar in landscape) + increased space at the bottom of the avatar in top main bar 2024-09-11 15:52:21 +02:00
Sylvain Berfini
c0e8bb6c1a Fixed issue with top margin, specifically visible when in landscape + reduced spaced in drawer layout for phones in landscape 2024-09-11 12:05:58 +02:00
Sylvain Berfini
1ef4e57d9e Reduced voice recording max width for narrow screens 2024-09-10 16:46:00 +02:00
Sylvain Berfini
85a61839bc Fixed display issue with files in chat bubbles on small screens 2024-09-10 14:23:39 +02:00
Sylvain Berfini
30ab0fa827 Enable both SIP INFO & RFC 2833 for DTMFs 2024-09-10 11:10:53 +02:00
Sylvain Berfini
439e115338 Added media encryption & media encryption mandatory advanced settings 2024-09-10 11:07:25 +02:00
Sylvain Berfini
722840f1c5 Hide software echo canceller toggle 2024-09-10 10:40:43 +02:00
Sylvain Berfini
db751efa91 Wait for adapter to contain data before attaching it to recyclerview to prevent loosing scroll position when rotating device 2024-09-09 11:13:13 +02:00
Sylvain Berfini
6c22b1f66d Added mark as read when dismissing message notif setting 2024-09-05 10:22:15 +02:00
Sylvain Berfini
9fc9574369 Added echo canceller calibration & adaptive rate control settings 2024-09-05 10:10:35 +02:00
Sylvain Berfini
ce4fed2197 Bumped dependencies 2024-09-05 09:27:23 +02:00
Sylvain Berfini
e03dcf3f88 Increased touch area for IMDN notifications + hide keyboard when showing IMDN/Emoji bottom sheet 2024-09-04 16:04:02 +02:00
Sylvain Berfini
d434ab7298 Bumped Firebase BoM version 2024-09-04 15:23:55 +02:00
Sylvain Berfini
4673db9e4e Do not automatically enable full screen mode when only our video is being sent 2024-09-03 17:15:21 +02:00
Sylvain Berfini
6893d35a93 Fixed muted state in conversations list cell when updated from conversation 2024-09-03 16:49:33 +02:00
Sylvain Berfini
4cbad5308c Fixed recordings parsing when file extensions have different lengths 2024-09-03 09:52:59 +02:00
Sylvain Berfini
7fb3a6ada3 Use .mka extension instead of .mkv for voice recordings, use .smff instead of mkv for call recordings 2024-09-02 17:03:44 +02:00
Sylvain Berfini
ace4caca3f Fixed incoming/outgoing call header overlapping system status bar 2024-09-02 16:50:00 +02:00
Sylvain Berfini
95c4106bd0 Prevent crash due to calling chatRoom.getLocalAddress() when it's in Instanciated state (because returned value is null) 2024-09-02 16:26:39 +02:00
Sylvain Berfini
c2e1333be1 Cancelling forward event (if any) when leaving fragment 2024-09-02 14:47:46 +02:00
Sylvain Berfini
b4a754db53 Should prevent 'no network' alert over orange background (instead of red) in main activity due to race condition 2024-09-02 14:35:03 +02:00
Sylvain Berfini
49c3e68f84 Removed useless code (but rotation still brings user to the bottom of messages, don't know why yet) 2024-08-29 17:19:57 +02:00
Sylvain Berfini
3359fbcbd7 Fixed history/conversations list scroll position not retained upon rotation 2024-08-29 17:08:54 +02:00
Sylvain Berfini
6308d66eb6 Fixed in-call numpad not sending DTMFs + prevent keyboard to open when touching numpad area 2024-08-29 17:00:47 +02:00
Sylvain Berfini
4628560411 Fixed meetings list scroll position not retained upon rotation 2024-08-29 16:54:43 +02:00
Sylvain Berfini
84af9437bc Revert picture(s) message summary using emoji 2024-08-29 14:36:44 +02:00
Sylvain Berfini
8a1d88c4b5 Prevent file transfer notification being still displayed after all transfer have ended 2024-08-29 14:19:22 +02:00
Sylvain Berfini
5a7872222d Added advanced account setting to manually update password 2024-08-29 13:43:46 +02:00
Sylvain Berfini
5d7addb8d8 Adding contentIntent to file transfer & push notification services 2024-08-29 11:16:15 +02:00
Sylvain Berfini
bed9288f21 Added device ID setting + fixed some wrong styles 2024-08-29 10:21:01 +02:00
Sylvain Berfini
ed1cf58fd4 Updated time formatter 2024-08-28 21:20:57 +02:00
Sylvain Berfini
a29777eebc Added back kill app button 2024-08-28 12:09:24 +02:00
Sylvain Berfini
1f604d54f2 Automatically enable full-screen mode when enabling video during a call 2024-08-27 17:02:35 +02:00
Sylvain Berfini
245b848c91 Fixed refreshMicrophoneState() that was toggling microphone state 2024-08-27 11:10:36 +02:00
Sylvain Berfini
34e1f0070e Adding back 'hide empty chat rooms' setting, was disabled at some point during 6.0 dev but should be enabled back 2024-08-27 11:07:33 +02:00
Sylvain Berfini
fb9d1c5c0d Prevent staying in assistant when logging in if login was denied once due to invalid password 2024-08-27 10:31:35 +02:00
Sylvain Berfini
b301a5227a Fixed meeting scheduling issues + fixed meetings for today not being displayed 2024-08-27 08:54:55 +02:00
Sylvain Berfini
94f2c1cc98 Fixed text message description not being italic for some parts + improved description: added duration to voice message & replaced file name by image emoji for pictures 2024-08-26 17:13:19 +02:00
Sylvain Berfini
cbaf7673f5 Improved chat bubbles & text color in dark mode 2024-08-26 15:38:18 +02:00
Sylvain Berfini
4d190cabdd Show cancelled meetings in list + fixed scroll to today 2024-08-26 12:03:57 +02:00
Sylvain Berfini
5c77b58154 Prevent case causing memory chat room to be re-used as a diffent one, leading to duplicated listener 2024-08-26 11:05:55 +02:00
Sylvain Berfini
ce1c3dad65 Prevent crash in CoreFileTransferService if Core's thread is busy and doesn't trigger notification fast enough 2024-08-26 10:44:40 +02:00
Sylvain Berfini
4a75315240 Log custom or unexpected types when parsing phone number labels 2024-08-25 10:47:53 +02:00
Sylvain Berfini
317bbb470b Prevent using requireContext() from Core's thead in Fragment 2024-08-23 09:24:49 +02:00
Sylvain Berfini
d8d424d446 Updated README 2024-08-22 15:36:19 +02:00
Sylvain Berfini
ff81a1c615 Bumped emoji picker version, adds skintone picker by long pressing emoji 2024-08-22 11:05:07 +02:00
Sylvain Berfini
769006b043 Added app's versionCode before git describe in troubleshooting fragment 2024-08-22 11:04:00 +02:00
Sylvain Berfini
c40c15b66f Added undertermined progress bar while fetching devices list in account profile 2024-08-22 11:03:21 +02:00
Sylvain Berfini
7c3bed7dd4 Fixed attach file button visible while recording a voice message 2024-08-21 18:44:53 +02:00
Sylvain Berfini
674fa1f41b Catch exception when posting a navigation task on main thread 2024-08-21 10:36:26 +02:00
Sylvain Berfini
ab1ea3392d Fixed duplicated phone numbers in loaded contacts from native addressbook + improved code by factorizing some parts of it 2024-08-21 10:23:53 +02:00
Sylvain Berfini
2821d4b72f Fixed meeting schedule using 'all day' toggle 2024-08-21 10:01:10 +02:00
Sylvain Berfini
9ed2415d1b Fixed participants devices events in history when using search & go to original message 2024-08-20 17:31:30 +02:00
Sylvain Berfini
859e32e655 Added hidden menus to display account contact address GRUU param & chat room peer address 2024-08-20 16:32:36 +02:00
Sylvain Berfini
f325c5ebbd Prevent calling getCallLogs() if Core isn't started yet 2024-08-20 11:05:16 +02:00
Sylvain Berfini
be849e0c80 Hide send message area while doing a search in a conversation 2024-08-20 09:53:16 +02:00
Sylvain Berfini
de9a2318af List all existing & writeable conversations in forward message conversations list 2024-08-19 16:50:31 +02:00
Sylvain Berfini
9ec927c0c4 Fixed issue in chat message reply layout for outgoing bubble 2024-08-19 16:07:01 +02:00
Sylvain Berfini
453b986f82 Fixed meeting list not always displaying week info 2024-08-19 14:19:19 +02:00
Sylvain Berfini
b123082559 Added back config entry to allow disabling native contacts default directory filter 2024-08-19 14:00:10 +02:00
Sylvain Berfini
27298639c3 Reduced high white space in schedule meeting layout at the top 2024-08-19 12:37:23 +02:00
Sylvain Berfini
2c09158977 Added a way to choose which friend list to use to store newly created contacts into (for CardDAV friend list for example) 2024-08-19 12:27:19 +02:00
Sylvain Berfini
bf4ab1b412 Added logs to help debug mic muted button state 2024-08-19 11:32:10 +02:00
Sylvain Berfini
ce7779f720 Do not display the duplicated SIP addresses in contact details 2024-08-19 11:13:17 +02:00
Sylvain Berfini
8c8f15b02d Prevent crash due to un-initialized property 2024-08-17 13:58:25 +02:00
Sylvain Berfini
ded00052b5 Use chatMessage.markAsRead() newly added API 2024-08-14 23:08:58 +02:00
Sylvain Berfini
e0dc53564e Fixed chat attachments preview 2024-08-14 18:25:09 +02:00
Sylvain Berfini
349167868f When scrolling to first unread message of many, remove 1 from counter 2024-08-14 12:55:29 +02:00
Sylvain Berfini
00d14feded Prevent crash when parsing a legacy call recording 2024-08-14 12:01:28 +02:00
Sylvain Berfini
48fd2ba1f8 SDK filters out duplicated SIP addresses & phone numbers now, no need to do it ourselves 2024-08-14 11:40:30 +02:00
Sylvain Berfini
dced2dae7e Improved contacts loader performances 2024-08-14 10:28:39 +02:00
Sylvain Berfini
00dd62553b Various minor improvements 2024-08-14 09:34:41 +02:00
Sylvain Berfini
a855c569fb Fixed scrolling up to original message from reply when it's not in the currently loaded history + hide keyboard when displaying message long press menu 2024-08-13 16:45:43 +02:00
Sylvain Berfini
2021c5e102 Added white background for PDF viewer (in case PDF background is transparent...) 2024-08-13 16:13:09 +02:00
Sylvain Berfini
bd93f0ed71 Bumped dependencies 2024-08-13 15:46:52 +02:00
Sylvain Berfini
5889389866 Keep pick file button instead of emoji picker in conversation while the keyboard is opened 2024-08-13 15:32:01 +02:00
Sylvain Berfini
2104d79e1c Prevent duplicated chat rooms in conversations list when added/removed/added 2024-08-13 14:59:43 +02:00
Sylvain Berfini
415da6f03d Added logs to help troubleshoot release APK signing 2024-08-13 12:11:28 +02:00
Sylvain Berfini
1d0b5a5d4d Build & upload nightly APK as release instead of debug 2024-08-13 10:23:53 +02:00
Sylvain Berfini
e5c7fa07cc Commented out Android Auto calling features for now as they are only available in private beta and we're not part of it yet 2024-08-13 09:50:38 +02:00
Sylvain Berfini
9c40709666 Removed the possibility to have a second chance at ZRTP SAS validation 2024-08-12 17:07:19 +02:00
Sylvain Berfini
4978c9a16d Close search/filter bar with back gesture/button 2024-08-12 16:49:25 +02:00
Sylvain Berfini
196a010f36 Prevent on some devices (such as Pocophone F1) display issue with keyboard & window insets 2024-08-12 15:56:01 +02:00
Sylvain Berfini
b58a23b60d Fixed Android Auto favorites no having generated avatar if no picture available + make them round 2024-08-12 14:37:37 +02:00
Sylvain Berfini
12c112fa39 Added favorites contacts grid in Android Auto UI 2024-08-12 13:43:30 +02:00
Sylvain Berfini
3d8bc10499 Revert changes to Manifest, only add min api car level for actual templated app 2024-08-12 12:15:16 +02:00
Sylvain Berfini
fdee7e618e Added min car level API to Manifest just in case 2024-08-12 11:52:41 +02:00
Sylvain Berfini
5ce7a5524a Added custom settings to directly go to third party sip account login & pre-fill some fields 2024-08-12 10:54:49 +02:00
Sylvain Berfini
ef32dac910 Prevent 'waiting for encryption' label in ended call screen if outgoing call was terminated before other end has answered it 2024-08-09 09:35:33 +02:00
Sylvain Berfini
ea49d3a411 Fixed missing contacts' avatars in chat room shortcuts 2024-08-08 20:25:34 +02:00
Sylvain Berfini
6280ed5f3d Fixed IM encryption mandatory setting that wasn't having any effect anymore 2024-08-08 20:03:33 +02:00
Sylvain Berfini
51d9b18c1c Added missing foreground service notification for outgoing call 2024-08-08 19:47:13 +02:00
Sylvain Berfini
6746e71197 Update call UI to reflect mic muted state when toggled from Android Auto 2024-08-08 19:38:51 +02:00
Sylvain Berfini
57f3b0c78b Added missing auth id field in third party account login form 2024-08-07 17:18:01 +02:00
Sylvain Berfini
455db9b9eb Added a setting to have the dialpad automatically showing up (disabled by default) 2024-08-07 17:12:15 +02:00
Sylvain Berfini
c8d9248e0c Close any expanded bottom sheet in call/conference fragment instead of leaving is user click/gesture back (except for back arrow at top) 2024-08-07 17:06:08 +02:00
Sylvain Berfini
a7f868fe15 Copy app/SDK version when clicking on it 2024-08-07 16:36:27 +02:00
Sylvain Berfini
59aa036875 Hide any message bottom sheet when leaving conversation 2024-08-07 15:40:08 +02:00
Sylvain Berfini
1c8a376c7f Also improved account devices when system display & text size is increased by user 2024-08-07 15:19:52 +02:00
Sylvain Berfini
1ee5993624 Improved help UI when system display & text size is increased by user 2024-08-07 15:09:06 +02:00
Sylvain Berfini
247788c64c Added a setting to choose if we want to use the SIP address username as SSO username 2024-08-07 14:21:41 +02:00
Sylvain Berfini
505fa3b66c Go to assistant when last account has been removed 2024-08-07 11:33:09 +02:00
Sylvain Berfini
a676c51401 Finally managed to make call notifications visible on Android Auto! 2024-08-07 10:00:10 +02:00
Sylvain Berfini
8e588bf800 Added missing pending intent for keep alive service 2024-08-06 16:15:16 +02:00
Sylvain Berfini
f29f5f9bc7 Fixed our own avatar not updated in meetings & conversations after being changed 2024-08-06 15:46:59 +02:00
Sylvain Berfini
a136b7da8b Added missing start at boot feature 2024-08-06 11:39:05 +02:00
Sylvain Berfini
bc38ff19b6 Added more space around list cells 2024-08-06 10:48:56 +02:00
Sylvain Berfini
9e3a7055fe Fixed contact avatars in group chat room messages after reload 2024-08-06 09:43:22 +02:00
Sylvain Berfini
36cc74956e Various fixes from Crashlytics reported issues 2024-08-05 15:31:29 +02:00
Sylvain Berfini
6c20ac8d40 Fixed upload progress indicator not visible when uploading only one file 2024-08-05 12:16:40 +02:00
Sylvain Berfini
59ebff6d15 Added IME action for main fragments search bar to close keyboard 2024-08-05 11:36:17 +02:00
Sylvain Berfini
b55d7070b0 Update participant(s) info in conversation when contact(s) changes 2024-08-05 10:59:51 +02:00
Sylvain Berfini
55cd29e710 Using new SDK APIs to improve chat message search in conversation 2024-08-05 10:01:46 +02:00
Sylvain Berfini
8a410fc77f Store selected contacts list filter & show favorites user preferences to re-use them later 2024-07-31 14:49:52 +02:00
Sylvain Berfini
5cc491335c Prevent crashes in case of exception 2024-07-31 09:29:21 +02:00
Sylvain Berfini
e14a142fb7 Fixed repo path in gitlab-ci file 2024-07-30 14:02:19 +02:00
Sylvain Berfini
f3b6627635 Fixed generated APK file name 2024-07-30 13:48:19 +02:00
Sylvain Berfini
3ec942c475 Forgot to add job-upload.yml to included files 2024-07-30 13:27:06 +02:00
Sylvain Berfini
ce5a5c5ddc Restored gitlab CI sign & upload jobs 2024-07-30 12:27:26 +02:00
Sylvain Berfini
a53542c092 Revert change due to API change in SDK that was reverted 2024-07-30 12:05:28 +02:00
Sylvain Berfini
ae59911f0a Do not use color state resource for background (crashes on Android 9), use drawable instead + use direct color attribute when possible 2024-07-29 19:24:50 +02:00
Sylvain Berfini
e29d64a02b Fixed build with latest 5.4 SDK & bumped dependencies 2024-07-29 14:11:16 +02:00
Sylvain Berfini
4c66012372 Fixed video player aspect ratio 2024-07-22 11:12:36 +02:00
Sylvain Berfini
9465593aa6 Fixed full screen switch after device rotation in media viewer 2024-07-22 10:25:37 +02:00
Sylvain Berfini
2394c701cf Added shortcut in call settings to change incoming call ringtone 2024-07-20 18:13:00 +02:00
Sylvain Berfini
0bea45054b Fixed chat bubbles & text going out of screen 2024-07-19 16:16:40 +02:00
Sylvain Berfini
756a83797f Various UI improvements related to emoji reactions 2024-07-19 15:13:19 +02:00
Sylvain Berfini
87f4ebbd4c Do not start keep alive service as foreground yet, wait for it to be started 2024-07-19 12:46:35 +02:00
Sylvain Berfini
5689557487 Also enable Crashlytics for releases 2024-07-19 11:24:28 +02:00
Sylvain Berfini
e85488cf65 Fixed audio not routed to connected bluetooth device during call 2024-07-19 11:16:19 +02:00
Sylvain Berfini
83df46ca8b Remove existing chat message reaction instead of trying to send it again 2024-07-18 17:21:44 +02:00
Sylvain Berfini
1d328f98e3 Fixed margin in conversation header if no back button 2024-07-18 14:59:58 +02:00
Sylvain Berfini
b5b37bd74d Hide 'copy text' from chat message long press menu if message contains no text 2024-07-18 14:50:29 +02:00
Sylvain Berfini
a6b510e536 Fixed 'no meeting today' at the bottom of the screen with header at the top if list is empty 2024-07-18 14:46:34 +02:00
Sylvain Berfini
22795a5284 Improved main screens' top bar 2024-07-18 14:21:02 +02:00
Sylvain Berfini
4c9bdec61e No longer use dialog for chat message long press 2024-07-18 12:26:12 +02:00
Sylvain Berfini
a73e483478 Fixed issue in permissions layout in landscape + post_notifications permission not granted alert when it was in fact granted 2024-07-18 09:50:51 +02:00
Sylvain Berfini
8ce91d1300 Fixed 'waiting for encryption' when leavign & going back to a call 2024-07-16 16:01:59 +02:00
Sylvain Berfini
79ca1d6505 Using newly added API to know if FEC is available in the current call 2024-07-16 09:27:05 +02:00
Sylvain Berfini
7d40ad5ad1 Finished international prefix help dialog 2024-07-15 16:12:06 +02:00
Sylvain Berfini
681c0f22c3 Do not duplicate ConversationFragment.kt for in-call view anymore 2024-07-15 16:12:06 +02:00
Sylvain Berfini
2086fbad66 Fixed chat bubble long press layout issue in full-screen mode 2024-07-15 16:12:05 +02:00
Sylvain Berfini
fb86cd9bcb Show check mark next to selected contacts when creating a group call/conversation 2024-07-15 16:12:05 +02:00
Sylvain Berfini
b9addcf683 Updated schedule meeting layout 2024-07-15 16:12:05 +02:00
Sylvain Berfini
6c757a9637 Finished contact trust explanation dialog 2024-07-15 16:12:05 +02:00
Sylvain Berfini
6cabf0bdf7 Improved empty subject for start/edit group call/conversation 2024-07-15 16:12:05 +02:00
Sylvain Berfini
179a6c39ca Hide mark as read action on conversation if no unread message 2024-07-15 16:12:05 +02:00
Sylvain Berfini
8a92624254 Updated dependencies 2024-07-15 16:12:05 +02:00
Sylvain Berfini
7aebfafde3 Fixed 1-1 conversation having chartoom-xxxx subject 2024-07-15 16:12:05 +02:00
Sylvain Berfini
1cd79b5086 Added FEC in-call stats 2024-07-15 16:12:05 +02:00
Sylvain Berfini
25be5e1814 Updated CHANGELOG 2024-07-15 16:12:05 +02:00
Sylvain Berfini
c7032b6000 Added data sync foreground service when transfering files through chat (upload & download) 2024-07-15 16:12:05 +02:00
Sylvain Berfini
bf1140fb38 Fixed contact that can't be opened in-app because it has no refKey 2024-07-15 16:12:05 +02:00
Sylvain Berfini
e012d45c10 Disable action buttons & hide numbers & SIP addresses fields if contact doesn't contains any 2024-07-15 16:12:05 +02:00
Sylvain Berfini
11eb0199f8 Start CardDAV synchronization when app starts + improved contact edition 2024-07-15 16:12:05 +02:00
Sylvain Berfini
949d870d7e Force contacts reload after adding/removing CardDAV account 2024-07-15 16:12:05 +02:00
Sylvain Berfini
0cfdedc09a Go back after deleting CardDAV config 2024-07-15 16:12:05 +02:00
Sylvain Berfini
d8c406320c Fixed status bar color in task manager 2024-07-15 16:12:05 +02:00
Sylvain Berfini
a278333eb4 Fixed time zone not displayed in meeting details 2024-07-15 16:12:05 +02:00
Sylvain Berfini
0d5b189978 Added unread messages count on conversation icon during call 2024-07-15 16:12:05 +02:00
Sylvain Berfini
e1852f4ae4 Updated edit meeting layout to match schedule meeting one + hidden meeting repeat option as it's not available yet 2024-07-15 16:12:05 +02:00
Sylvain Berfini
964a597aa1 Improved empty list display due to filter not matching any item 2024-07-15 16:12:05 +02:00
Sylvain Berfini
5ae345e794 Fixed time picker in conference scheduler + added time zone picker in schedule meeting UI 2024-07-15 16:12:05 +02:00
Sylvain Berfini
5289dc4824 Fixed issue in contacts list if some share the same SIP address(es) 2024-07-15 16:12:05 +02:00
Sylvain Berfini
f94b57d304 Added Android 15 AppStartupListener to print info 2024-07-15 16:12:05 +02:00
Sylvain Berfini
72fd952a55 Prevent paused by remote label to hide remote call end display name 2024-07-15 16:12:05 +02:00
Sylvain Berfini
811a0466d4 Updated one_account_max to allow to set a different limit 2024-07-15 16:12:05 +02:00
Sylvain Berfini
f449763c4e Improved new conversation insertion in list 2024-07-15 16:12:05 +02:00
Sylvain Berfini
c4fa68858c Fixed display issue if upload logs button is hidden 2024-07-15 16:12:05 +02:00
Sylvain Berfini
77e99fbfd3 Trying to prevent 'network not reachable' alert displayed when it shouldn't + added logs to help debug conferences list 2024-07-15 16:12:05 +02:00
Sylvain Berfini
af3cab475b Fixed missing toasts in conversations list + improved conversation removal (no longer scrolling back to top) 2024-07-15 16:12:05 +02:00
Sylvain Berfini
57f8ff3341 Replaced AccountCreator by AccountManagerServices & using it to list account devices 2024-07-15 16:12:05 +02:00
Sylvain Berfini
54ee456f8e Added missing cancel dialog when deleting a meeting for which we are the organizer from the meetings list + updated strings 2024-07-15 16:12:05 +02:00
Sylvain Berfini
f3eb821946 Fixed stucked in 'wait for encryption' for SRTP & not encrypted calls 2024-07-15 16:12:05 +02:00
Sylvain Berfini
a788191e50 Bumped car dependency version 2024-07-15 16:12:05 +02:00
Sylvain Berfini
94e6b28f4f Fixed ZRTP dialogs missing upon rotation 2024-07-15 16:12:05 +02:00
Sylvain Berfini
ce3c37ad15 Hide zrtp dialog alert try again button after second failed attempt + added landscape layout + fixed security level update on avatar 2024-07-15 16:12:05 +02:00
Sylvain Berfini
af6cfdfc18 Leave start call fragment after successful call && updated name group call / conversation dialog to stay at top of screen 2024-07-15 16:12:05 +02:00
Sylvain Berfini
515b645b89 Using call.callLog.remoteAddress() instead of call.remoteAddress() to have Address' displayName use P-Asserted-Identity info if available 2024-07-15 16:12:05 +02:00
Sylvain Berfini
c6550b6256 Intercept click on background while operation in progress dialog is visible 2024-07-15 16:12:05 +02:00
Sylvain Berfini
e3f1611a0f Prevent unecessary sound devices reload 2024-07-15 16:12:05 +02:00
Sylvain Berfini
08052d64fd Added top bar alert if post notifications permission isn't granted + improved connexion error toast + improved network unreachable alert 2024-07-15 16:12:05 +02:00
Sylvain Berfini
8f34c3ea5c Added dialog when ZRTP SAS token doesn't match or when clicking on 'nothing matches' button 2024-07-15 16:12:05 +02:00
Sylvain Berfini
ceb3679975 New ZRTP SAS validation dialog for cache mismatch scenario 2024-07-15 16:12:05 +02:00
Sylvain Berfini
72f8574a1e Improvements & fixes related to bottom sheets 2024-07-15 16:12:05 +02:00
Sylvain Berfini
ec6316f6e5 Reworked zrtp sas validation dialog layout 2024-07-15 16:12:05 +02:00
Sylvain Berfini
4bd6dc4e0f Improved & factorized call's media encryption icon & label info 2024-07-15 16:12:05 +02:00
Sylvain Berfini
cad05a0c83 Targetting Android 15 2024-07-15 16:12:05 +02:00
Sylvain Berfini
c68db48de9 Speed up a bit conversation update security level 2024-07-15 16:12:05 +02:00
Sylvain Berfini
e80c0e6068 Added back IPv6 network setting 2024-07-15 16:12:05 +02:00
Sylvain Berfini
b3464433ed Updated CHANGELOG 2024-07-15 16:12:05 +02:00
Sylvain Berfini
5cd0a741ab Forgot to break loop + fixed issue in log 2024-07-15 16:12:05 +02:00
Sylvain Berfini
60a3752fe8 Factorized code 2024-07-15 16:12:05 +02:00
Sylvain Berfini
217f116324 Hidden separator in accounts list cells 2024-07-15 16:12:05 +02:00
Sylvain Berfini
1b644741c6 Bumped gradle to 8.7.0 and AGP to 8.5.0 2024-07-15 16:12:05 +02:00
Sylvain Berfini
c7586feebc Fixed group chat room security level indicator over avatar 2024-07-15 16:12:05 +02:00
Sylvain Berfini
5b80833f30 Reworked chat message forward UI/UX 2024-07-15 16:12:05 +02:00
Sylvain Berfini
a858cffc82 Prevent crash if LDAP server URL wasn't filled 2024-07-15 16:12:05 +02:00
Sylvain Berfini
974e0cffc6 Started CHANGELOG 2024-07-15 16:12:03 +02:00
Sylvain Berfini
9f36ec950f Hidden main color selector 2024-07-15 16:10:44 +02:00
Sylvain Berfini
791e209d62 x86 & x86_64 ABIs are now removed 2024-07-15 16:10:44 +02:00
Sylvain Berfini
cb589b95c8 Bumped agp to 8.4.2 2024-07-15 16:10:44 +02:00
Sylvain Berfini
5e727b4e42 Fixed conversation opened upon rotating the device after clicking on message notification 2024-07-15 16:10:44 +02:00
Sylvain Berfini
058a88424c Token to read in ZRTP SAS validation is now in bold 2024-07-15 16:10:44 +02:00
Sylvain Berfini
3ccf4dc3a6 Check if SIP addresses fields are empty before parsing them 2024-07-15 16:10:44 +02:00
Sylvain Berfini
b7996b9e82 Added missing URI handler schemes 2024-07-15 16:10:44 +02:00
Sylvain Berfini
274ed49f16 Update ZRTP related code to use newly added APIs 2024-07-15 16:10:44 +02:00
Sylvain Berfini
fb75ea344c Fixed reply display name when it's one of our messages that has been replied to 2024-07-15 16:10:44 +02:00
Sylvain Berfini
483e88a02d Force navigation bar to use dark mode in Call activity 2024-07-15 16:10:44 +02:00
Sylvain Berfini
ef9339e912 Renamed & moved around some strings 2024-07-15 16:10:44 +02:00
Sylvain Berfini
896b621545 Sort delivery status lists by timestamp 2024-07-15 16:10:44 +02:00
Sylvain Berfini
12e7041c57 Moved some settings around, removed ADB logcat toggle 2024-07-15 16:10:44 +02:00
Sylvain Berfini
01f6ac29e9 Adding clear input when long pressing backspace button in numpad + playing DTMF tone if enabled at OS level 2024-07-15 16:10:44 +02:00
Sylvain Berfini
a71ba2096b Fixed auto start call recording setting 2024-07-15 16:10:44 +02:00
Sylvain Berfini
ee4e332330 Removed route audio to bluetooth setting, already done by TelecomManager 2024-07-15 16:10:44 +02:00
Sylvain Berfini
f9f25b2b15 Removed auto-export media, won't work with auto download 2024-07-15 16:10:44 +02:00
Sylvain Berfini
87bfe5b6c4 Renaming a few things to make it better understandable 2024-07-15 16:10:44 +02:00
Sylvain Berfini
88c230f136 Removed secureMode/interopMode related code for now, will do it again the day it will be available in SDK + minor improvements 2024-07-15 16:10:44 +02:00
Sylvain Berfini
cfe7a8ed38 Added confirmation dialog before clearing conversation history 2024-07-15 16:10:44 +02:00
Sylvain Berfini
a3489f4064 Fixed text file sharing (including linphone config) 2024-07-15 16:10:44 +02:00
Sylvain Berfini
ab9aedf5ee Fixed file viewer header colors in dark mode 2024-07-15 16:10:44 +02:00
Sylvain Berfini
aab5863b57 Reworked Core startup & configuration 2024-07-15 16:10:44 +02:00
Sylvain Berfini
0238de5b3b Added log regarding Core's video policy 2024-07-15 16:10:44 +02:00
Sylvain Berfini
b8e2541cda Updated dependencies 2024-07-15 16:10:44 +02:00
Sylvain Berfini
87153679a3 No longer need to remove reply action from message notification 2024-07-15 16:10:44 +02:00
Sylvain Berfini
91df15c022 Auto enable keep-alive foreground service when configuring an account that requires it 2024-07-15 16:10:44 +02:00
Sylvain Berfini
8f4c5bdc61 Getting rid of dataSync foreground service type for keeping app alive in background as it won't work more than 6h a day starting Android 15 2024-07-15 16:10:44 +02:00
Sylvain Berfini
84b39df26c Removed unused code + updated account settings layout 2024-07-15 16:10:44 +02:00
Sylvain Berfini
1741ed5fe5 Fixed clicking message notification not going into conversation 2024-07-15 16:10:44 +02:00
Sylvain Berfini
56d8e64762 Fixed being stuck in contacts if permission wasn't granted 2024-07-15 16:10:44 +02:00
Sylvain Berfini
78f1a1e645 Updated recordings list & added recording player (audio & video) 2024-07-15 16:10:44 +02:00
Sylvain Berfini
37786a0b83 Added copy SIP address in account profile 2024-07-15 16:10:44 +02:00
Sylvain Berfini
824b225549 No longer needed to filter out deprecated devices 2024-07-15 16:10:44 +02:00
Sylvain Berfini
4c127344c1 Forgot file viewer inset padding + allow SIP address in account profile to take two lines 2024-07-15 16:10:44 +02:00
Sylvain Berfini
61c1079e7c Revert "Targetting & compiling for Android 15", it prevents from installing on Android older than 15
This reverts commit 21a96a403406752e9f5793fc696ce61673a042ab.
2024-07-15 16:10:44 +02:00
Sylvain Berfini
dcbd978184 Keep shortedges instead of always as the later was only added in Android 11 2024-07-15 16:10:44 +02:00
Sylvain Berfini
fa6d93076c Targetting & compiling for Android 15 2024-07-15 16:10:44 +02:00
Sylvain Berfini
a95c6fb287 Android 15 will force edge-to-edge, starting supporting it now 2024-07-15 16:10:44 +02:00
Sylvain Berfini
e039a562fe Fixed chronometer not hidden in fullscreen 2024-07-15 16:10:44 +02:00
Sylvain Berfini
69d149a284 Trying to prevent sliding pane child to open when app starts after it was killed while in background with sliding pane child opened 2024-07-15 16:10:44 +02:00
Sylvain Berfini
6d099a3075 Enable proximity sensor for outgoing calls if video is disabled 2024-07-15 16:10:44 +02:00
Sylvain Berfini
eea978d8e8 Improved opening meeting with a lot of participants fluidity 2024-07-15 16:10:44 +02:00
Sylvain Berfini
46a6751df6 Fixed meetings participants not aligned top in list 2024-07-15 16:10:44 +02:00
Sylvain Berfini
1594252aeb Updated icon & text for in-call media encryption label for SRTP 2024-07-15 16:10:44 +02:00
Sylvain Berfini
ec207af81d Added filter for recordings + legacy recording files 2024-07-15 16:10:44 +02:00
Sylvain Berfini
cda23c44c0 Added a logger for Android Auto connection state + do not use bluetooth sound card as playback device for voice recordings when connected to Auto 2024-07-15 16:10:44 +02:00
Sylvain Berfini
144fc5d728 Fixed back stack with conversation shortcuts on launcher icon 2024-07-15 16:10:44 +02:00
Sylvain Berfini
a424f05ab9 Fixed meeting participants list not showing all of them 2024-07-15 16:10:44 +02:00
Sylvain Berfini
481e8db0aa Keep phone number label(s) if any after editing a contact 2024-07-15 16:10:44 +02:00
Sylvain Berfini
f6e6914656 Leave CardDAV config fragment when sync is succesful 2024-07-15 16:10:44 +02:00
Sylvain Berfini
75ce3a9c05 Fixed toast color 2024-07-15 16:10:44 +02:00
Sylvain Berfini
e83afdf436 Update meeting details after successfull edit 2024-07-15 16:10:44 +02:00
Sylvain Berfini
5d58b2a0fd Ask contacts permission in fragment's onResume() if needed 2024-07-15 16:10:44 +02:00
Sylvain Berfini
133f533b87 Fixed back stack when sharing logs using conversation 2024-07-15 16:10:44 +02:00
Sylvain Berfini
c927a83aa5 Migrated buildconfig 2024-07-15 16:10:44 +02:00
Sylvain Berfini
3a2757d962 Alert user when player can't open a voice recording 2024-07-15 16:10:44 +02:00
Sylvain Berfini
2e8b258b90 Updated android gradle plugin version 2024-07-15 16:10:44 +02:00
Sylvain Berfini
37df7d83e3 Updated crashlytics & firebase BoM 2024-07-15 16:10:44 +02:00
Sylvain Berfini
dddfdd8b78 Updated settings.gradle.kts to only have one maven repository for linphone, depending on if local exists or not 2024-07-15 16:10:44 +02:00
Sylvain Berfini
76716503e9 Added Tunnel settings 2024-07-15 16:10:44 +02:00
Sylvain Berfini
26e1332421 Added share / download long press menu items for call recordings 2024-07-15 16:10:44 +02:00
Sylvain Berfini
6cfec04424 Added MWI urgent messages count to drawer 2024-07-15 16:10:44 +02:00
Sylvain Berfini
bf64b496c9 Added audio & video codecs settings in advanced parameters 2024-07-15 16:10:44 +02:00
Sylvain Berfini
4f6c5c7f48 Added audio device picker in advanced settings 2024-07-15 16:10:44 +02:00
Sylvain Berfini
d48f7697df Added MWI URI account setting + added missing hints into text based settings 2024-07-15 16:10:44 +02:00
Sylvain Berfini
99e771898a Prevents attaching more than 12 files to a same message 2024-07-15 16:10:44 +02:00
Sylvain Berfini
7cf51f51a1 Added explanation bottom sheet for unsafe conversations instead of going into account profile mode fragment that was recently removed 2024-07-15 16:10:44 +02:00
Sylvain Berfini
fc8ac5fc56 Only go to choose secure/interop mode after successfully connecting first sip.linphone.org account 2024-07-15 16:10:44 +02:00
Sylvain Berfini
7cff514c3a Hidden change mode area & child fragment in account profile, moved IM encryption mandatory to account settings 2024-07-15 16:10:44 +02:00
Sylvain Berfini
3060af3940 Fixed size issue for chat media grid item 2024-07-15 16:10:44 +02:00
Sylvain Berfini
458f817142 Fixed missed call(s) indicator not visible or not hidden in drawer menu 2024-07-15 16:10:44 +02:00
Sylvain Berfini
701117474e Fixed main activity rotation issue + back stack not empty when clicking chat message notification 2024-07-15 16:10:44 +02:00
Sylvain Berfini
e93e6ca5cc Renamed recordings fragment 2024-07-15 16:10:44 +02:00
Sylvain Berfini
f8d60c1284 Moved push notif account toggle from profile to settings 2024-07-15 16:10:44 +02:00
Sylvain Berfini
01d721d477 Added countdowns in chat bubbles for ephemeral messages 2024-07-15 16:10:43 +02:00
Sylvain Berfini
6ecc0839ea Added empty recordings list image & label 2024-07-15 16:10:43 +02:00
Sylvain Berfini
1d5e496f3f Fix for recording player status when starting another one 2024-07-15 16:10:43 +02:00
Sylvain Berfini
3adbd83259 Fixed some icons color 2024-07-15 16:10:43 +02:00
Sylvain Berfini
376af91e88 Reworked file viewer interface, improved video/audio player 2024-07-15 16:10:43 +02:00
Sylvain Berfini
26e30c6060 Improved startup reactivity 2024-07-15 16:10:43 +02:00
Sylvain Berfini
f6545f5641 Reworked Groovy gradle files to Kotlin 2024-07-15 16:10:43 +02:00
Sylvain Berfini
31580e6291 Reworked recordings player 2024-07-15 16:10:43 +02:00
Sylvain Berfini
4f848b182a Updated pick number or address title 2024-07-15 16:10:43 +02:00
Sylvain Berfini
d5f43323a2 Increase play/pause button size in recordings list 2024-07-15 16:10:43 +02:00
Sylvain Berfini
f90b518f43 Added a setting to disable UI secure mode preventing screenshots & screen recording 2024-07-15 16:10:43 +02:00
Sylvain Berfini
8f1eeebd66 Fixed meetings briefly visible sometimes 2024-07-15 16:10:43 +02:00
Sylvain Berfini
9a97a45448 Do not start welcome or assistant acitvities if remote provisioning URI is given in intent data 2024-07-15 16:10:43 +02:00
Sylvain Berfini
1ea32e7544 Added simple call recording player 2024-07-15 16:10:43 +02:00
Sylvain Berfini
80da408930 Downgraded material version to fix contact devices progress bar 2024-07-15 16:10:43 +02:00
Sylvain Berfini
8303b356da Added offline_access scope to SSO builder so SDK can refresh token 2024-07-15 16:10:43 +02:00
Sylvain Berfini
a557875ce8 Started call recordings list 2024-07-15 16:10:43 +02:00
Sylvain Berfini
4d561a4635 Updated SSO to add client ID & token endpoint URL to auth info 2024-07-15 16:10:43 +02:00
Sylvain Berfini
fa7d4bc267 Updated dependencies 2024-07-15 16:10:43 +02:00
Sylvain Berfini
876029cbc9 Added back proximity sensor during active audio calls while using the earpiece 2024-07-15 16:10:43 +02:00
Sylvain Berfini
e9f7e4dcf5 Added missing cap sentences flag to display name field in third party account login 2024-07-15 16:10:43 +02:00
Sylvain Berfini
82071985bc Added back secure flag to prevent screenshots 2024-07-15 16:10:43 +02:00
Sylvain Berfini
10b7545807 Using OpenID library fetchFromIssuer instead of handling well-known manually 2024-07-15 16:10:43 +02:00
Sylvain Berfini
ccd53b74db Moved file & media viewer in separated activities 2024-07-15 16:10:43 +02:00
Sylvain Berfini
eb0748df7f Also close drawer menu when clicking on brand icon 2024-07-15 16:10:43 +02:00
Sylvain Berfini
ecad8fbdce Updated third party SIP account login to allow for SIP identity with a different domain for proxy + bearer auth 2024-07-15 16:10:43 +02:00
Sylvain Berfini
655cc8c291 Conversation performances improvements 2024-07-15 16:10:43 +02:00
Sylvain Berfini
889b98db1e Refactored toasts 2024-07-15 16:10:43 +02:00
Sylvain Berfini
465201010d Added missing toasts 2024-07-15 16:10:43 +02:00
Sylvain Berfini
b3ef701661 Added back .debug package name suffix for debug builds (configurable) 2024-07-15 16:10:43 +02:00
Sylvain Berfini
d062910133 Added dialog asking user whether to open or export file that app can't display 2024-07-15 16:10:43 +02:00
Sylvain Berfini
405596d291 Added multi-accounts notifications top bar 2024-07-15 16:10:43 +02:00
Sylvain Berfini
63b06ed1fa Improved audio file duration label color 2024-07-15 16:10:43 +02:00
Sylvain Berfini
bb844739c1 Display audio files duration as well 2024-07-15 16:10:43 +02:00
Sylvain Berfini
4fe0487eaf Fixed display issue in media grid 2024-07-15 16:10:43 +02:00
Sylvain Berfini
121f400732 Keep displaying contact's trust level even if e2e encryption isn't mandatory 2024-07-15 16:10:43 +02:00
Sylvain Berfini
1a5bc838b9 Fixed focus issue when entering a conversation after filtering conversations list 2024-07-15 16:10:43 +02:00
Sylvain Berfini
552e158979 Fixed expiration time in bearer tokens 2024-07-15 16:10:43 +02:00
Sylvain Berfini
d8f0338f7c Improved media grid, making sub-sections by months 2024-07-15 16:10:43 +02:00
Sylvain Berfini
54a775e0fd Focus subject field of create conversation/group call dialog 2024-07-15 16:10:43 +02:00
Sylvain Berfini
9c7739cf6c Using real SDK API to know if account has configured IM encryption as mandatory or not 2024-07-15 16:10:43 +02:00
Sylvain Berfini
22254b7846 Added very simple media player for audio files 2024-07-15 16:10:43 +02:00
Sylvain Berfini
fde0ca60a1 Display audio files as well as media & images 2024-07-15 16:10:43 +02:00
Sylvain Berfini
faf20eb369 Fixed contact discard changes dialog showing up if no changes were made 2024-07-15 16:10:43 +02:00
Sylvain Berfini
68f2535072 Added confirmation dialog before removing contact from list 2024-07-15 16:10:43 +02:00
Sylvain Berfini
9df97d7594 Added splashscreen branding image 2024-07-15 16:10:43 +02:00
Sylvain Berfini
efbcfee316 Layouts cleanup 2024-07-15 16:10:43 +02:00
Sylvain Berfini
c9c362d570 Fixed scheduled meetings duration 2024-07-15 16:10:43 +02:00
Sylvain Berfini
65a3d0520f Added missing content descriptions 2024-07-15 16:10:43 +02:00
Sylvain Berfini
86beb60507 Fixed hardcoded french text 2024-07-15 16:10:43 +02:00
Sylvain Berfini
78052cae12 Code cleanup, removed unused resources, fixed some colors 2024-07-15 16:10:43 +02:00
Sylvain Berfini
65595a1d51 Updated GMS & Kotlin versions 2024-07-15 16:10:43 +02:00
Sylvain Berfini
2b315cd0e4 Fixed issues related to multi call when one of them is a conference 2024-07-15 16:10:43 +02:00
Sylvain Berfini
b880578f99 Improved show video stats in call condition 2024-07-15 16:10:43 +02:00
Sylvain Berfini
3343b728fa Added no participants selected placeholder 2024-07-15 16:10:43 +02:00
Sylvain Berfini
3b588f467a Allow to cancel meeting removal by clicking outside of dialog 2024-07-15 16:10:43 +02:00
Sylvain Berfini
fec0766501 Improved SSO error log + added retry using well-known 2024-07-15 16:10:43 +02:00
Sylvain Berfini
ffa9a909a9 Bumped dependencies 2024-07-15 16:10:43 +02:00
Sylvain Berfini
507fb8a3ce Reworked auth requested callback & SSO activity to handle Bearer authentication requests 2024-07-15 16:10:43 +02:00
Sylvain Berfini
ebf9fa9145 Fixed waiting room joining screen when rotating device 2024-07-15 16:10:43 +02:00
Sylvain Berfini
9ca6978b7c Fixed issue when trying to open a file received by chat if the in-app viewer doesn't support it 2024-07-15 16:10:43 +02:00
Sylvain Berfini
d55c60cd98 Fixed display issue when switching between accounts if one of them isn't registered 2024-07-15 16:10:43 +02:00
Sylvain Berfini
a9c28aa1aa Fixed status bar color in dark mode when using a non-default theme 2024-07-15 16:10:43 +02:00
Sylvain Berfini
2cef97bce1 Improved a bit conversation participants mass removal 2024-07-15 16:10:43 +02:00
Sylvain Berfini
ed541bf5c0 Hide horizontal scroll bar in participants selection fragments 2024-07-15 16:10:43 +02:00
Sylvain Berfini
107388584b Also updated conversations participants selection to handle removal from add participant(s) fragment 2024-07-15 16:10:43 +02:00
Sylvain Berfini
1719a57cdc Updated meetings participants picker 2024-07-15 16:10:43 +02:00
Sylvain Berfini
b4d25b0e6e Moved click to add participants label in meeting edit fragment to top of participants list + prepend newly added participants in picker 2024-07-15 16:10:43 +02:00
Sylvain Berfini
b4baddcc5b Added shortcut to account profile mode fragment from plain text conversation 2024-07-15 16:10:43 +02:00
Sylvain Berfini
cf7dbb7f61 Added fake implem for Account.isEndToEndEncryptionMandatory() + possibility to change mode 2024-07-15 16:10:43 +02:00
Sylvain Berfini
7633af198a Simplified themes 2024-07-15 16:10:43 +02:00
Sylvain Berfini
20f4a072c4 Also tint app icon in drawer menu 2024-07-15 16:10:43 +02:00
Sylvain Berfini
0c7a6bb3b3 Added missing tint on mountains drawable 2024-07-15 16:10:43 +02:00
Sylvain Berfini
ca475c0ab8 Enabled FEC by default + added setting 2024-07-15 16:10:42 +02:00
Sylvain Berfini
7e06c5d85a Added error toast with reason when call ends in error 2024-07-15 16:10:42 +02:00
Sylvain Berfini
0239d3a3a6 Fixed switch to grid layout in conference if there is exactly 6 participants 2024-07-15 16:10:42 +02:00
Sylvain Berfini
0dbd403d5b Check if current fragment matches before going back 2024-07-15 16:10:42 +02:00
Sylvain Berfini
39fba60066 Fixed groip chat room shortcuts generated avatar not using initials 2024-07-15 16:10:42 +02:00
Sylvain Berfini
8e2fc8b6cd Improved account profile if push notifications aren't available on the device (or not configured in app) 2024-07-15 16:10:42 +02:00
Sylvain Berfini
74ad52e0f5 Also fixed voice recording in progress gradient color when a theme is being used & removed orange_main_400 that wasn't used 2024-07-15 16:10:42 +02:00
Sylvain Berfini
42e2a1c040 Updated mountains drawable to properly handle main color change 2024-07-15 16:10:42 +02:00
Sylvain Berfini
f532214e1a Add already selected participants to list when adding new participants 2024-07-15 16:10:42 +02:00
Sylvain Berfini
e9f0bed2d2 Close search bar when clicking on clear filter button if filter is empty 2024-07-15 16:10:42 +02:00
Sylvain Berfini
00b92a61b4 Improved app fluidity 2024-07-15 16:10:42 +02:00
Sylvain Berfini
41de644945 Trying to prevent getting stuck in permissions fragment 2024-07-15 16:10:42 +02:00
Sylvain Berfini
fb7c3a3cdc Reload accounts when applying remote provisioning 2024-07-15 16:10:42 +02:00
Sylvain Berfini
9daa433c44 Display week info above meeting in list 2024-07-15 16:10:42 +02:00
Sylvain Berfini
177eb186a5 Improved meetings list design 2024-07-15 16:10:42 +02:00
Sylvain Berfini
d848622ace Removed avatar generation for conversations/meetings depending on members avatars 2024-07-15 16:10:42 +02:00
Sylvain Berfini
81af7a8bc0 Update new conversation / meeting icons 2024-07-15 16:10:42 +02:00
Sylvain Berfini
6856a399c5 Updated meeting icon, now using video_conference from phosphoricons 2024-07-15 16:10:42 +02:00
Sylvain Berfini
84f7af8d13 Disable change audio output device until call is in Ringing state 2024-07-15 16:10:42 +02:00
Sylvain Berfini
2070f8fb08 Fixed dialog theme status bar color + added more colors (for fun) 2024-07-15 16:10:42 +02:00
Sylvain Berfini
fbf2d39640 Set front facing camera as default one each time last calls end 2024-07-15 16:10:42 +02:00
Sylvain Berfini
6b0bae9c3d Increased trust/unsafe avatar overlay size for small/medium avatars so they are more visible 2024-07-15 16:10:42 +02:00
Sylvain Berfini
bd197bd219 Fixed issue with full screen 2024-07-15 16:10:42 +02:00
Sylvain Berfini
5300cf698d Disable full screen mode when remote device stops sending it's video 2024-07-15 16:10:42 +02:00
Sylvain Berfini
f048b895b5 Added main color theme selector in settings 2024-07-15 16:10:42 +02:00
Sylvain Berfini
fdafcfd7a4 Added dialog to confirm removing a participant from conference 2024-07-15 16:10:42 +02:00
Sylvain Berfini
79961739e0 Improved managed own calls permission request 2024-07-15 16:10:42 +02:00
Sylvain Berfini
33865b469c Prepared listener when clicking on disabled conversation warning 2024-07-15 16:10:42 +02:00
Sylvain Berfini
502c7f9fc1 Fixed crash in NotificationBroadcastReceiver if coreContext isn't created when intent is received 2024-07-15 16:10:42 +02:00
Sylvain Berfini
9c855ef923 Improved Linphone login adding automatically default domain to username if not entered by user 2024-07-15 16:10:42 +02:00
Sylvain Berfini
6ceee7fdb7 Made welcome pages scrollable, required for small screens 2024-07-15 16:10:42 +02:00
Sylvain Berfini
498b8435bf Hide contact's devices & trust for third party accounts 2024-07-15 16:10:42 +02:00
Sylvain Berfini
1578e76700 Refactored code a bit to make it simplier 2024-07-15 16:10:42 +02:00
Sylvain Berfini
fbb59cbc2a When upgrading from previous version, do not show AssitantLanding page after WelcomeActivity and/or PermissionsFragment 2024-07-15 16:10:42 +02:00
Sylvain Berfini
23c3a63aed Improved toast layout for small screen devices 2024-07-15 16:10:42 +02:00
Sylvain Berfini
cacaf29771 Should fix black avatar for chat rooms & conferences if there is a picture set for a contact but that file can't be read for some reason 2024-07-15 16:10:42 +02:00
Sylvain Berfini
559397d420 Added support for JSON file in plain text file viewer 2024-07-15 16:10:42 +02:00
Sylvain Berfini
a64db777d9 Various fixes for crash reported on Crashlytics 2024-07-15 16:10:42 +02:00
Sylvain Berfini
aa36129053 Fixed mute microphone in waiting room not applied once conference has been joined 2024-07-15 16:10:42 +02:00
Sylvain Berfini
1aeb917d62 Added download & apply button next to remote provisioning URL advanced settings 2024-07-15 16:10:42 +02:00
Sylvain Berfini
486f905d65 Added data sync keep alive service for third party accounts without push notifications 2024-07-15 16:10:42 +02:00
Sylvain Berfini
7f1dc95cfc Fluidify navigation if SDK is under burst 2024-07-15 16:10:42 +02:00
Sylvain Berfini
ace9e02133 Added clear cache files menu in help 2024-07-15 16:10:42 +02:00
Sylvain Berfini
770e816468 Hide file size in chat bubble when 0 (for outgoing messages) 2024-07-15 16:10:42 +02:00
Sylvain Berfini
bbe26ec35b Renamed a few classes related to call logs for better understanding 2024-07-15 16:10:42 +02:00
Sylvain Berfini
c002ea3205 Added missing license header on some files 2024-07-15 16:10:42 +02:00
Sylvain Berfini
0fe03e2eec Removed SSO fragment, only keep activity 2024-07-15 16:10:42 +02:00
Sylvain Berfini
5aa949b42c Added account setting to enable/disable CPIM in basic chat rooms when in interop mode 2024-07-15 16:10:42 +02:00
Sylvain Berfini
11cab8c4a4 Changes in gradle dependencies 2024-07-15 16:10:42 +02:00
Sylvain Berfini
7c94bd8b67 Layout improvements for phones with small width such as Samsung S22 2024-07-15 16:10:42 +02:00
Sylvain Berfini
4f25dfb33c Fixed participants list in conversation info scrolling 2024-07-15 16:10:42 +02:00
Sylvain Berfini
b96e5e8121 Using onlyDisplaySipUriUsername preference 2024-07-15 16:10:42 +02:00
Sylvain Berfini
2d1479a64f Fixed camera switch button not visible when alone in a conf 2024-07-15 16:10:42 +02:00
Sylvain Berfini
a13b46bd2c Bumped dependencies 2024-07-15 16:10:42 +02:00
Sylvain Berfini
c4358f20f5 Fixed doubled stack conversations list fragment causing file sharing from outside not working if Linphone opened and already in conversations list 2024-07-15 16:10:42 +02:00
Sylvain Berfini
fd7700a819 Another attempt to fix duplicated messages sometimes 2024-07-15 16:10:42 +02:00
Sylvain Berfini
48ec21def4 Fixed missing end day date picker when all day meeting toggle is ON 2024-07-15 16:10:42 +02:00
Sylvain Berfini
4ce7c90577 Updated style of 'do SAS validation again' in-call button 2024-07-15 16:10:42 +02:00
Sylvain Berfini
896ede0b88 Fixed conf stuck in full screen mode when last participant leaves 2024-07-15 16:10:42 +02:00
Sylvain Berfini
ed05b648d1 Updated active speaker portrait layout + added landscape layout 2024-07-15 16:10:42 +02:00
Sylvain Berfini
b6001544f1 Added back sip-linphone & sips-linphone URI handlers + added linphone-sso URI handler & activity + factorized showToast methods 2024-07-15 16:10:42 +02:00
Sylvain Berfini
7607607857 Fixed video direction when toggling camera during call 2024-07-15 16:10:42 +02:00
Sylvain Berfini
0c261ca4cb Improved opened conversation view when a lot of unread messages are in it 2024-07-15 16:10:42 +02:00
Sylvain Berfini
4eb11a05bc Another fix for in-call navigation 2024-07-15 16:10:42 +02:00
Sylvain Berfini
e616582162 Started advanced settings, currently only contains remote provisioning URI 2024-07-15 16:10:42 +02:00
Sylvain Berfini
7b13bb3bd2 Fixed toast icon for deletion actions 2024-07-15 16:10:42 +02:00
Sylvain Berfini
b16ca7325e Fixed crash when merging calls into conference 2024-07-15 16:10:42 +02:00
Sylvain Berfini
81bb7156dc Fixed merge calls into conference dialog label 2024-07-15 16:10:42 +02:00
Sylvain Berfini
54818c51b4 Prevents duplicated message when entering a conversation while SDK is still doing aggregation 2024-07-15 16:10:42 +02:00
Sylvain Berfini
9e3c679665 Fixed permissions layout issue in landscape + assistant landing page on narrow screen 2024-07-15 16:10:42 +02:00
Sylvain Berfini
637d0b9cfb Fixed weird video preview shape while in PiP 2024-07-15 16:10:42 +02:00
Sylvain Berfini
61fe57628f Improved PiP 2024-07-15 16:10:42 +02:00
Sylvain Berfini
8c7c4bee0d Update call foreground service types when disabling/enabling video 2024-07-15 16:10:42 +02:00
Sylvain Berfini
7e0208f8e8 Reversed media/documents list order to have most recents ones at the top 2024-07-15 16:10:42 +02:00
Sylvain Berfini
680c6877e4 Do not start audio calls with video enabled but in inactive direction 2024-07-15 16:10:42 +02:00
Sylvain Berfini
16fa960a0b Do not notify received reactions for messages we didn't sent + no longer repeat person reacting name in notification body 2024-07-15 16:10:42 +02:00
Sylvain Berfini
c0b0ef66ff Trying to fix lost remote video upon rotation 2024-07-15 16:10:42 +02:00
Sylvain Berfini
3e845b32b6 Improved meeting room fragment, added cancel button 2024-07-15 16:10:42 +02:00
Sylvain Berfini
b0a05b5905 Updated gradle, fixed configuration migration 2024-07-15 16:10:42 +02:00
Sylvain Berfini
afc2017e1e Fixed video setting hidden when disabled 2024-07-15 16:10:42 +02:00
Sylvain Berfini
5a37f15bc7 Fixed issues with incoming call notification not being dismissed in some cases + fixed toast in case of remote provisioning issue 2024-07-15 16:10:42 +02:00
Sylvain Berfini
6e97466a86 Factorized code for call.terminate()/call.decline() 2024-07-15 16:10:42 +02:00
Sylvain Berfini
0d96010865 Started merge calls into conference 2024-07-15 16:10:42 +02:00
Sylvain Berfini
fea42aba3b Display our own video (if enabled) while waiting for other participants to join the conference 2024-07-15 16:10:42 +02:00
Sylvain Berfini
065cdfa8c1 Fixed video preview moving depending on remote end video being sent or not 2024-07-15 16:10:42 +02:00
Sylvain Berfini
16d15242c7 Improved connection to meeting waiting screen 2024-07-15 16:10:42 +02:00
Sylvain Berfini
4d8b74ee41 Fixed participant added/removed toast 2024-07-15 16:10:41 +02:00
Sylvain Berfini
f5bdaf85fd Improved contacts display, fixed issue with LDAP results 2024-07-15 16:10:41 +02:00
Sylvain Berfini
9c1b9b2939 Using new APIs to be able to make asymetrical video calls 2024-07-15 16:10:41 +02:00
Sylvain Berfini
d6ea531cea Moved some code around & added invite participant into conf 2024-07-15 16:10:41 +02:00
Sylvain Berfini
d19f08cf86 Started to add chat during call 2024-07-15 16:10:41 +02:00
Sylvain Berfini
e805fbc7f3 Prevent blue lining & trusted badge on conference avatar 2024-07-15 16:10:41 +02:00
Sylvain Berfini
0db1754603 Change contacts list filter function depending on whether the default account is a Linphone account in secure mode or something else 2024-07-15 16:10:41 +02:00
Sylvain Berfini
f70ab87952 Started admin view of conference participants list 2024-07-15 16:10:41 +02:00
Sylvain Berfini
561216320f Fixed empty bubble when shared file is considered as plain/text 2024-07-15 16:10:41 +02:00
Sylvain Berfini
1ae85611bf Fixed margin around images with large ratio between w/h in chat bubbles 2024-07-15 16:10:41 +02:00
Sylvain Berfini
cab66e844b Added missing cross to close number/address picker dialog 2024-07-15 16:10:41 +02:00
Sylvain Berfini
d693cfe58d Removed ringtone picker setting 2024-07-15 16:10:41 +02:00
Sylvain Berfini
86387ef5b8 Improved multi files chat bubble 2024-07-15 16:10:41 +02:00
Sylvain Berfini
0707e60c26 Reworked notifications channel, using incoming call one for playing ringtone instead of SDK 2024-07-15 16:10:41 +02:00
Sylvain Berfini
44457665ef Prevent release build from failing due to Android linter 2024-07-15 16:10:41 +02:00
Sylvain Berfini
19cee069ec Updated icons 2024-07-15 16:10:41 +02:00
Sylvain Berfini
9416305f61 Added locus ID management in conversation 2024-07-15 16:10:41 +02:00
Sylvain Berfini
7b115caf61 Added protobuf javalite dependency to parse native crash tombstone 2024-07-15 16:10:41 +02:00
Sylvain Berfini
7076acc540 Improved a few things related to conversation 2024-07-15 16:10:41 +02:00
Sylvain Berfini
8a2e2c074b Updated gradle version 2024-07-15 16:10:41 +02:00
Sylvain Berfini
e438617241 Improved multiple files upload/sending chat bubble 2024-07-15 16:10:41 +02:00
Sylvain Berfini
4098827253 Fixed square image in contact editor 2024-07-15 16:10:41 +02:00
Sylvain Berfini
945fd709d4 Fixed media list when VFS is enabled + removed debug logs and trying something else instead 2024-07-15 16:10:41 +02:00
Sylvain Berfini
30927ac6db Should prevent going to the wrong conversation when multiple message notifications are still visible 2024-07-15 16:10:41 +02:00
Sylvain Berfini
dd96cac3d0 Added temporary logs to help debug duplicated chat messages sometimes 2024-07-15 16:10:41 +02:00
Sylvain Berfini
b0f5141e7d Fixed crash when loading more data in conversation 2024-07-15 16:10:41 +02:00
Sylvain Berfini
578b372335 Fixed used of some interpretUrl method calls 2024-07-15 16:10:41 +02:00
Sylvain Berfini
a871fb971b Updated conference invitation chat message layout to add label/icon when meeting is updated/cancelled 2024-07-15 16:10:41 +02:00
Sylvain Berfini
b41bc3bb7d Updated message description depending on conference info state (cancelled, updated, etc...) 2024-07-15 16:10:41 +02:00
Sylvain Berfini
af8071bf0e Update chat rooms list when a message is deleted in a conversation 2024-07-15 16:10:41 +02:00
Sylvain Berfini
fcf2ffa39c Added dialog asking whether to cancel the meeting or not when deleting it 2024-07-15 16:10:41 +02:00
Sylvain Berfini
2505aad1e8 Prevent smiley/offline presence being briefly visible when switching conversation on tablet 2024-07-15 16:10:41 +02:00
Sylvain Berfini
5da4f748a3 Fixed operation in progress dialog background color transparency 2024-07-15 16:10:41 +02:00
Sylvain Berfini
63138e818c Added operation in progress dialog while upload logs 2024-07-15 16:10:41 +02:00
Sylvain Berfini
b3c31d14ad Use default string from resources if a contact's device has no name, updated TODOs & FIXMEs comments 2024-07-15 16:10:41 +02:00
Sylvain Berfini
57b6c5daa2 Disable recordings in call & conference if corePreferences related setting is disabled 2024-07-15 16:10:41 +02:00
Sylvain Berfini
8ec7542a60 Store and use 'do not display anymore' option of zrtp trust call dialog 2024-07-15 16:10:41 +02:00
Sylvain Berfini
d64f1e033b Improved some spinners display 2024-07-15 16:10:41 +02:00
Sylvain Berfini
6b9ca15dff Fixed dialplan prefix issue in account creator 2024-07-15 16:10:41 +02:00
Sylvain Berfini
2ca3d152d1 Restored initial login form on assistant landing page 2024-07-15 16:10:41 +02:00
Sylvain Berfini
5ab0b32705 Added log in new Message Waiting Indicator (MWI) account callback 2024-07-15 16:10:41 +02:00
Sylvain Berfini
5f82cf9066 Fixed scolling in contact editor while in landscape 2024-07-15 16:10:41 +02:00
Sylvain Berfini
46570a4152 Fixed ephemeral message conversation menu item not working 2024-07-15 16:10:41 +02:00
Sylvain Berfini
34c3dff137 Do ZRTP SAS validation again button now working 2024-07-15 16:10:41 +02:00
Sylvain Berfini
91e0cb5838 Fixes & improvements for incoming group calls 2024-07-15 16:10:41 +02:00
Sylvain Berfini
90a2c0539c Fixed conversation info toast + participants list not refreshed + events not appearing in messages list when going back 2024-07-15 16:10:41 +02:00
Sylvain Berfini
7f24902fc7 Fixed new call/chat search bar length in landscape 2024-07-15 16:10:41 +02:00
Sylvain Berfini
81f0a9515f Hide some parts of the UI depending on Core or account configuration 2024-07-15 16:10:41 +02:00
Sylvain Berfini
7d7b037741 Added some customization settings 2024-07-15 16:10:41 +02:00
Sylvain Berfini
37af11d3e1 Improved VFS class code a bit 2024-07-15 16:10:41 +02:00
Sylvain Berfini
c35025aedb Added save on disk for plain text file, reworked share button to use native sharing 2024-07-15 16:10:41 +02:00
Sylvain Berfini
70e25b7792 Added button in debug help fragment to display config file 2024-07-15 16:10:41 +02:00
Sylvain Berfini
d91093cf01 Added VFS & security settings section 2024-07-15 16:10:41 +02:00
Sylvain Berfini
8226f6e1b3 Factorized avatar + presence & trust badges 2024-07-15 16:10:41 +02:00
Sylvain Berfini
d023519cd8 Use linphone API to get contact's devices & trust 2024-07-15 16:10:41 +02:00
Sylvain Berfini
2de4067b03 Added missing contacts settings expand value initialization 2024-07-15 16:10:41 +02:00
Sylvain Berfini
44009cfd92 Do not notify early contacts list after first time 2024-07-15 16:10:41 +02:00
Sylvain Berfini
ddcd7d7dc1 Removed emoji compat library, only use it for picker 2024-07-15 16:10:41 +02:00
Sylvain Berfini
3e6c856ee5 Fixed build with latest SDK 2024-07-15 16:10:41 +02:00
Sylvain Berfini
29218a5311 Hide 'mode selector' in account profile for third party SIP accounts + fixed issues when switching between a linphone account and a third party one 2024-07-15 16:10:41 +02:00
Sylvain Berfini
dcca7d6952 Started CardDAV & LDAP configuration fragments 2024-07-15 16:10:41 +02:00
Sylvain Berfini
deaf9cd0db Fixed contacts list 'header' for emojis 2024-07-15 16:10:41 +02:00
Sylvain Berfini
38eeb56741 Keep synchronizing native address book even now that imported friends are stored in database 2024-07-15 16:10:41 +02:00
Sylvain Berfini
9b61700b79 Improved emoji library loading wait 2024-07-15 16:10:41 +02:00
Sylvain Berfini
c90961408f Added bundle mode account setting 2024-07-15 16:10:41 +02:00
Sylvain Berfini
0fac6150c7 Added support for screen sharing using new SDK APIs 2024-07-15 16:10:41 +02:00
Sylvain Berfini
719fa62752 Trying to wait for emoji lib to have loaded before displaying account display name, in case it contains emoji(s) 2024-07-15 16:10:41 +02:00
Sylvain Berfini
f19fbb66e7 Updated gradle 2024-07-15 16:10:41 +02:00
Sylvain Berfini
8aac439a5b Added landscape version of contact editor + fixed issue in dark mode 2024-07-15 16:10:41 +02:00
Sylvain Berfini
1d7f531f6a Added missing trust badge in meetings' participants list 2024-07-15 16:10:41 +02:00
Sylvain Berfini
eefbec5358 Fixed dark mode issues 2024-07-15 16:10:41 +02:00
Sylvain Berfini
3aaea594f4 Prevent typing message after starting voice recording in chat by disabling field & hiding keyboard 2024-07-15 16:10:41 +02:00
Sylvain Berfini
40610fd98f Trim fields in assistant 2024-07-15 16:10:41 +02:00
Sylvain Berfini
9a1ca386ca Store native contacts copy inside app 2024-07-15 16:10:41 +02:00
Sylvain Berfini
3a2d85265d Small improvements 2024-07-15 16:10:41 +02:00
Sylvain Berfini
01c69b9396 Fixed dark theme issue 2024-07-15 16:10:41 +02:00
Sylvain Berfini
d61f94c42e Renamed a few strings 2024-07-15 16:10:41 +02:00
Sylvain Berfini
70d128edab Finished French translations 2024-07-15 16:10:41 +02:00
Sylvain Berfini
a3966a72ad Fixed issues with participant video window id in confernece 2024-07-15 16:10:41 +02:00
Sylvain Berfini
5c4d73f34a Started french translation 2024-07-15 16:10:41 +02:00
Sylvain Berfini
d2b8689f48 Fixed call history list cells not refreshed when native contacts are loaded or presence is received 2024-07-15 16:10:41 +02:00
Sylvain Berfini
c9eb856f19 Improved stats layouts, video full screen & bottom sheets animations 2024-07-15 16:10:40 +02:00
Sylvain Berfini
7ba19364b9 Improved parsing of account settings + navigate out of resuming fragment if no longer available due to config changes 2024-07-15 16:10:40 +02:00
Sylvain Berfini
dce27530e0 Fixd margin issue in call stats layout 2024-07-15 16:10:40 +02:00
Sylvain Berfini
fd8b8b5c29 Hide non-scheduled conferences from meetings list 2024-07-15 16:10:40 +02:00
Sylvain Berfini
3b626bd0a6 Updated media encryption statistics layout 2024-07-15 16:10:40 +02:00
Sylvain Berfini
37bf87e23e Switched from findLastCompletelyVisibleItemPosition() to findLastVisibleItemPosition() to prevent display issue if latest message is very long 2024-07-15 16:10:40 +02:00
Sylvain Berfini
93f1ef1aca Improved update password dialog layout 2024-07-15 16:10:40 +02:00
Sylvain Berfini
4536f917f6 Added statistics bottom sheet to conference 2024-07-15 16:10:40 +02:00
Sylvain Berfini
d7d6767361 Added joining indeterminate progress bar/label & paused icon/label on conference participants cells 2024-07-15 16:10:40 +02:00
Sylvain Berfini
a707c6c988 Added auth requested dialog 2024-07-15 16:10:40 +02:00
Sylvain Berfini
8750c2da55 Updated meeting shedule layout 2024-07-15 16:10:40 +02:00
Sylvain Berfini
308aff5392 Started call stats 2024-07-15 16:10:40 +02:00
Sylvain Berfini
a7c63c748a Remove reply action in message notification if account is not in secure mode and message is received in a unsecure conversation 2024-07-15 16:10:40 +02:00
Sylvain Berfini
8e104114f7 Enable bundle mode by default for third party SIP accounts as well 2024-07-15 16:10:40 +02:00
Sylvain Berfini
bf8d2de176 Slightly improved unsecured conversation list cell layout 2024-07-15 16:10:40 +02:00
Sylvain Berfini
7734e47742 Started inConference/joining states of participant device in conference 2024-07-15 16:10:40 +02:00
Sylvain Berfini
913d126203 Automatically switch to active speaker layout when more participant devices than authorized by grid layout are present 2024-07-15 16:10:40 +02:00
Sylvain Berfini
146b9d0cdf Fixed crash 2024-07-15 16:10:40 +02:00
Sylvain Berfini
af1ee922ad Updated SSO 2024-07-15 16:10:40 +02:00
Sylvain Berfini
06074f0490 Fixed disabled buttons color in call 2024-07-15 16:10:40 +02:00
Sylvain Berfini
e82f42e51a Improved conference pause/resume + hide video surfaces when call or conference is paused 2024-07-15 16:10:40 +02:00
Sylvain Berfini
f91252f678 Revert part of previous changes, causes issues 2024-07-15 16:10:40 +02:00
Sylvain Berfini
625edfe33a Various UI fixes 2024-07-15 16:10:40 +02:00
Sylvain Berfini
780c2f55dc Added onTrimMemory callback + trying to properly shut down Core when latest activity is being destroyed 2024-07-15 16:10:40 +02:00
Sylvain Berfini
897312831e Trying to cache native friend requests 2024-07-15 16:10:40 +02:00
Sylvain Berfini
8746a2646a Do not show when for push received notification 2024-07-15 16:10:40 +02:00
Sylvain Berfini
82d6d37fd7 Make sure to always search contacts using a cleaned SIP address + improved Android DB search for SIP address + delay loading contacts to after an account was successfully registered 2024-07-15 16:10:40 +02:00
Sylvain Berfini
aa36235ab1 Should speed up a little the first display when app starts 2024-07-15 16:10:40 +02:00
Sylvain Berfini
f9667ff2e4 Removed smooth scrolling to go to bottom 2024-07-15 16:10:40 +02:00
Sylvain Berfini
59ece2f8a8 Properly integrated SSO in app 2024-07-15 16:10:40 +02:00
Sylvain Berfini
9c3102392b Improved push notification received notification by inheriting PushService from SDK 2024-07-15 16:10:40 +02:00
Sylvain Berfini
e618992fb5 Updated contact editor to allow creating a contact with only a organization name 2024-07-15 16:10:40 +02:00
Sylvain Berfini
c7b4c14d66 Improved video activation while in audio only conference 2024-07-15 16:10:40 +02:00
Sylvain Berfini
aedd1a2577 Fixed issue with account login 2024-07-15 16:10:40 +02:00
Sylvain Berfini
f93e771bab Changed SSO redirect URI scheme, updated default SSO URI, delete saved config in case of token refresh error before retrying 2024-07-15 16:10:40 +02:00
Sylvain Berfini
bfd5a8f6fa Split login step to allow detecting if using digest or SSO authentication 2024-07-15 16:10:40 +02:00
Sylvain Berfini
935d134bbd Show paused icon instead of call quality indicator while paused or paused by remote 2024-07-15 16:10:40 +02:00
Sylvain Berfini
f9ae9985a4 Created landscape versions of meeting waiting room & conference menu 2024-07-15 16:10:40 +02:00
Sylvain Berfini
03299bae16 Fixed missing e2e encrypted header 2024-07-15 16:10:40 +02:00
Sylvain Berfini
fc82c8bcf6 Slightly improved disabled plain text chat room warning display 2024-07-15 16:10:40 +02:00
Sylvain Berfini
3882c57cfc Fixed gif display in media grid 2024-07-15 16:10:40 +02:00
Sylvain Berfini
d9b0d6c740 Factorized code 2024-07-15 16:10:40 +02:00
Sylvain Berfini
8204f6d2da Hide emoji reactions & reply action from long press menu on chat message in not encrypted conversation (if account is in secure mode) 2024-07-15 16:10:40 +02:00
Sylvain Berfini
476aec1916 Use adapter & recyclerview to display media & documents in conversation 2024-07-15 16:10:40 +02:00
Sylvain Berfini
940937a9b7 Fixed broken media viewer 2024-07-15 16:10:40 +02:00
Sylvain Berfini
598ac6cbd3 Added documents menu (like media) 2024-07-15 16:10:40 +02:00
Sylvain Berfini
109b5e71e2 Updated schedule meeting layout 2024-07-15 16:10:40 +02:00
Sylvain Berfini
9872f505b6 Prevent duplicated participants in list during meeting schedule + prevent scheduling a meeting without subject or participant + added icon to remove participant from list 2024-07-15 16:10:40 +02:00
Sylvain Berfini
2483b7b6d0 Fixed media encryption status bottom sheet in landscape 2024-07-15 16:10:40 +02:00
Sylvain Berfini
56283a1480 Improved chat bubble touch area 2024-07-15 16:10:40 +02:00
Sylvain Berfini
8a01f30a8d Prevent e2e details modale to show up while scrolling 2024-07-15 16:10:40 +02:00
Sylvain Berfini
d429f181a0 Updated chat list cell layout 2024-07-15 16:10:40 +02:00
Sylvain Berfini
ab72e5eb62 Added conversation messages decorator in secured chat room to explain and show info on click 2024-07-15 16:10:40 +02:00
Sylvain Berfini
d77f51a5e2 Prevent replying & transfering messages to read only or disabled conversations 2024-07-15 16:10:40 +02:00
Sylvain Berfini
1697dc7f7b Removed toast no longer required 2024-07-15 16:10:40 +02:00
Sylvain Berfini
012b2419e5 Show ephemeral messages icon under conversation title if they are enabled 2024-07-15 16:10:40 +02:00
Sylvain Berfini
ef53eb62ae Changed troubleshooting toggle to only enable/disable printing logs to adb logcat 2024-07-15 16:10:40 +02:00
Sylvain Berfini
2ca3d017bf Updated conversation event design 2024-07-15 16:10:40 +02:00
Sylvain Berfini
6faff7f780 Fixed going back to default fragment when rotating the device 2024-07-15 16:10:40 +02:00
Sylvain Berfini
91ec675cbe Refactored Media & File viewers 2024-07-15 16:10:40 +02:00
Sylvain Berfini
c71af9f23a Uniformized popup menus 2024-07-15 16:10:40 +02:00
Sylvain Berfini
30ab1c2196 Various fixes & improvements 2024-07-15 16:10:40 +02:00
Sylvain Berfini
3810ab4ae9 Added medias list in conversation 2024-07-15 16:10:40 +02:00
Sylvain Berfini
db486360cc Update cell signal icon & color depending on call current quality 2024-07-15 16:10:40 +02:00
Sylvain Berfini
879b6b3b7e Added empty contacts settings (for now) 2024-07-15 16:10:40 +02:00
Sylvain Berfini
3fb8b77f87 Improved contact save changes & join meeting buttons 2024-07-15 16:10:40 +02:00
Sylvain Berfini
2824c1a3f8 Fixed conference layouts not visible + trying to improve navigation 2024-07-15 16:10:40 +02:00
Sylvain Berfini
e994edbf0a Added missing long click listener for generic file chat bubble 2024-07-15 16:10:40 +02:00
Sylvain Berfini
1c7316408d Started to add content descriptions 2024-07-15 16:10:40 +02:00
Sylvain Berfini
0105f1d669 Code improvements 2024-07-15 16:10:40 +02:00
Sylvain Berfini
32145980f4 Fixed missing 'play' icon above video preview if it is the only file in a bubble + added video duration if info is available + improved onLongClick on bubbles 2024-07-15 16:10:40 +02:00
Sylvain Berfini
c62ac4359a Switched mute button background to red when selected 2024-07-15 16:10:40 +02:00
Sylvain Berfini
d002d308c4 Show all conversations but disable non-encrypted ones if in secure mode 2024-07-15 16:10:40 +02:00
Sylvain Berfini
01361bfcaa Fixed top header in call/conference for both short/long name/title 2024-07-15 16:10:40 +02:00
Sylvain Berfini
25a0f4b65a Added edit meeting fragment 2024-07-15 16:10:40 +02:00
Sylvain Berfini
65f3dd896c Added code to create group call from existing chat room 2024-07-15 16:10:40 +02:00
Sylvain Berfini
25ab474fba Added button to do ZRTP SAS validation again 2024-07-15 16:10:40 +02:00
Sylvain Berfini
e19e9bfdc7 Only show 'call can be trusted' toast when SAS is validated 2024-07-15 16:10:40 +02:00
Sylvain Berfini
523b762cac Fixed issues related to contacts being added/edited/removed 2024-07-15 16:10:40 +02:00
Sylvain Berfini
3864d54936 Reworked conference/incoming/outgoing/ended call fragments to match changes made to active call fragment 2024-07-15 16:10:40 +02:00
Sylvain Berfini
58e41d99c9 Animate caret to handle and back while in call 2024-07-15 16:10:40 +02:00
Sylvain Berfini
1c24c805df Started audio only layout for conference 2024-07-15 16:10:40 +02:00
Sylvain Berfini
784803336c Trying new in-call layout 2024-07-15 16:10:40 +02:00
Sylvain Berfini
72daf9ebd2 Hide imdn status for received messages in 1-1 conversation + always show date/time & imdn status even for grouped messages 2024-07-15 16:10:40 +02:00
Sylvain Berfini
940a6b0577 Fixed navigation status bar color for file viewer fragment 2024-07-15 16:10:40 +02:00
Sylvain Berfini
ed9df940af Fixed mute mic while in conference 2024-07-15 16:10:40 +02:00
Sylvain Berfini
d32c6f70a1 Hide active speaker miniature + fix display name missing + added missing mute indicator + added avatar if not sending video 2024-07-15 16:10:40 +02:00
Sylvain Berfini
bd2936b05e Fixed weird scrolling position in conversation after going into info and going back 2024-07-15 16:10:39 +02:00
Sylvain Berfini
52e7acb4ee Reworked a few things to speed up app cold startup 2024-07-15 16:10:39 +02:00
Sylvain Berfini
1734d11639 Fixed conversation menu icon size + prevent switching to mosaic mode when more than 6 participants while in conference 2024-07-15 16:10:39 +02:00
Sylvain Berfini
318a487e10 Restored start-up screen 2024-07-15 16:10:39 +02:00
Sylvain Berfini
ddd614acd0 Trying to speed up app startup & prevent blank screen 2024-07-15 16:10:39 +02:00
Sylvain Berfini
b1d0554a07 Minor update 2024-07-15 16:10:39 +02:00
Sylvain Berfini
254d8619fe Make sure friend lists subscruptions are enabled for sip.linphone.org accounts and disabled for others 2024-07-15 16:10:39 +02:00
Sylvain Berfini
44af8bb340 Added start group call feature from start call fragment 2024-07-15 16:10:39 +02:00
Sylvain Berfini
260ad798ed Improved message regexp 2024-07-15 16:10:39 +02:00
Sylvain Berfini
a024dd2278 Fixed received/sent audio files handled as voice recordings 2024-07-15 16:10:39 +02:00
Sylvain Berfini
ce1d3d4807 Added audio route selection to conference waiting room fragment 2024-07-15 16:10:39 +02:00
Sylvain Berfini
4697af6c27 Fixed devices order depending on conference layout + set/restore navigation status bar color when going/leaving waiting room 2024-07-15 16:10:39 +02:00
Sylvain Berfini
247f763c11 Improved scroll to today in meetings list 2024-07-15 16:10:39 +02:00
Sylvain Berfini
c8319ed014 Improved margins between chat bubbles 2024-07-15 16:10:39 +02:00
Sylvain Berfini
75d1f719ae Fixed favorites caret not switching direction in landscape + conversation title in landscape not displayed properly + show suggestions SIP addresses domain instead of just username + remove default account address from suggestions 2024-07-15 16:10:39 +02:00
Sylvain Berfini
98a3c89435 Fixed recyclerview header not colliding in some special case 2024-07-15 16:10:39 +02:00
Sylvain Berfini
9fc6a1eb57 Fixed wrong position of today indicator in meetings list 2024-07-15 16:10:39 +02:00
Sylvain Berfini
e6d5e35f29 Updated picto in calls history when call log was for a conference 2024-07-15 16:10:39 +02:00
Sylvain Berfini
8870564066 Go to waiting room when clicking on call icon in calls history if conference call log + enabled video by default in waiting room when possible 2024-07-15 16:10:39 +02:00
Sylvain Berfini
f23510da3a More work for having both grid & active speaker layout but not at the same time 2024-07-15 16:10:39 +02:00
Sylvain Berfini
bf99bd402f Updated gradle to 8.2.2 2024-07-15 16:10:39 +02:00
Sylvain Berfini
7eb756cae6 Started active speaker layout 2024-07-15 16:10:39 +02:00
Sylvain Berfini
51c6037f3f Fixed white screen when starting the app sometimes + conversations list fragment stacked twice when opening app from chat notification 2024-07-15 16:10:39 +02:00
Sylvain Berfini
d2dc99d7a1 Using newly added callback to properly handle presence & contacts reload 2024-07-15 16:10:39 +02:00
Sylvain Berfini
1299ff0f05 Added back some linphonerc_default values that were removed from 5.2 version 2024-07-15 16:10:39 +02:00
Sylvain Berfini
b82f8aed2b Fixed meeting invitation participant label with only 1 participant 2024-07-15 16:10:39 +02:00
Sylvain Berfini
fcbe629e48 Make sure there is always a 'today' indicator in meetings list 2024-07-15 16:10:39 +02:00
Sylvain Berfini
a198fab204 Fixed text watcher for @participant not working after leaving & resuming conversation 2024-07-15 16:10:39 +02:00
Sylvain Berfini
a50b74d042 Hidden some options of conversation long press menu if read only 2024-07-15 16:10:39 +02:00
Sylvain Berfini
e4a85985c2 Removed some logs 2024-07-15 16:10:39 +02:00
Sylvain Berfini
d6630e05d4 Enabled auto policy for hardware video codecs 2024-07-15 16:10:39 +02:00
Sylvain Berfini
8c068d3be7 Prevent call trusted toast to be displayed multiple times + fixed number of paused calls when > 2 2024-07-15 16:10:39 +02:00
Sylvain Berfini
3a240f107c Hiding some menus & buttons when conversation is read only 2024-07-15 16:10:39 +02:00
Sylvain Berfini
6f8469eb0b Fixed leave group not hiding send message area 2024-07-15 16:10:39 +02:00
Sylvain Berfini
ef47624b9d Only load 30 messages when opening conversation, loading more messages when scrolling up 2024-07-15 16:10:39 +02:00
Sylvain Berfini
b919f51ecb Improved conversations list computing when filter is empty 2024-07-15 16:10:39 +02:00
Sylvain Berfini
7fec6cbb75 Removed useless address cleaning, already done by SDK 2024-07-15 16:10:39 +02:00
Sylvain Berfini
dc23df8b3a More dark theme fixes 2024-07-15 16:10:39 +02:00
Sylvain Berfini
b185de70ca Added media encryption statistics when long clicking media encryption icon on top of active call 2024-07-15 16:10:39 +02:00
Sylvain Berfini
d232fa0d14 Removed cellular/wifi signal alerts 2024-07-15 16:10:39 +02:00
Sylvain Berfini
3e845f45f7 Prevent crash 2024-07-15 16:10:39 +02:00
Sylvain Berfini
8b1ff7af2b Added entries to conversation popup menu + mute indicator below conversation title 2024-07-15 16:10:39 +02:00
Sylvain Berfini
570492cea9 Fixed disable video button even in call + audio route switching from bluetooth to speaker when video was enabled 2024-07-15 16:10:39 +02:00
Sylvain Berfini
8aa17ed097 Added ourselves as participant of a conversation 2024-07-15 16:10:39 +02:00
Sylvain Berfini
e4d073471c More dark theme improvements 2024-07-15 16:10:39 +02:00
Sylvain Berfini
ae4d087ad6 Improvements for dark theme 2024-07-15 16:10:39 +02:00
Sylvain Berfini
b1e5e45b43 Improved themes 2024-07-15 16:10:39 +02:00
Sylvain Berfini
eb6ab49543 Disable video toggle while outgoing call is ringing 2024-07-15 16:10:39 +02:00
Sylvain Berfini
834f8f7d7e Removed toast when entering conversation + improved chat message notification pending intent 2024-07-15 16:10:39 +02:00
Sylvain Berfini
43d5e8ff23 Fixed click on call notification not opening in-call activity 2024-07-15 16:10:39 +02:00
Sylvain Berfini
1d122abb17 Updated colors on some parts while in call 2024-07-15 16:10:39 +02:00
Sylvain Berfini
83070a9c01 Do not show the ZRTP SAS dialog again when clicking on wrong letters 2024-07-15 16:10:39 +02:00
Sylvain Berfini
b308e2a8a0 Revert hiding some features 2024-07-15 16:10:39 +02:00
Sylvain Berfini
c447b2699c Changes regarding call notifications 2024-07-15 16:10:39 +02:00
Sylvain Berfini
43c22b8ed5 Hidden for now account mode selection, moved push notification toggle from account settings to account profile 2024-07-15 16:10:39 +02:00
Sylvain Berfini
8c7ca490c8 Hidden & disabled some features for now 2024-07-15 16:10:39 +02:00
Sylvain Berfini
e8da60cb77 Updated license year 2024-07-15 16:10:39 +02:00
Sylvain Berfini
110ff995f8 Storing & using international prefix iso country code 2024-07-15 16:10:39 +02:00
Sylvain Berfini
c8a20f4f57 Prevent input error causing crash 2024-07-15 16:10:39 +02:00
Sylvain Berfini
f3ab328d74 Fixed download progress for message sent & synchronized on another device 2024-07-15 16:10:39 +02:00
Sylvain Berfini
82f1e1b486 Remove display name part of call transfer related toasts 2024-07-15 16:10:39 +02:00
Sylvain Berfini
4b2058fbe6 Show blue toast when entering an end-to-end encrypted conversation 2024-07-15 16:10:39 +02:00
Sylvain Berfini
6f623ae080 Shorten durating for call ended screen if terminate call action was made by user 2024-07-15 16:10:39 +02:00
Sylvain Berfini
715375831a Updated layout for meeting participant picker 2024-07-15 16:10:39 +02:00
Sylvain Berfini
3e0cc865b0 Clear filter after selecting an address 2024-07-15 16:10:39 +02:00
Sylvain Berfini
7c462cbb64 Fixed scrolling when opening keyboard in chat + prevent duplicated or missing messages 2024-07-15 16:10:39 +02:00
Sylvain Berfini
c5e7b4c8a2 Update isRead flag in message model when chat room is marked as read 2024-07-15 16:10:39 +02:00
Sylvain Berfini
a00409e003 Added unread message indicator when scrolling up in conversation 2024-07-15 16:10:39 +02:00
Sylvain Berfini
acfabaae23 Revert changes breaking app version & incoming call go to call activity when answering 2024-07-15 16:10:39 +02:00
Sylvain Berfini
4dc660a52d Handle possible null call in alerts 2024-07-15 16:10:39 +02:00
Sylvain Berfini
ee8c36f70b Getting rid of two TODOs regarding asking permissions during call 2024-07-15 16:10:39 +02:00
Sylvain Berfini
fa5eb6a285 Improved build.gradle a bit 2024-07-15 16:10:39 +02:00
Sylvain Berfini
281b44a240 Added conversations & meetings settings 2024-07-15 16:10:39 +02:00
Sylvain Berfini
6c03a6fb7a Fixed crash when failing to connect third party sip account 2024-07-15 16:10:39 +02:00
Sylvain Berfini
8a6a2bef02 Added label to let user know there is no message matching it's filter 2024-07-15 16:10:39 +02:00
Sylvain Berfini
29e4bb5932 Started chat & meetings settings, improved other settings 2024-07-15 16:10:39 +02:00
Sylvain Berfini
70e98dfe78 Hide video related icons when feature disabled in core + prevent two seconds delay for outgoing call fragment to display info by starting it sooner (OutgoingInit instead of OutgoingProgress) 2024-07-15 16:10:39 +02:00
Sylvain Berfini
6ab9f4232b Updated some values in default/factory RCs 2024-07-15 16:10:39 +02:00
Sylvain Berfini
e98ccdc580 Improved how chat room mark as read is handled 2024-07-15 16:10:39 +02:00
Sylvain Berfini
e1c1be3e50 Fixed click on notification no going into conversation 2024-07-15 16:10:39 +02:00
Sylvain Berfini
a547a04258 Removed + when long pressing 0 in in-call numpad 2024-07-15 16:10:39 +02:00
Sylvain Berfini
0b90af9ccc Clicking on the avatar of a selected participant will remove it as well as the cross 2024-07-15 16:10:39 +02:00
Sylvain Berfini
615185deb9 Updated layout & flow for group conversation creation + all floating action buttons are now on orange background 2024-07-15 16:10:39 +02:00
Sylvain Berfini
d82eac6175 Fixed numpad bottom sheet visible after switching fullscreen mode in video call 2024-07-15 16:10:39 +02:00
Sylvain Berfini
b87a3dd92c Display meeting subject in message description 2024-07-15 16:10:39 +02:00
Sylvain Berfini
cc403f2624 Also search in contact name when filtering conversations list + updated gradle 2024-07-15 16:10:39 +02:00
Sylvain Berfini
61c85128e8 Mark chat room as read when sending a new message to update getFirstUnreadMessagePosition() in conversation adapter 2024-07-15 16:10:39 +02:00
Sylvain Berfini
ac7e19144d Allow for no selected dial plan in Account's profile 2024-07-15 16:10:39 +02:00
Sylvain Berfini
8fb87b18e8 Fixed crash when touching multiple calls top bar if not on active call fragment + fixed top bar alert & status bar color after terminating more than one call 2024-07-15 16:10:39 +02:00
Sylvain Berfini
bf089193d4 Properly close any sliding pane child fragment when default account changes 2024-07-15 16:10:39 +02:00
Sylvain Berfini
c8b1231322 Improved settings layout 2024-07-15 16:10:39 +02:00
Sylvain Berfini
be3f6ea301 Fixed group chat room shortcut generation 2024-07-15 16:10:39 +02:00
Sylvain Berfini
4330b814a6 Switched SDK to 5.4 2024-07-15 16:10:39 +02:00
Sylvain Berfini
1cab186403 Moved TTFD to end of splash screen 2024-07-15 16:10:39 +02:00
Sylvain Berfini
5ee5982e3c Generate conversations & conferences avatars on worker thread & save result in cache directory 2024-07-15 16:10:39 +02:00
Sylvain Berfini
03ee116ed4 Updated ImageUtils.getBitmapFromMultipleAvatars() to make it synchronous, fixing avatar in conversations list when quickly scrolling + shortcuts for group conversation 2024-07-15 16:10:39 +02:00
Sylvain Berfini
f702054ac4 Added navigating to a mentionned contact on click in chat bubble + low media volume alert when playing voice recording in chat bubble 2024-07-15 16:10:39 +02:00
Sylvain Berfini
4bf0a2fa5d Updated conversation top bar layout 2024-07-15 16:10:39 +02:00
Sylvain Berfini
a2b86ff5f6 Fixed outgoing message file upload display 2024-07-15 16:10:39 +02:00
Sylvain Berfini
1b827bcf76 Fixed menu button showing in '@' participants menu 2024-07-15 16:10:39 +02:00
Sylvain Berfini
6338fb65d1 Various fixes & improvements related to chat room shorcuts 2024-07-15 16:10:39 +02:00
Sylvain Berfini
70b1c67f90 Fixed sliding pane staying open on empty fragment is back gesture done while it was opening 2024-07-15 16:10:39 +02:00
Sylvain Berfini
eaa55ab068 Added button to take photos in chat directly 2024-07-15 16:10:38 +02:00
Sylvain Berfini
62c23c248f Bumped dependencies 2024-07-15 16:10:38 +02:00
Sylvain Berfini
25c2cfc84e Added button to scroll to bottom 2024-07-15 16:10:38 +02:00
Sylvain Berfini
4ac3649c90 Fixed crash due to nav graph not loaded 2024-07-15 16:10:38 +02:00
Sylvain Berfini
10b9044aa8 Fixed scrolling issue due to MessageModel staying not read + scroll to bottom when keyboard is opened if no scrolling was initiated by user 2024-07-15 16:10:38 +02:00
Sylvain Berfini
eb5f985712 Getting rid of scroll to bottom code & events that are no longer required 2024-07-15 16:10:38 +02:00
Sylvain Berfini
62ea993847 No longer use conversation send area as a bottom sheet 2024-07-15 16:10:38 +02:00
Sylvain Berfini
73fda1fb25 Fixed generated APK name & .gitlab-ci artifact path 2024-07-15 16:10:38 +02:00
Sylvain Berfini
919abd3bd3 Fixed CI build to due debug libs not found, disabling crashlytics upload in that case 2024-07-15 16:10:38 +02:00
Sylvain Berfini
5d3d8eeedc Added plain text file viewer 2024-07-15 16:10:38 +02:00
Sylvain Berfini
de2f247c5f Added Crashlytics to debug build 2024-07-15 16:10:38 +02:00
Sylvain Berfini
d52c12606f Added message forward + a few small improvements 2024-07-15 16:10:38 +02:00
Sylvain Berfini
678949aff2 Added toast to let user know when files are waiting for a conversation to be opened to add files in it 2024-07-15 16:10:38 +02:00
Sylvain Berfini
d5b0d82adc Added forwarded message indicator 2024-07-15 16:10:38 +02:00
Sylvain Berfini
637f424a70 Fixed foreground service types & use 2024-07-15 16:10:38 +02:00
Sylvain Berfini
2274cdd343 Try to open unsupported files in native app 2024-07-15 16:10:38 +02:00
Sylvain Berfini
aa34132047 Replaced dots by current page / page count in PDF viewer 2024-07-15 16:10:38 +02:00
Sylvain Berfini
54b9ae8cd4 Detect & improve broken files if possible 2024-07-15 16:10:38 +02:00
Sylvain Berfini
c6b7ed0ef3 Added logs & small fixes 2024-07-15 16:10:38 +02:00
Sylvain Berfini
6645d579a2 Added audio device callback to reload sound devices when needed 2024-07-15 16:10:38 +02:00
Sylvain Berfini
ec07d54ed9 Improved long press menu on chat bubble when text was very long 2024-07-15 16:10:38 +02:00
Sylvain Berfini
2ad4e76bc3 Fixed clicking on chat message notification while being in conversations list stacked it twice 2024-07-15 16:10:38 +02:00
Sylvain Berfini
7c51cf7588 Added sharing file from file viewer 2024-07-15 16:10:38 +02:00
Sylvain Berfini
b32007b1ca Added export to PDF file received by chat to Android public storage 2024-07-15 16:10:38 +02:00
Sylvain Berfini
80b887c874 Improved PDF viewer 2024-07-15 16:10:38 +02:00
Sylvain Berfini
ff521eb559 Prevent bottom sheet dialog to hide some long press options (mostly happened in landscape) 2024-07-15 16:10:38 +02:00
Sylvain Berfini
e9b1bfd2a0 Fixed first sliding pane child fragment not being poped-up when closing sliding pane, manually navigating to empty fragment to workaround that 2024-07-15 16:10:38 +02:00
Sylvain Berfini
9eb4458c73 Close sliding pane when default account changed 2024-07-15 16:10:38 +02:00
Sylvain Berfini
e9f19b6834 Added toast when new account is successfully configured + set first available account as default when previous one was removed 2024-07-15 16:10:38 +02:00
Sylvain Berfini
fa796b9609 Updated way of setting light/dark/auto mode 2024-07-15 16:10:38 +02:00
Sylvain Berfini
e6d33a9e1a Fixed timestamps in message notification 2024-07-15 16:10:38 +02:00
Sylvain Berfini
6f9a5a6009 Small improvements over reactions layout 2024-07-15 16:10:38 +02:00
Sylvain Berfini
d178ce40b5 Fixed & improved back press callbacks set by app 2024-07-15 16:10:38 +02:00
Sylvain Berfini
ddc8ba7105 Scroll to first unread message in conversation upon entering it 2024-07-15 16:10:38 +02:00
Sylvain Berfini
006fa3fa4a This should prevent empty fragment visible sometimes 2024-07-15 16:10:38 +02:00
Sylvain Berfini
fd088fdbc8 Fixed double back gesture required to leave conversations list when app is opened from shortcut 2024-07-15 16:10:38 +02:00
Sylvain Berfini
ef1669dd4c Fetch all unread messages before creating a new notifiable for a chat room 2024-07-15 16:10:38 +02:00
Sylvain Berfini
58ec0e7abb Fixed & improved a few things related to chat 2024-07-15 16:10:38 +02:00
Sylvain Berfini
f5d141d59f Added app version (git describe) to troubleshooting view 2024-07-15 16:10:38 +02:00
Sylvain Berfini
b30f44a0a1 Do not display confirm dialog when leaving new contact fragment without having typed anything 2024-07-15 16:10:38 +02:00
Sylvain Berfini
02b372fa3d Reset some message model fields when re-computing contents list 2024-07-15 16:10:38 +02:00
Sylvain Berfini
aab9704d24 Added onAccountRemoved + fixed FAB background color + removed setFriendsDatabasePath that no longer exists 2024-07-15 16:10:38 +02:00
Sylvain Berfini
9b69074352 Updated colors in main call views to keep the same color no matter the light/dark theme 2024-07-15 16:10:38 +02:00
Sylvain Berfini
9702118f3f Bumped dependencies 2024-07-15 16:10:38 +02:00
Sylvain Berfini
ee04b728c9 Do not use color night variant, instead use attr & theme 2024-07-15 16:10:38 +02:00
Sylvain Berfini
b80a86a366 Added full screen intent permission for Android 14 + updated callbacks due to rework 2024-07-15 16:10:38 +02:00
Sylvain Berfini
3e7e2000d5 Fixed favourite status in contacts list not matching reality after a change 2024-07-15 16:10:38 +02:00
Sylvain Berfini
9070b77b30 Updated previous & next messages bubbles if needed when a message is deleted 2024-07-15 16:10:38 +02:00
Sylvain Berfini
99445cc8d6 Updated events layout & send message icon 2024-07-15 16:10:38 +02:00
Sylvain Berfini
bfc2a8ae34 Improved scrolling performances in contacts list 2024-07-15 16:10:38 +02:00
Sylvain Berfini
6badcc2887 Fixed crash when newly received/sent message should update group with last one in history + improved outgoing bubble background 2024-07-15 16:10:38 +02:00
Sylvain Berfini
b10b51f839 Do update conference layout in dialog 2024-07-15 16:10:38 +02:00
Sylvain Berfini
9496999773 Added back intent filter for dialing tel, sip or sips URI 2024-07-15 16:10:38 +02:00
Sylvain Berfini
c77df7228e Added image placeholder, restore round corners except for gifs 2024-07-15 16:10:38 +02:00
Sylvain Berfini
fdc696691a Updated dependencies 2024-07-15 16:10:38 +02:00
Sylvain Berfini
ff2d04351f Renamed chat message to simply message 2024-07-15 16:10:38 +02:00
Sylvain Berfini
45ca7aa348 Proper fix for chatroom-xxxx + updated logs to use conversation instead of chat room 2024-07-15 16:10:38 +02:00
Sylvain Berfini
24cbcd3937 Updated call transfer icon 2024-07-15 16:10:38 +02:00
Sylvain Berfini
489aece7c5 Dismiss dialogs when fragment is paused to prevent crashes when rotating device 2024-07-15 16:10:38 +02:00
Sylvain Berfini
39a6254ee1 Refresh chat room info when state is going to Created, prevents chatroom-xxxx display 2024-07-15 16:10:38 +02:00
Sylvain Berfini
0bbab221a2 Updated disabled buttons when in pause 2024-07-15 16:10:38 +02:00
Sylvain Berfini
9094a167eb Fixed bubble UI when scrolling after a voice record has been displayed 2024-07-15 16:10:38 +02:00
Sylvain Berfini
39b8358ef5 Fixed reply through slide action not opening keyboard 2024-07-15 16:10:38 +02:00
Sylvain Berfini
a571b77117 Started conference layout bottom sheet 2024-07-15 16:10:38 +02:00
Sylvain Berfini
fd3f85f2b7 Reworked audio devices list in call 2024-07-15 16:10:38 +02:00
Sylvain Berfini
55d67e92d3 Reworked light/dark theme setting 2024-07-15 16:10:38 +02:00
Sylvain Berfini
a85d0df668 Quickly added dark mode from Figma, still a lot of work to do... 2024-07-15 16:10:38 +02:00
Sylvain Berfini
f623de53d4 Workaround to display basic/not secured conversation if unread count > 0 2024-07-15 16:10:38 +02:00
Sylvain Berfini
5aaa174b20 Improved conference call notification + new conference call log details layout 2024-07-15 16:10:38 +02:00
Sylvain Berfini
c273fc451a Added orange border to chat bubble to show selected bubble when opening bottom sheet menu + small refactoring 2024-07-15 16:10:38 +02:00
Sylvain Berfini
62fa1d532c Started improvement over notify received to prevent blinking lists 2024-07-15 16:10:38 +02:00
Sylvain Berfini
92835a1e10 Fixed issue with first letter displayed when refreshing contacts list 2024-07-15 16:10:38 +02:00
Sylvain Berfini
20fbeda124 Improved avatars cache 2024-07-15 16:10:38 +02:00
Sylvain Berfini
56bc96314a Using same color for dialog scrim as Android 2024-07-15 16:10:38 +02:00
Sylvain Berfini
688f797acf Use better quality of native contact picture if available, otherwise fallback to thumbnail 2024-07-15 16:10:38 +02:00
Sylvain Berfini
5f9edb4fcc Added swipe left to right on a chat bubble to reply 2024-07-15 16:10:38 +02:00
Sylvain Berfini
f35df5e418 Update conversation subject when leaving info fragment + removed todos, replaced by toasts 2024-07-15 16:10:38 +02:00
Sylvain Berfini
a64e13a021 Fixed wrong status bar color when non-default account registration fails 2024-07-15 16:10:38 +02:00
Sylvain Berfini
294f7f6fae Added export file to Android's MediaStore from FileViewer 2024-07-15 16:10:38 +02:00
Sylvain Berfini
746ddf6457 Fixed some TODOs 2024-07-15 16:10:38 +02:00
Sylvain Berfini
373a5f004b Replaced top bar check icon by big bottom button, fixed conversation users_three icon instead of meeting 2024-07-15 16:10:37 +02:00
Sylvain Berfini
ad35f85c3a Added compatibility package 2024-07-15 16:10:37 +02:00
Sylvain Berfini
0d31bfe3c3 Updated layout for default/interop mode selection + added logs to contacts manager 2024-07-15 16:10:37 +02:00
Sylvain Berfini
bd9947a705 Display in_progress drawable + text on chat rooms list cell to show removal is in progress 2024-07-15 16:10:37 +02:00
Sylvain Berfini
d30f7ba5ba Fixed deadlock when receiving call 2024-07-15 16:10:37 +02:00
Sylvain Berfini
3ea3ff288b Added schedule meeting from conversation info, fixed previous participants removed when adding new participant(s) to a meeting schedule 2024-07-15 16:10:37 +02:00
Sylvain Berfini
4e602fc5e8 Fixed sharing logs with Linphone from debug fragment 2024-07-15 16:10:37 +02:00
Sylvain Berfini
7099a33dc4 Added logo on configure chat messages ephemeral duration + fixed welcome linphone logo 2024-07-15 16:10:37 +02:00
Sylvain Berfini
ee15b00533 Fixed chat rooms list not re-ordering properly 2024-07-15 16:10:37 +02:00
Sylvain Berfini
d480840353 Added see contact / add to contacts action in 1-1 conversation info 2024-07-15 16:10:37 +02:00
Sylvain Berfini
16dd423016 Reworked ephemeral lifetime configuration dialog into a dedicated fragment 2024-07-15 16:10:37 +02:00
Sylvain Berfini
fb05cf6280 Fixed unread message count not updated in chat room list cell when a new message was received 2024-07-15 16:10:37 +02:00
Sylvain Berfini
c2eccd23a8 Open keyboard on reply, scroll to latest message when keyboard is opened, removed onNewEvents causing duplicated messages, fixed latest message display when adding new ones that should be grouped with it 2024-07-15 16:10:37 +02:00
Sylvain Berfini
ed23268672 Updated meeting icon + using newly added callbacks in SDK for default account 2024-07-15 16:10:37 +02:00
Sylvain Berfini
975473b2e4 Fixed issue in ephemeral lifetime dialog 2024-07-15 16:10:37 +02:00
Sylvain Berfini
c0707e8cb5 Fixed action in contact details 2024-07-15 16:10:37 +02:00
Sylvain Berfini
d58edf0614 Fixed delete conversation history not updating chat messages list after going back 2024-07-15 16:10:37 +02:00
Sylvain Berfini
ce1cb3a15a Fixed issue with suggestion initials display + fixed animated in_progress 2024-07-15 16:10:37 +02:00
Sylvain Berfini
c650887c04 More work related to ephemeral messages 2024-07-15 16:10:37 +02:00
Sylvain Berfini
991a5e695a Added missed call(s) notification 2024-07-15 16:10:37 +02:00
Sylvain Berfini
2f18ecb562 Added auto downloaded files to notifications 2024-07-15 16:10:37 +02:00
Sylvain Berfini
370b786ed0 Remove chat room shortcut after deleting it 2024-07-15 16:10:37 +02:00
Sylvain Berfini
a092922145 Code cleanup 2024-07-15 16:10:37 +02:00
Sylvain Berfini
ff6d722e44 Attempt to fix random dead lock when app starts 2024-07-15 16:10:37 +02:00
Sylvain Berfini
d325203e8b Fixed in-call avatar not updated when ZRTP SAS is validated + scroll to top of calls logs history on resume 2024-07-15 16:10:37 +02:00
Sylvain Berfini
c11567f095 Bumped gradle to 8.1.4 2024-07-15 16:10:37 +02:00
Sylvain Berfini
bd38c7dc49 Added ephemeral messages to conversation info 2024-07-15 16:10:37 +02:00
Sylvain Berfini
80994ffafb Added chat message re-send action on long press if outgoing and in NotDelivered state 2024-07-15 16:10:37 +02:00
Sylvain Berfini
a1374d228e Changes for first launches & proper back nav 2024-07-15 16:10:37 +02:00
Sylvain Berfini
c491f18ba5 Fixed hanging up call from notification 2024-07-15 16:10:37 +02:00
Sylvain Berfini
c7f86311aa Reworked top bar alert mechanism, added network not reachable alert 2024-07-15 16:10:37 +02:00
Sylvain Berfini
6b95cc6a5c Added long press bottom sheet menu to meetings list cell 2024-07-15 16:10:37 +02:00
Sylvain Berfini
3ce702fc0d Updated conversation info participant cell with missing menu + added debug logs 2024-07-15 16:10:37 +02:00
Sylvain Berfini
6c72fb9689 Moved FileViewerFragment out of chat nav scope to be able to display it fullscreen when in landscape 2024-07-15 16:10:37 +02:00
Sylvain Berfini
7cca8f1889 Fixed subject changed event using chat room username insteadf of subject 2024-07-15 16:10:37 +02:00
Sylvain Berfini
c92d4982c6 Using different regex for URL detection to ensure it starts by http(s) and thus prevents ActivityNotFound exception 2024-07-15 16:10:37 +02:00
Sylvain Berfini
5f5885cb18 Fixed race condition issue on avatarsMap + listener removed by mistake 2024-07-15 16:10:37 +02:00
Sylvain Berfini
b113e2b729 Fixed issues when switching default account 2024-07-15 16:10:37 +02:00
Sylvain Berfini
8d05d786ce Added video player 2024-07-15 16:10:37 +02:00
Sylvain Berfini
b6c146f123 Added PDF file viewer 2024-07-15 16:10:37 +02:00
Sylvain Berfini
73229f51a1 Ask for record audio permission when trying to record a voice message if not granted yet 2024-07-15 16:10:37 +02:00
Sylvain Berfini
26b3fe67a3 Updated adapters (no longer need viewlifecycleowner) & improved avatar loader 2024-07-15 16:10:37 +02:00
Sylvain Berfini
01dab1613d More layout improvements, fixed nested recyclerview invisible in landscape 2024-07-15 16:10:37 +02:00
Sylvain Berfini
6f4e1a45d1 Reworked main layouts with recyclerview to improve scrolling performances (like in chat) 2024-07-15 16:10:37 +02:00
Sylvain Berfini
297eb71ff7 Various UI fixes 2024-07-15 16:10:37 +02:00
Sylvain Berfini
b66a40fa41 Fixed contacts list issue when changing filter 2024-07-15 16:10:37 +02:00
Sylvain Berfini
ac1ae71f11 Fixed bad display of chat message reply if original message is missing 2024-07-15 16:10:37 +02:00
Sylvain Berfini
00cea06899 Improved processing of contacts in contacts list 2024-07-15 16:10:37 +02:00
Sylvain Berfini
07926bad1e Fixed all participants displayed as admin in video conference 2024-07-15 16:10:37 +02:00
Sylvain Berfini
57eb506bdf Updated share icon behavior in meeting info + added new item in popup menu 2024-07-15 16:10:37 +02:00
Sylvain Berfini
5cc8407077 Updated chat message in progress status icon (now animated) 2024-07-15 16:10:37 +02:00
Sylvain Berfini
f1eca63b5a Updated style of login/register bottom button in assistant 2024-07-15 16:10:37 +02:00
Sylvain Berfini
ac1e636caa Disable back gesture/button while on EndedCall fragment to prevent user from leaving it until timer has expired 2024-07-15 16:10:37 +02:00
Sylvain Berfini
567ef561c0 Generate avatar for conferences based on participants avatar (like for group conversations) 2024-07-15 16:10:37 +02:00
Sylvain Berfini
035738f4c5 Proper display of group conversation events 2024-07-15 16:10:37 +02:00
Sylvain Berfini
61bd2967b0 Fixed & improved UI 2024-07-15 16:10:37 +02:00
Sylvain Berfini
2fa856e790 Added file download 2024-07-15 16:10:37 +02:00
Sylvain Berfini
a8aa3be08a Added layout for not downloaded file 2024-07-15 16:10:37 +02:00
Sylvain Berfini
6ae57158b7 Made chat message bottom sheet content dynamic 2024-07-15 16:10:37 +02:00
Sylvain Berfini
6fec1958b8 Added file content layout in bubble 2024-07-15 16:10:37 +02:00
Sylvain Berfini
efdfc809bc Made call history detail fragment scrollable, removed viewLifecycleOwner param from adapters that do not require it 2024-07-15 16:10:37 +02:00
Sylvain Berfini
e62b1b4999 Added missing plain/text sharing feature + proper sliding pane layout sizes in landscape 2024-07-15 16:10:37 +02:00
Sylvain Berfini
7f10548ecb Removed unseless Fragment only containing SlidingPaneLayout + using same values for SlidingPaneLayout childs width as on previous releases 2024-07-15 16:10:37 +02:00
Sylvain Berfini
53ca7aef1a Started splashscreen, waiting for first fragment to be displayed 2024-07-15 16:10:37 +02:00
Sylvain Berfini
6aa2f49321 Fixed ANR related to voice record player + other related improvements 2024-07-15 16:10:37 +02:00
Sylvain Berfini
e0d07c80ac Fixed date time & imdn icon alignement on outgoing bubbles 2024-07-15 16:10:37 +02:00
Sylvain Berfini
fb5d89e987 Added voice record player in chat bubble 2024-07-15 16:10:37 +02:00
Sylvain Berfini
f84f42d8bd Moved voice recording layout to it's own file 2024-07-15 16:10:37 +02:00
Sylvain Berfini
c28433688a Added voice record player 2024-07-15 16:10:37 +02:00
Sylvain Berfini
80fe93c6c4 Added voice recording, have to do voice record player 2024-07-15 16:10:37 +02:00
Sylvain Berfini
3071c079ba Split ConversationViewModel in two 2024-07-15 16:10:37 +02:00
Sylvain Berfini
8f33f1f0c9 Started voice recording layout 2024-07-15 16:10:37 +02:00
Sylvain Berfini
f31209162a Added some space between bottom sheet and call area 2024-07-15 16:10:37 +02:00
Sylvain Berfini
c1d76bbd29 Renamed some views, replaced caret by handle for in call bottom sheet 2024-07-15 16:10:37 +02:00
Sylvain Berfini
3507117cff Fixed small code issue 2024-07-15 16:10:37 +02:00
Sylvain Berfini
efb8ec66a2 Commented out postpone enter transition for conversation fragment, seems better 2024-07-15 16:10:37 +02:00
Sylvain Berfini
97cae93bb5 Started external file sharing 2024-07-15 16:10:37 +02:00
Sylvain Berfini
16bf6bfc2c Finished sending files through chat 2024-07-15 16:10:37 +02:00
Sylvain Berfini
116ca3cbfe Reworked participants list for mentions a bit, added possibility to attach/remove files to/from sending area 2024-07-15 16:10:37 +02:00
Sylvain Berfini
713e048db9 Much better scrolling performances in chat messages 2024-07-15 16:10:37 +02:00
Sylvain Berfini
1f18b6b0da Fixed group avatar if only one available image 2024-07-15 16:10:37 +02:00
Sylvain Berfini
5eb53725fb Few changes 2024-07-15 16:10:37 +02:00
Sylvain Berfini
dab462de35 Fixed chat rooms list sort order + added mentions menu when typing '@' + hide participants in non-group conversation 2024-07-15 16:10:37 +02:00
Sylvain Berfini
d895fc6a09 Added click on web links to open browser in chat bubble 2024-07-15 16:10:37 +02:00
Sylvain Berfini
1c7fe3fd3e Re-order conversations list when needed + scroll to bottom when a new message is sent or received in a conversation 2024-07-15 16:10:37 +02:00
Sylvain Berfini
8cbe832a67 Updated gradle to 8.1.3, added missing input type to chat text field & correct default icon for group chat rooms 2024-07-15 16:10:37 +02:00
Sylvain Berfini
e61a6a0b7f Improved reply layout 2024-07-15 16:10:37 +02:00
Sylvain Berfini
cd3b9e1422 Various UI improvements 2024-07-15 16:10:37 +02:00
Sylvain Berfini
76b41b693b Move as much as possible code from onBindViewHolder to onCreateViewHolder 2024-07-15 16:10:37 +02:00
Sylvain Berfini
115ce8148a More performance improvements 2024-07-15 16:10:37 +02:00
Sylvain Berfini
1fbad779af Performance improvements for chat 2024-07-15 16:10:37 +02:00
Sylvain Berfini
fa78f7b9b3 Updated forward/reply icons + fixed long press on text in bubble not working 2024-07-15 16:10:37 +02:00
Sylvain Berfini
b4a52e244d Updated margins on chat bubble 2024-07-15 16:10:37 +02:00
Sylvain Berfini
46ae326781 Improved chat image viewer 2024-07-15 16:10:37 +02:00
Sylvain Berfini
af03b30352 Fixed trusted icon visible in account profile if not in secure mode, hardcoded texts, wrong conference subject displayed in calls history list 2024-07-15 16:10:37 +02:00
Sylvain Berfini
2857378c87 Added conference info in chat bubble layout 2024-07-15 16:10:37 +02:00
Sylvain Berfini
1723525077 Keep scroll position in conversation when going back after leaving 2024-07-15 16:10:37 +02:00
Sylvain Berfini
ad1625dbb3 Started image viewer 2024-07-15 16:10:37 +02:00
Sylvain Berfini
b585ba7a8b Updated chat bubbles to display images grid 2024-07-15 16:10:37 +02:00
Sylvain Berfini
cafa301ea8 Started grid layout 2024-07-15 16:10:37 +02:00
Sylvain Berfini
7f739a4bc1 Displaying image (or video preview) in chat bubble if alone 2024-07-15 16:10:36 +02:00
Sylvain Berfini
59c7140ce6 Do not repeat SIP / Phone header in contact editor 2024-07-15 16:10:36 +02:00
Sylvain Berfini
aeaa41fcbe Fixed crash when rotating device while new contact editor is opened 2024-07-15 16:10:36 +02:00
Sylvain Berfini
223c91b6b9 Added long press menu on outgoing chat bubbles 2024-07-15 16:10:36 +02:00
Sylvain Berfini
30f9f381cd Added click to remove our own reaction 2024-07-15 16:10:36 +02:00
Sylvain Berfini
7aed1d83e3 Started mention of participant in chat message 2024-07-15 16:10:36 +02:00
Sylvain Berfini
5e069033b3 Updated bubbles color + added call on click in SIP URI in chat 2024-07-15 16:10:36 +02:00
Sylvain Berfini
a919f5edbc Added single sign on using OpenID and our Keycloak test instance 2024-07-15 16:10:36 +02:00
Sylvain Berfini
325feb5637 It seems that annotating the lambda makes the thread check works 2024-07-15 16:10:36 +02:00
Sylvain Berfini
61bd3978a4 Fixed crash due to .value = called from WorkerThread 2024-07-15 16:10:36 +02:00
Sylvain Berfini
56ec0f8911 Added calls list while in conference + started conference participants list 2024-07-15 16:10:36 +02:00
Sylvain Berfini
48baed897c More UI work on conference related screens 2024-07-15 16:10:36 +02:00
Sylvain Berfini
aa52f3d2b5 Started to add video to conference participants + fixed call ended fragment timer 2024-07-15 16:10:36 +02:00
Sylvain Berfini
2eb8b496cd Various fixes & improvements 2024-07-15 16:10:36 +02:00
Sylvain Berfini
178aae3883 Improved conference call log display in history (avatar to improve) 2024-07-15 16:10:36 +02:00
Sylvain Berfini
17a4e546a5 Started mosaic display for conferences 2024-07-15 16:10:36 +02:00
Sylvain Berfini
46dc3b2d00 Added missing trust badge on some call views 2024-07-15 16:10:36 +02:00
Sylvain Berfini
6dea7c3fec Create if necessary then go to 1-1 chat room when using message button from contact/history details 2024-07-15 16:10:36 +02:00
Sylvain Berfini
425722ef65 Started conference extra actions menu 2024-07-15 16:10:36 +02:00
Sylvain Berfini
36f57be6cc Rework to factorize code 2024-07-15 16:10:36 +02:00
Sylvain Berfini
fadc6032fb Updated shape of FAB + updated chat icon in bottom nav bar 2024-07-15 16:10:36 +02:00
Sylvain Berfini
85f97b86da Added possibility to send reply & scroll to original message on click 2024-07-15 16:10:36 +02:00
Sylvain Berfini
e8ca20a7e2 Started conference call UI 2024-07-15 16:10:36 +02:00
Sylvain Berfini
4368e1a5f7 Indicator in meetings list for today 2024-07-15 16:10:36 +02:00
Sylvain Berfini
a129cc5f95 Started conference waiting room 2024-07-15 16:10:36 +02:00
Sylvain Berfini
98488e5798 Added call button (does nothing yet) in chat room info + updated reactions layout 2024-07-15 16:10:36 +02:00
Sylvain Berfini
b80f520162 Fixed unecessary reloading of nav graph when app is going from background to foreground 2024-07-15 16:10:36 +02:00
Sylvain Berfini
1d7ca67053 Changes to make chat room shortcuts work 2024-07-15 16:10:36 +02:00
Sylvain Berfini
58c30a638f Started to display incoming messages reply to message 2024-07-15 16:10:36 +02:00
Sylvain Berfini
077e625512 Show message bottom sheet for 1-1 chat room 2024-07-15 16:10:36 +02:00
Sylvain Berfini
c166c87479 Reworked emoji reaction picker & display 2024-07-15 16:10:36 +02:00
Sylvain Berfini
19a15bedfa Meetings can be scheduled 2024-07-15 16:10:36 +02:00
Sylvain Berfini
cf41bef449 Allow to edit subject of group chat room 2024-07-15 16:10:36 +02:00
Sylvain Berfini
a27cf28544 Adding participants to existing group 2024-07-15 16:10:36 +02:00
Sylvain Berfini
d9d7508292 Improved genericity related to contact / suggestion picker 2024-07-15 16:10:36 +02:00
Sylvain Berfini
5847e7d2c2 Fixed new call / new chat room fragments never visible due to empty contact list 2024-07-15 16:10:36 +02:00
Sylvain Berfini
f855426a9f Made chat message delivery bottom sheet generic to also use it to display emoji reactions lists 2024-07-15 16:10:36 +02:00
Sylvain Berfini
18f0d9109e Fixed broken conference factory URI due to dumb mistake 2024-07-15 16:10:36 +02:00
Sylvain Berfini
a78421d79a Fixed incoming call style notification not always displayed 2024-07-15 16:10:36 +02:00
Sylvain Berfini
a49cb0935d Started add participant to existing group chat room 2024-07-15 16:10:36 +02:00
Sylvain Berfini
27a408c9f1 Improved avatar geneator to be able to create Icons for notifications & shortcuts 2024-07-15 16:10:36 +02:00
Sylvain Berfini
682aaafc85 Added shortcuts + improved bubble layouts 2024-07-15 16:10:36 +02:00
Sylvain Berfini
636a0b5c4d Hide & show again bottom sheet dialog when switching message 2024-07-15 16:10:36 +02:00
Sylvain Berfini
e24d3ca33f Fixed conversation not visible after creating it + missing presence in 1-1 conversation 2024-07-15 16:10:36 +02:00
Sylvain Berfini
76ba9e5e35 Using recyclerview to improve delivery status list performances 2024-07-15 16:10:36 +02:00
Sylvain Berfini
b9e4fcf1a6 Simple reactions to chat message display 2024-07-15 16:10:36 +02:00
Sylvain Berfini
f821707cc6 More work related to IMDNs display & chat in general 2024-07-15 16:10:36 +02:00
Sylvain Berfini
c151ef1526 Improved delivery bottom sheet 2024-07-15 16:10:36 +02:00
Sylvain Berfini
170e441744 Trying to optimize the ContactAvatarModel use 2024-07-15 16:10:36 +02:00
Sylvain Berfini
be908cdf0e Started proto for IMDNs display 2024-07-15 16:10:36 +02:00
Sylvain Berfini
0defb639c2 Fixed group avatar with two participants 2024-07-15 16:10:36 +02:00
Sylvain Berfini
3d455d0fc9 Finished emoji picker, started file picker 2024-07-15 16:10:36 +02:00
Sylvain Berfini
561c36bfe0 Added conference factory URIs & LIME server URL to account params 2024-07-15 16:10:36 +02:00
Sylvain Berfini
f07a8f6c2b Added chat room participant admin popup to remove a person from a group & give/remove admin rights 2024-07-15 16:10:36 +02:00
Sylvain Berfini
1c7b97d8db Added subject field for group chat room creation 2024-07-15 16:10:36 +02:00
Sylvain Berfini
ee46722a3d Started multiple contact selection in chat room creation 2024-07-15 16:10:36 +02:00
Sylvain Berfini
4fb4c7c85d Fixed manual filter of chat rooms 2024-07-15 16:10:36 +02:00
Sylvain Berfini
8526c24c3e Started 1-1 chat room creation 2024-07-15 16:10:36 +02:00
Sylvain Berfini
db72bffcbd Do not show notifications for messages received in currently displayed chat room 2024-07-15 16:10:36 +02:00
Sylvain Berfini
b3396fa62e Do no apply filter in conversation when filter is empty 2024-07-15 16:10:36 +02:00
Sylvain Berfini
17d3208cbd Fixed assistant pages header on large tablets in landscape 2024-07-15 16:10:36 +02:00
Sylvain Berfini
f9d2e04609 Added search in conversation top bar 2024-07-15 16:10:36 +02:00
Sylvain Berfini
a2355e3225 Started conversation info fragment 2024-07-15 16:10:36 +02:00
Sylvain Berfini
c076dcb2c7 Fixed account profile avatar not being updated when set/removed + started chat event display 2024-07-15 16:10:36 +02:00
Sylvain Berfini
ae1e2599a8 Improved account mode fragment 2024-07-15 16:10:36 +02:00
Sylvain Berfini
8858deb42f Improved incoming bubble layout 2024-07-15 16:10:36 +02:00
Sylvain Berfini
e3c1280278 No longer move chat message position in long press dialog to match position in list + disabled emojis & reply action if chat room is read only 2024-07-15 16:10:36 +02:00
Sylvain Berfini
8794146df7 ShapeImageView can handle rounding images by itself, no need for Coil's CircleCropTransform 2024-07-15 16:10:36 +02:00
Sylvain Berfini
4ce5d989c5 Use ShapableImageView to easily draw borders around avatars, fixed trust badge display condition 2024-07-15 16:10:36 +02:00
Sylvain Berfini
1dc5776cb8 Fixed group avatar with 3 images 2024-07-15 16:10:36 +02:00
Sylvain Berfini
43cf0f5cba Finished removing AvatarView (still borders to do and fix display issue when group has 3 participants) 2024-07-15 16:10:36 +02:00
Sylvain Berfini
e4a3ec37c5 Started removing AvatarView dependency 2024-07-15 16:10:36 +02:00
Sylvain Berfini
2cef7980ea Fixed hidden last message(s) of conversation 2024-07-15 16:10:36 +02:00
Sylvain Berfini
db5ea158c3 Send custom reaction using emoji picker 2024-07-15 16:10:36 +02:00
Sylvain Berfini
daacb3ca98 Huge performances boost by removing AvatarView, todo: do it everywhere 2024-07-15 16:10:36 +02:00
Sylvain Berfini
888c8c453a Reworked algorithm that groups chat messages together 2024-07-15 16:10:36 +02:00
Sylvain Berfini
c9db3df251 Reworked permissions fragment 2024-07-15 16:10:36 +02:00
Sylvain Berfini
faf3e887b9 Added send reaction, copy text & delete options to chat message long press menu 2024-07-15 16:10:36 +02:00
Sylvain Berfini
8269228b8a Update some info dynamically 2024-07-15 16:10:36 +02:00
Sylvain Berfini
d9b6f0482a PoC for blurring conversation except for long pressed chat bubble 2024-07-15 16:10:36 +02:00
Sylvain Berfini
b8d8e877d7 More work on conversation 2024-07-15 16:10:36 +02:00
Sylvain Berfini
db6f71c8cb Added simple text message sending + improved sent chat bubble 2024-07-15 16:10:36 +02:00
Sylvain Berfini
9c6533bceb Added unread messages count indicator to bottom nav bar 2024-07-15 16:10:36 +02:00
Sylvain Berfini
0d43006a14 Added missing paddings to notifications counter to be more centered in background circle 2024-07-15 16:10:36 +02:00
Sylvain Berfini
8a62a68d6e Added group avatar for group chat rooms 2024-07-15 16:10:36 +02:00
Sylvain Berfini
3dc23d906e Improved incoming bubble layout 2024-07-15 16:10:36 +02:00
Sylvain Berfini
9d5474f352 Added popup menu & meeting removal, hiding operation in progress for now 2024-07-15 16:10:36 +02:00
Sylvain Berfini
3ae9740336 Started meeting detail view 2024-07-15 16:10:36 +02:00
Sylvain Berfini
5531dabae4 Started meeting scheduler UI 2024-07-15 16:10:36 +02:00
Sylvain Berfini
7c28c37d0b Improved touch size of some buttons 2024-07-15 16:10:36 +02:00
Sylvain Berfini
b6bdca7b89 Apply filter to meetings list + only show meetings for default account 2024-07-15 16:10:36 +02:00
Sylvain Berfini
c952178749 Started grouping chat messages from same person in a short interval of time 2024-07-15 16:10:36 +02:00
Sylvain Berfini
395dc379ed Fixed issue in conversations list & conversation related to group 2024-07-15 16:10:36 +02:00
Sylvain Berfini
3873028209 Started meetings list 2024-07-15 16:10:36 +02:00
Sylvain Berfini
4a11554fa5 Store latest visited main page & use it as default page on next app opening + fixed registration failure top bar click 2024-07-15 16:10:36 +02:00
Sylvain Berfini
2fb97fbc51 Started displaying messages 2024-07-15 16:10:35 +02:00
Sylvain Berfini
c12ddf43a2 Started conversation layout 2024-07-15 16:10:35 +02:00
Sylvain Berfini
a481b5c6cd Fixed list header when only suggestions 2024-07-15 16:10:35 +02:00
Sylvain Berfini
797418ce1a Started new chat room fragment 2024-07-15 16:10:35 +02:00
Sylvain Berfini
95baa55472 Hidden separators, added red top bar for non-default accout registration failure notification 2024-07-15 16:10:35 +02:00
Sylvain Berfini
b105c436ba Bumped dependencies 2024-07-15 16:10:35 +02:00
Sylvain Berfini
0d880dda50 Started to display conversations list with long press menu 2024-07-15 16:10:35 +02:00
Sylvain Berfini
3f24e73978 Added grant all button on permissions fragment 2024-07-15 16:10:35 +02:00
Sylvain Berfini
7eaa5cfb22 Show persistent red toast while default account is in failed registration state 2024-07-15 16:10:35 +02:00
Sylvain Berfini
a66d29ba2e Showing media encryption icon 2024-07-15 16:10:35 +02:00
Sylvain Berfini
c3de2e906b Updated dependencies, better way to show/hide keyboard 2024-07-15 16:10:35 +02:00
Sylvain Berfini
30d03d280c Updated path to debug APK in CI 2024-07-15 16:10:35 +02:00
Sylvain Berfini
3971b18b04 Removed deprecated code 2024-07-15 16:10:35 +02:00
Sylvain Berfini
46c3ed0b0d Wait a bit before turning status bar green in MainActivity to give time to start CallActivity that will change it back to orange anyway 2024-07-15 16:10:35 +02:00
Sylvain Berfini
bef704d445 Moved account unread notification count in drawer 2024-07-15 16:10:35 +02:00
Sylvain Berfini
6e027811ee Updated default avatar in contact editor + remove it when a picture is selected 2024-07-15 16:10:35 +02:00
Sylvain Berfini
7f9dbaec2a Removed history list swipe to remove callbacks 2024-07-15 16:10:35 +02:00
Sylvain Berfini
3bbccf6c89 Added player for device default ringtone in settings 2024-07-15 16:10:35 +02:00
Sylvain Berfini
528855c5d5 Updated navigation & other views when switching default account 2024-07-15 16:10:35 +02:00
Sylvain Berfini
78dd449baf Do not disable save button in contact editor when mandatory fields aren't filled, TODO: notify user 2024-07-15 16:10:35 +02:00
Sylvain Berfini
ada4e786ec Pre-display the list of calls list after 20 items 2024-07-15 16:10:35 +02:00
Sylvain Berfini
57094d6cce Added CI 2024-07-15 16:10:35 +02:00
Sylvain Berfini
de37ae245d Added account settings 2024-07-15 16:10:35 +02:00
Sylvain Berfini
e1c4be005f Updated SMS invite icon 2024-07-15 16:10:35 +02:00
Sylvain Berfini
df728f6856 Added gradient to assistant & welcome header image 2024-07-15 16:10:35 +02:00
Sylvain Berfini
1445760cc0 Fixed firstName / lastName mixup 2024-07-15 16:10:35 +02:00
Sylvain Berfini
3c0640ce11 Updated remote recording to show toast 2024-07-15 16:10:35 +02:00
Sylvain Berfini
9268ef5d2f Updated some colors 2024-07-15 16:10:35 +02:00
Sylvain Berfini
adad98f3e2 Added permissions fragment 2024-07-15 16:10:35 +02:00
Sylvain Berfini
ea87c48586 Added help popup for international prefix picker in account profile 2024-07-15 16:10:35 +02:00
Sylvain Berfini
9c06c9802d Updated margins 2024-07-15 16:10:35 +02:00
Sylvain Berfini
2686f80502 Change transfer call action label when more than one call 2024-07-15 16:10:35 +02:00
Sylvain Berfini
5e50ef4045 Fixed in-call top bar displaying other calls 2024-07-15 16:10:35 +02:00
Sylvain Berfini
c4fe71c59f Added internation prefix picker in account profile 2024-07-15 16:10:35 +02:00
Sylvain Berfini
860371e698 Added top bar while in call to display paused calls 2024-07-15 16:10:35 +02:00
Sylvain Berfini
1d0abc4cb9 Attended transfer 2024-07-15 16:10:35 +02:00
Sylvain Berfini
d0f052177e Fixed sliding pane layout width on mobiles + pause ringtone player when leaving settings 2024-07-15 16:10:35 +02:00
Sylvain Berfini
429e8d2704 Added ended call fragment 2024-07-15 16:10:35 +02:00
Sylvain Berfini
464865c091 Shorten selected country in account creation spinner 2024-07-15 16:10:35 +02:00
Sylvain Berfini
7f5a9763e7 Fixed order when adding new contact from history 2024-07-15 16:10:35 +02:00
Sylvain Berfini
da707dda4b No longer copy logs URL in clipboard automatically, can be done from sharing screen if needed 2024-07-15 16:10:35 +02:00
Sylvain Berfini
2205ad1eb8 Replaced fat check by simple one in toasts 2024-07-15 16:10:35 +02:00
Sylvain Berfini
db722badaf Disable camera button when call is paused + show Paused / Paused by remote instead of chrono 2024-07-15 16:10:35 +02:00
Sylvain Berfini
e6387e124f Updated welcome pages with proper content 2024-07-15 16:10:35 +02:00
Sylvain Berfini
c72e2cb852 Added pause/resume from in-call extras actions + update available dialog + removed unused resources 2024-07-15 16:10:35 +02:00
Sylvain Berfini
e35b76d0f4 Updated icons according to latest changes 2024-07-15 16:10:35 +02:00
Sylvain Berfini
f00c21be91 Update recording indicator + added remote recording indicator 2024-07-15 16:10:35 +02:00
Sylvain Berfini
62fa5515e1 Updated colors & in-call header 2024-07-15 16:10:35 +02:00
Sylvain Berfini
e6287631aa Added country to prefix picker popup list 2024-07-15 16:10:35 +02:00
Sylvain Berfini
377f5000a5 Added animation + white background to settings page 2024-07-15 16:10:35 +02:00
Sylvain Berfini
82c041329b Added empty account settings fragment 2024-07-15 16:10:35 +02:00
Sylvain Berfini
f1c410deaa Added light/dark/auto theme setting 2024-07-15 16:10:35 +02:00
Sylvain Berfini
429553a6df Added ringtone player next to picker 2024-07-15 16:10:35 +02:00
Sylvain Berfini
2a1b2bf7ac Started ringtone picker 2024-07-15 16:10:35 +02:00
Sylvain Berfini
bdca32be49 Added call recording 2024-07-15 16:10:35 +02:00
Sylvain Berfini
95e8ef9fc4 Finished trust/distrust icons & border color 2024-07-15 16:10:35 +02:00
Sylvain Berfini
a2433956fe Started RecyclerViewSwipe 2024-07-15 16:10:35 +02:00
Sylvain Berfini
1fe8bad37b Moved some functions from LinphoneUtils to AppUtils 2024-07-15 16:10:35 +02:00
Sylvain Berfini
78eec3f6c8 Updated icon for active call in calls list 2024-07-15 16:10:35 +02:00
Sylvain Berfini
6d0886423c Show operation in progress view while loading contacts & call history for the first time 2024-07-15 16:10:35 +02:00
Sylvain Berfini
e4f4396a6b Added QR code remote provisioning 2024-07-15 16:10:35 +02:00
Sylvain Berfini
4336266b7f Disable account creation if push notifications aren't available 2024-07-15 16:10:35 +02:00
Sylvain Berfini
e5617d53ee Reworked & improved how we load contact pictures 2024-07-15 16:10:35 +02:00
Sylvain Berfini
a037fb5487 Updated away color 2024-07-15 16:10:35 +02:00
Sylvain Berfini
80c1b5722f Fixed dropdown overlap 2024-07-15 16:10:35 +02:00
Sylvain Berfini
95401fb8c4 Added edit/remove contact/account picture 2024-07-15 16:10:35 +02:00
Sylvain Berfini
42dd293aa8 Use dropdown for country picker 2024-07-15 16:10:35 +02:00
Sylvain Berfini
f4566ce812 Display currently selected country flag 2024-07-15 16:10:35 +02:00
Sylvain Berfini
cb20acfa52 Display unread messages & missed calls counters on each account in side menu 2024-07-15 16:10:35 +02:00
Sylvain Berfini
565e957387 Fixed issue when creating a new contact 2024-07-15 16:10:35 +02:00
Sylvain Berfini
9fcdb2bffa Made account devices remove button clickable 2024-07-15 16:10:35 +02:00
Sylvain Berfini
1c72196943 Bottom nav bar & corePreferences changes 2024-07-15 16:10:35 +02:00
Sylvain Berfini
845fd7ee03 Reverted scrollview, causing issues with recyclerview 2024-07-15 16:10:35 +02:00
Sylvain Berfini
5dc4c32eba Made history contact fragment scrollable so it is usable in landscape 2024-07-15 16:10:35 +02:00
Sylvain Berfini
d58e5f9fc2 Added contacts list filter popup menu 2024-07-15 16:10:35 +02:00
Sylvain Berfini
027e5dd61b Started blind call transfer 2024-07-15 16:10:35 +02:00
Sylvain Berfini
a60c66ad33 Added pause/resume action to long press menu in calls list + fixing issues with multi calls 2024-07-15 16:10:35 +02:00
Sylvain Berfini
51179c083c Not needed 2024-07-15 16:10:35 +02:00
Sylvain Berfini
ee8779cc00 Added in-call numpad 2024-07-15 16:10:35 +02:00
Sylvain Berfini
913e7cdb02 Fixed call duration timer when leaving & resuming active call fragment 2024-07-15 16:10:35 +02:00
Sylvain Berfini
46e06f2c9d Added long press menu to calls list 2024-07-15 16:10:35 +02:00
Sylvain Berfini
ad06f989b7 Fixed issue with foreground service & notifications 2024-07-15 16:10:35 +02:00
Sylvain Berfini
f5a4922aa3 Renamed VoiP to Call 2024-07-15 16:10:35 +02:00
Sylvain Berfini
856e3542e8 Renamed history related code & layout 2024-07-15 16:10:35 +02:00
Sylvain Berfini
8011bd997c Started calls list 2024-07-15 16:10:35 +02:00
Sylvain Berfini
f345db49cd Updated color + added ZRTP SAS validation in landscape 2024-07-15 16:10:35 +02:00
Sylvain Berfini
2896da7f9d Added back Android auto Manifest info 2024-07-15 16:10:35 +02:00
Sylvain Berfini
c314c4cba3 Updated notifications manager to allow mark as read & reply on chat messages notifications 2024-07-15 16:10:35 +02:00
Sylvain Berfini
686503c83c Improvements on search native contact when UI hasn't been started 2024-07-15 16:10:35 +02:00
Sylvain Berfini
fe3a448231 No longer need to do the iterate ourselves, the SDK can handle it on the proper thread now 2024-07-15 16:10:35 +02:00
Sylvain Berfini
1b2f6f4e3d Updated disabled color in accounts list in drawer menu 2024-07-15 16:10:35 +02:00
Sylvain Berfini
262d6d551b Fixed contact lookup from background 2024-07-15 16:10:35 +02:00
Sylvain Berfini
4d2d01195a Display account display name for which incoming call is being received if more than one account 2024-07-15 16:10:35 +02:00
Sylvain Berfini
a68977d9dd Hide numpad when leaving start call fragment 2024-07-15 16:10:35 +02:00
Sylvain Berfini
952bde0e96 Select TLS as default transport for third party SIP accounts 2024-07-15 16:10:35 +02:00
Sylvain Berfini
93bb1c0c96 Also hide video call button if video is disabled 2024-07-15 16:10:35 +02:00
Sylvain Berfini
544f37a75b Hide message button if chat is disabled 2024-07-15 16:10:35 +02:00
Sylvain Berfini
141c613eb9 Added confirmation dialog for contact removal & call logs removal 2024-07-15 16:10:35 +02:00
Sylvain Berfini
6faa7b4cb6 Added missing confirm dialog when leaving contact edit fragment 2024-07-15 16:10:35 +02:00
Sylvain Berfini
c4f666fdd6 Various changes 2024-07-15 16:10:35 +02:00
Sylvain Berfini
e90aa13890 Added flags in country adapter 2024-07-15 16:10:35 +02:00
Sylvain Berfini
cb6850f287 Improved notifications manager to remove notification when chat room has been read 2024-07-15 16:10:35 +02:00
Sylvain Berfini
58b93a9fa9 Increased back buttons size 2024-07-15 16:10:34 +02:00
Sylvain Berfini
809fd2f7ff Renamed colors 2024-07-15 16:10:34 +02:00
Sylvain Berfini
5813c5d9d8 Added proper error message for failed auth 2024-07-15 16:10:34 +02:00
Sylvain Berfini
62139047b3 Various UI changes 2024-07-15 16:10:34 +02:00
Sylvain Berfini
6974c53194 Fixed account profile bottom margin 2024-07-15 16:10:34 +02:00
Sylvain Berfini
3832299463 More error info in assistant 2024-07-15 16:10:34 +02:00
Sylvain Berfini
8f51de45d0 Updated links 2024-07-15 16:10:34 +02:00
Sylvain Berfini
ea5b9518ad Updated skip button from welcome activity to directly go to Assistant 2024-07-15 16:10:34 +02:00
Sylvain Berfini
d1029af180 Started new call fragment while in call 2024-07-15 16:10:34 +02:00
Sylvain Berfini
88c5e28577 Removed contact_avatar, use user_circle instead 2024-07-15 16:10:34 +02:00
Sylvain Berfini
ce8aee9192 Fixed find contact by phone number 2024-07-15 16:10:34 +02:00
Sylvain Berfini
08643df831 Added translatable strings for account profile devices 2024-07-15 16:10:34 +02:00
Sylvain Berfini
42a115e93b Improved RecyclerViewHeaderDecoration to make it sticky 2024-07-15 16:10:34 +02:00
Sylvain Berfini
9c9391c95b Merged contacts & suggestions in the same list 2024-07-15 16:10:34 +02:00
Sylvain Berfini
82ae513d17 Improved layout for landscape foldables 2024-07-15 16:10:34 +02:00
Sylvain Berfini
c3101e92b4 Created styles to avoid code duplication 2024-07-15 16:10:34 +02:00
Sylvain Berfini
14dca2f984 Improved reaction notification 2024-07-15 16:10:34 +02:00
Sylvain Berfini
d531a47c70 Prevent calls list to scroll to top at each refresh 2024-07-15 16:10:34 +02:00
Sylvain Berfini
47c2024f67 Removed code to alter nav bar color 2024-07-15 16:10:34 +02:00
Sylvain Berfini
5eb323c662 Added in-call top bar 2024-07-15 16:10:34 +02:00
Sylvain Berfini
2181c74b3e Updated signal strength alert due to changes in SDK 2024-07-15 16:10:34 +02:00
Sylvain Berfini
1665c4de22 Started devices in account profile 2024-07-15 16:10:34 +02:00
Sylvain Berfini
62cb1fb3f1 Added confirmation dialog for account removal 2024-07-15 16:10:34 +02:00
Sylvain Berfini
a2bd50ee22 Design changes 2024-07-15 16:10:34 +02:00
Sylvain Berfini
61365ff3c2 Changes related to TODO comments 2024-07-15 16:10:34 +02:00
Sylvain Berfini
86d9e25f17 Differentiate between WiFi and cellular signals in alerts 2024-07-15 16:10:34 +02:00
Sylvain Berfini
e0b146039f Refresh friend after edition + limit native contacts refresh to 1 per minute 2024-07-15 16:10:34 +02:00
Sylvain Berfini
54a299f23c Started settings 2024-07-15 16:10:34 +02:00
Sylvain Berfini
24a5ae8122 Added help layout 2024-07-15 16:10:34 +02:00
Sylvain Berfini
27063c9e45 Added profile mode fragment for account params 2024-07-15 16:10:34 +02:00
Sylvain Berfini
78e40807b1 Finished account profile 2024-07-15 16:10:34 +02:00
Sylvain Berfini
7fef6bde78 Updated account profile fragment, still work to do 2024-07-15 16:10:34 +02:00
Sylvain Berfini
7f78cf23d7 Started account profile mode (secure/default vs interop) 2024-07-15 16:10:34 +02:00
Sylvain Berfini
34424dfbc3 Renamed some drawables, removed some others 2024-07-15 16:10:34 +02:00
Sylvain Berfini
d51f25ea88 Added backspace button in numpad 2024-07-15 16:10:34 +02:00
Sylvain Berfini
c8ea1bcd8c Fixed issue with video call layout + auto accept video requests 2024-07-15 16:10:34 +02:00
Sylvain Berfini
036301e34f Code cleanup 2024-07-15 16:10:34 +02:00
Sylvain Berfini
e3022f42ab Removed legacy chat views 2024-07-15 16:10:34 +02:00
Sylvain Berfini
548a597843 Added missing androidx.media dependency 2024-07-15 16:10:34 +02:00
Sylvain Berfini
f6479826ca Added support of PiP 2024-07-15 16:10:34 +02:00
Sylvain Berfini
e4551713dc Fixed issue with call notifications & channels 2024-07-15 16:10:34 +02:00
Sylvain Berfini
18254fd385 Reworked in-call fragment to properly handle foldable phones 2024-07-15 16:10:34 +02:00
Sylvain Berfini
0dbc9f7a8a Moved strings to translatable file + use only toast layout 2024-07-15 16:10:34 +02:00
Sylvain Berfini
7e36ffc2b4 Make third part ysip account warning scrollable 2024-07-15 16:10:34 +02:00
Sylvain Berfini
16dc339b2d Improved toasts mechanism so they don't overlap 2024-07-15 16:10:34 +02:00
Sylvain Berfini
ae7c35c75d Fixed suggestions list not scrollable 2024-07-15 16:10:34 +02:00
Sylvain Berfini
5845c079cb Simplified generic fragment logic for going back 2024-07-15 16:10:34 +02:00
Sylvain Berfini
b6f8fb3354 Reverting, not really working 2024-07-15 16:10:34 +02:00
Sylvain Berfini
48db3d4aa2 Fixed sliding pane opening on resume issue 2024-07-15 16:10:34 +02:00
Sylvain Berfini
702504e1d5 Added logs & fixed nav graph id 2024-07-15 16:10:34 +02:00
Sylvain Berfini
619ab8a6ed Fixed issue with byNavGraphViewModel 2024-07-15 16:10:34 +02:00
Sylvain Berfini
594bcffd7c Added operation in progress layout 2024-07-15 16:10:34 +02:00
Sylvain Berfini
46dfd0bfa0 Bumped dependencies 2024-07-15 16:10:34 +02:00
Sylvain Berfini
bd523f307b Set +1 prefix by default 2024-07-15 16:10:34 +02:00
Sylvain Berfini
b220d979bd Re-use FlexiAPI token until it is consumed by account creation 2024-07-15 16:10:34 +02:00
Sylvain Berfini
85a904e173 Hide call controls when in full screen 2024-07-15 16:10:34 +02:00
Sylvain Berfini
16a467b7d1 Added country picker for assistant's account register 2024-07-15 16:10:34 +02:00
Sylvain Berfini
f4b6deb06a Layout changes for tablet 2024-07-15 16:10:34 +02:00
Sylvain Berfini
4b6ab58048 Fixes 2024-07-15 16:10:34 +02:00
Sylvain Berfini
b0a1ac3ee4 Fixed caret direction when in-call bottom sheet is opened by dragging the handle 2024-07-15 16:10:34 +02:00
Sylvain Berfini
8e1d22bbb9 Using permanent bottom sheet for in-call extras actions as well 2024-07-15 16:10:34 +02:00
Sylvain Berfini
dc01abf48f Added missing trims 2024-07-15 16:10:34 +02:00
Sylvain Berfini
fa7c6907be Reworked navigation 2024-07-15 16:10:34 +02:00
Sylvain Berfini
cbc46ed2e0 Improved dialer by using permanent bottom sheet 2024-07-15 16:10:34 +02:00
Sylvain Berfini
f0e39e92b7 Fixed issue in number picker 2024-07-15 16:10:34 +02:00
Sylvain Berfini
db3117b92e Compute & show last seen online at + close sip address/phone number picker dialog when used 2024-07-15 16:10:34 +02:00
Sylvain Berfini
3f6339887b Fixed no avatar issue in calls list since it is the default fragment 2024-07-15 16:10:34 +02:00
Sylvain Berfini
8057e9d0af Update friends lists subscription when switching default account 2024-07-15 16:10:34 +02:00
Sylvain Berfini
b08aa2ae1f Added clickable links to conditions & privacy policy dialog 2024-07-15 16:10:34 +02:00
Sylvain Berfini
a7a22f39d2 Show privacy policy & conditions accept dialog if not accepted yet when creating an account or logging in a third party one 2024-07-15 16:10:34 +02:00
Sylvain Berfini
6f416ab33f Set calls as start fragment 2024-07-15 16:10:34 +02:00
Sylvain Berfini
17ef0a5ca7 Improved back nav 2024-07-15 16:10:34 +02:00
Sylvain Berfini
643a2be9a2 Fixed drawer menu accounts list not refreshing avatar 2024-07-15 16:10:34 +02:00
Sylvain Berfini
29a47b87cd Made accounts switch work 2024-07-15 16:10:34 +02:00
Sylvain Berfini
36b5430861 Added third party SIP account login form 2024-07-15 16:10:34 +02:00
Sylvain Berfini
4f03015486 Added third party sip account login warning fragment 2024-07-15 16:10:34 +02:00
Sylvain Berfini
7b48d41d29 Using new APIs 2024-07-15 16:10:34 +02:00
Sylvain Berfini
2fe1bcbdff Started account creation 2024-07-15 16:10:34 +02:00
Sylvain Berfini
489483e22a Add back buttons in new fragments 2024-07-15 16:10:34 +02:00
Sylvain Berfini
89c7e734d4 Changes for friends list storage 2024-07-15 16:10:34 +02:00
Sylvain Berfini
b93f75aade Improved account avatar 2024-07-15 16:10:34 +02:00
Sylvain Berfini
157c233ab1 Fixed issue with abort confirmation dialog in new contact fragment 2024-07-15 16:10:34 +02:00
Sylvain Berfini
72e7445f87 Removed Friend aggregation for suggestions 2024-07-15 16:10:34 +02:00
Sylvain Berfini
ccf7ff82a1 Show SIP address & added international prefix setting in account profile 2024-07-15 16:10:34 +02:00
Sylvain Berfini
8342259054 Bumped min SDK to Android 9.0 2024-07-15 16:10:34 +02:00
Sylvain Berfini
abd5b865e1 Changes for assistant 2024-07-15 16:10:34 +02:00
Sylvain Berfini
973afe08c5 Added numpad 2024-07-15 16:10:34 +02:00
Sylvain Berfini
3201655870 Factorized code to fix issue in sip/number picking in start call fragmet 2024-07-15 16:10:34 +02:00
Sylvain Berfini
c051d28c95 Improvements 2024-07-15 16:10:34 +02:00
Sylvain Berfini
ab151cc409 Fixed performances issue by replacing suggestions recyclerview by linearlayout... 2024-07-15 16:10:34 +02:00
Sylvain Berfini
3c94068910 Layout of start call as intended but perfomances not great... 2024-07-15 16:10:34 +02:00
Sylvain Berfini
544ae39a95 Added clear field button on start call filter 2024-07-15 16:10:34 +02:00
Sylvain Berfini
26c79e6740 Removed TopBarFragment, simply include layout and have the view model inherits TopBarViewModel (now abstract) 2024-07-15 16:10:34 +02:00
Sylvain Berfini
cb48f73fd9 Started contacts filter button 2024-07-15 16:10:34 +02:00
Sylvain Berfini
728fa279dd Added QR code scanner 2024-07-15 16:10:34 +02:00
Sylvain Berfini
3d5ca3313b Do not use CoreService as it will attempt to use the Core on the main thread, TODO : disable activity monitor in SDK 2024-07-15 16:10:34 +02:00
Sylvain Berfini
ca080b2dcc Added underlines 2024-07-15 16:10:34 +02:00
Sylvain Berfini
476eabd0fe Trying to route audio using androidx Telecom API... 2024-07-15 16:10:34 +02:00
Sylvain Berfini
39ad8347c7 Started playing around with alerts 2024-07-15 16:10:34 +02:00
Sylvain Berfini
294410bfd2 Added first start presentation caroussel 2024-07-15 16:10:34 +02:00
Sylvain Berfini
92b3d08ea0 Using new androix telecom library 2024-07-15 16:10:34 +02:00
Sylvain Berfini
91b049e3a7 Using recyclerview for call log details to improve performances 2024-07-15 16:10:34 +02:00
Sylvain Berfini
5af3a490d4 Added contact export as vCard from contacts list 2024-07-15 16:10:34 +02:00
Sylvain Berfini
855b03fb34 Hide 'invite' menu when phone number has presence info 2024-07-15 16:10:34 +02:00
Sylvain Berfini
bb55246197 Fixed contac/call log showing when going back from account profile + added logs 2024-07-15 16:10:33 +02:00
Sylvain Berfini
395e43a3e6 Added decorators on CorePreference 2024-07-15 16:10:33 +02:00
Sylvain Berfini
656377875b Added gradient background to start group call button 2024-07-15 16:10:33 +02:00
Sylvain Berfini
32c5f56be1 Added confirmation dialog when trying to call a particular contact device to increase trust 2024-07-15 16:10:33 +02:00
Sylvain Berfini
ea155cd68c Improved shapes 2024-07-15 16:10:33 +02:00
Sylvain Berfini
7016afa6f8 Added trust explanation dialog in contact detail + improved DialogUtils class 2024-07-15 16:10:33 +02:00
Sylvain Berfini
cc99d4f93f Fixed various issues linked to smart address book 2024-07-15 16:10:33 +02:00
Sylvain Berfini
424d805def Fixed trust icon not visible when leaving & going back into call 2024-07-15 16:10:33 +02:00
Sylvain Berfini
cb9c43c2e7 Simple filter for now on call logs 2024-07-15 16:10:33 +02:00
Sylvain Berfini
ca5648ed03 Added output audio devices selection menu 2024-07-15 16:10:33 +02:00
Sylvain Berfini
e0f6121dc9 Improvements for video calls 2024-07-15 16:10:33 +02:00
Sylvain Berfini
c79f2896da Started audio routes 2024-07-15 16:10:33 +02:00
Sylvain Berfini
a02896333a Changes 2024-07-15 16:10:33 +02:00
Sylvain Berfini
409e658f80 New icons from phosphoricons library + moved blue toast from fragment to activty 2024-07-15 16:10:33 +02:00
Sylvain Berfini
0c8bd49908 Started notifications manager for calls 2024-07-15 16:10:33 +02:00
Sylvain Berfini
5e150ee16a Started missed calls count in bottom nav bar 2024-07-15 16:10:33 +02:00
Sylvain Berfini
09bbe05f08 Started bottom nav bar viewmodel + scroll to top when adding new items to calls history list 2024-07-15 16:10:33 +02:00
Sylvain Berfini
60965b767c Added missing add / go to contact action from calls list context menu + added logs 2024-07-15 16:10:33 +02:00
Sylvain Berfini
64c29d495d Improved switch between contacts & calls lists with postpone at parent level, added animation for account profile fragment 2024-07-15 16:10:33 +02:00
Sylvain Berfini
52238e5d27 Fixed a few things with AvatarView 2024-07-15 16:10:33 +02:00
Sylvain Berfini
1a9e11a258 Fixed show/hide password toggle 2024-07-15 16:10:33 +02:00
Sylvain Berfini
14d1726a7f Disable save contact if no firstname or lastname is set + when adding contact from call history, pre-propulate SIP address 2024-07-15 16:10:33 +02:00
Sylvain Berfini
5ebaf24c04 Disable phone numbers if default account is in secure mode 2024-07-15 16:10:33 +02:00
Sylvain Berfini
8bba5ea2b6 Added thread annotations to constructors + some missing other ones 2024-07-15 16:10:33 +02:00
Sylvain Berfini
9e259f02d8 Improved local friends / avatars / etc... 2024-07-15 16:10:33 +02:00
Sylvain Berfini
ce2a9b0bde Preventing crash due to keyboard visibility listener + improved suggestions list 2024-07-15 16:10:33 +02:00
Sylvain Berfini
bfe56579aa Improved start call lists behavior when doing search 2024-07-15 16:10:33 +02:00
Sylvain Berfini
10bd90ab18 Fixed issue with picture 2024-07-15 16:10:33 +02:00
Sylvain Berfini
cd3a4e0e63 Update local friend & refresh UI after making a change in UI profile 2024-07-15 16:10:33 +02:00
Sylvain Berfini
8b1057b97d Fixed issues with margins and pressed effect 2024-07-15 16:10:33 +02:00
Sylvain Berfini
d915e97ad2 Started account profile view 2024-07-15 16:10:33 +02:00
Sylvain Berfini
64855cba77 Added speaker off/on icons 2024-07-15 16:10:33 +02:00
Sylvain Berfini
9e4c71a3f0 Removed parenthesis after phone number if no label + added missing fragments (empty) 2024-07-15 16:10:33 +02:00
Sylvain Berfini
4c2b67a5aa Added pressed effect on some actions & menus 2024-07-15 16:10:33 +02:00
Sylvain Berfini
bde1258c87 Added user-input as suggestion when starting a new call 2024-07-15 16:10:33 +02:00
Sylvain Berfini
78edc79fc2 Using annotations to check which method is called from which thread 2024-07-15 16:10:33 +02:00
Sylvain Berfini
9ad121f7d7 Started new call fragment 2024-07-15 16:10:33 +02:00
Sylvain Berfini
407e474896 Added back log for which SDK is being used 2024-07-15 16:10:33 +02:00
Sylvain Berfini
c2a74df26d Hiding conversations & meetings for now 2024-07-15 16:10:33 +02:00
Sylvain Berfini
4018155899 Added activity monitor & presence status 2024-07-15 16:10:33 +02:00
Sylvain Berfini
a10f416f15 Added navigation from call log to contact 2024-07-15 16:10:33 +02:00
Sylvain Berfini
4ac78c5b30 Added confirmation dialog when going back during new/edit contact 2024-07-15 16:10:33 +02:00
Sylvain Berfini
838f9f592c Added SMS invite menu & feature 2024-07-15 16:10:33 +02:00
Sylvain Berfini
f13dceaa34 More work on contacts list context menu 2024-07-15 16:10:33 +02:00
Sylvain Berfini
ab15d05ffd Added sharing/removal of contact 2024-07-15 16:10:33 +02:00
Sylvain Berfini
715e6dc8be Fixed focus issue in contact editor 2024-07-15 16:10:33 +02:00
Sylvain Berfini
4a98610b67 Added contact image picker + favourite toggle 2024-07-15 16:10:33 +02:00
Sylvain Berfini
f4a53bee61 Started to allow for SIP address & phone number in new/edit contact form 2024-07-15 16:10:33 +02:00
Sylvain Berfini
cb1774a678 Added back buttons to assistant 2024-07-15 16:10:33 +02:00
Sylvain Berfini
0543ac33d2 Added no calls image & label 2024-07-15 16:10:33 +02:00
Sylvain Berfini
e3b5f0cc77 Various improvements 2024-07-15 16:10:33 +02:00
Sylvain Berfini
74a15cd0a1 Started call history detail popup menu 2024-07-15 16:10:33 +02:00
Sylvain Berfini
7c4b6d5b20 Improved in-call buttons size + toast above video 2024-07-15 16:10:33 +02:00
Sylvain Berfini
627a0d6f9e Added empty start call fragment 2024-07-15 16:10:33 +02:00
Sylvain Berfini
464e8b4899 Fixed duplicated SIP addresses issue 2024-07-15 16:10:33 +02:00
Sylvain Berfini
b2a89a46ca Added delete all call logs menu & dialog 2024-07-15 16:10:33 +02:00
Sylvain Berfini
8fcf3f1baa Added account menu popup 2024-07-15 16:10:33 +02:00
Sylvain Berfini
405ab20ab2 Limit contacts search to linphone contacts 2024-07-15 16:10:33 +02:00
Sylvain Berfini
e5bbe3a553 Reworked presence badge 2024-07-15 16:10:33 +02:00
Sylvain Berfini
5e1c681a8d Started account login & proper display of accounts 2024-07-15 16:10:33 +02:00
Sylvain Berfini
faa4309ece Started contact editor 2024-07-15 16:10:33 +02:00
Sylvain Berfini
79ca1523ef Show SIP address extracted from phone number through smart addressbook 2024-07-15 16:10:33 +02:00
Sylvain Berfini
8be39a6871 Updated list of call log icon depending on status and dir + text color 2024-07-15 16:10:33 +02:00
Sylvain Berfini
cc4bc0c3b0 Edit native contacts in native contact app 2024-07-15 16:10:33 +02:00
Sylvain Berfini
2ef56c0cbb Added delete call log + started top dots menu 2024-07-15 16:10:33 +02:00
Sylvain Berfini
8d74b8f133 Added copy to clipboard in call history list 2024-07-15 16:10:33 +02:00
Sylvain Berfini
8b446e2de0 Improved toast layout 2024-07-15 16:10:33 +02:00
Sylvain Berfini
8882cd9558 Added pressed/disabled effect on some buttons 2024-07-15 16:10:33 +02:00
Sylvain Berfini
174b8923dc Reworked top bar as fragment 2024-07-15 16:10:33 +02:00
Sylvain Berfini
26cd91ceb2 Small fixes 2024-07-15 16:10:33 +02:00
Sylvain Berfini
3a50ab3b91 Added call history in call log details 2024-07-15 16:10:33 +02:00
Sylvain Berfini
0a83695c45 Started call logs 2024-07-15 16:10:33 +02:00
Sylvain Berfini
571fa8c885 Added support to job title 2024-07-15 16:10:33 +02:00
Sylvain Berfini
1901a8535a Started login/create account 2024-07-15 16:10:33 +02:00
Sylvain Berfini
a23ffdcd1f Updated app icon, started drawer menu content 2024-07-15 16:10:33 +02:00
Sylvain Berfini
970652d81f Added video calls 2024-07-15 16:10:33 +02:00
Sylvain Berfini
6124cdd806 Fixed chrono 2024-07-15 16:10:33 +02:00
Sylvain Berfini
97a87c718a Improved toasts 2024-07-15 16:10:33 +02:00
Sylvain Berfini
f368114b88 Added first version of ZRTP SAS confirmation dialog 2024-07-15 16:10:33 +02:00
Sylvain Berfini
c4b0bf0ee0 Started call actions 2024-07-15 16:10:33 +02:00
Sylvain Berfini
fb9acf8da4 Added green & blue toasts + animation (added copy number to clipboard feature) 2024-07-15 16:10:33 +02:00
Sylvain Berfini
2dfc8f930e Updated sizes 2024-07-15 16:10:33 +02:00
Sylvain Berfini
68c132b003 Started outgoing calls 2024-07-15 16:10:33 +02:00
Sylvain Berfini
c2292fdadc Commented out workaround for now 2024-07-15 16:10:33 +02:00
Sylvain Berfini
0e7f00cddd Improved navigation 2024-07-15 16:10:33 +02:00
Sylvain Berfini
0cca9f8152 Updated headers 2024-07-15 16:10:33 +02:00
Sylvain Berfini
d8a6bf08cb Using font and fontWeight in style 2024-07-15 16:10:32 +02:00
Sylvain Berfini
6c8216d360 No need for third partly lib to display devices trust progress 2024-07-15 16:10:32 +02:00
Sylvain Berfini
f365a43f7b Added number/address picker dialog 2024-07-15 16:10:32 +02:00
Sylvain Berfini
af75f4c3a3 Fixed drawer opened to latest seen contact when navigating back 2024-07-15 16:10:32 +02:00
Sylvain Berfini
84e05e9490 Started calls list fragments 2024-07-15 16:10:32 +02:00
Sylvain Berfini
4cc2a11cc5 Updated self avatar with trusted info 2024-07-15 16:10:32 +02:00
Sylvain Berfini
2565d8155d Started devices trust in contact details 2024-07-15 16:10:32 +02:00
Sylvain Berfini
e147efd358 Added long press context menu to phone number & sip address in contact details 2024-07-15 16:10:32 +02:00
Sylvain Berfini
7c0f9585e7 Improved emoji compat use for contacts 2024-07-15 16:10:32 +02:00
Sylvain Berfini
8bb88f397e More work on contact details 2024-07-15 16:10:32 +02:00
Sylvain Berfini
11f4ff4594 Added GenericFragment logic from current app 2024-07-15 16:10:32 +02:00
Sylvain Berfini
29298ef978 Small UI change 2024-07-15 16:10:32 +02:00
Sylvain Berfini
437fa5c128 Added favourites contacts list 2024-07-15 16:10:32 +02:00
Sylvain Berfini
f104d5c891 Moved fragments in dedicated folders + started calls by creating empty files 2024-07-15 16:10:32 +02:00
Sylvain Berfini
5803fe18ed Trying a different architecture to show fragments correctly depending on device size & orientation 2024-07-15 16:10:32 +02:00
Sylvain Berfini
6cccf09bc1 Started favourites 2024-07-15 16:10:32 +02:00
Sylvain Berfini
bb22845b54 Changes to use shared main view model for sliding pane state 2024-07-15 16:10:32 +02:00
Sylvain Berfini
f3233528d6 Reverted some previous changes 2024-07-15 16:10:32 +02:00
Sylvain Berfini
319a971080 Locking orientation once app has started 2024-07-15 16:10:32 +02:00
Sylvain Berfini
0a978f07d9 Started contact details 2024-07-15 16:10:32 +02:00
Sylvain Berfini
4919347095 Started contacts list context menu 2024-07-15 16:10:32 +02:00
Sylvain Berfini
9f943fcaa5 Started contacts list header 2024-07-15 16:10:32 +02:00
Sylvain Berfini
7c8d11ca20 Using new lib to display avatars 2024-07-15 16:10:32 +02:00
Sylvain Berfini
1d9684e11e Started to display contacts 2024-07-15 16:10:32 +02:00
Sylvain Berfini
7e46fb8720 Started slidingpanel 2024-07-15 16:10:32 +02:00
Sylvain Berfini
3bceafef80 Use AppCompatTextView + only one bottom nav bar layout 2024-07-15 16:10:32 +02:00
Sylvain Berfini
5dfd04ad70 Changes 2024-07-15 16:10:32 +02:00
Sylvain Berfini
bd51fe383b Re-organized a few things 2024-07-15 16:10:32 +02:00
Sylvain Berfini
58d2362390 Fixed keyboard & contacts search bar 2024-07-15 16:10:32 +02:00
Sylvain Berfini
254cf3d9cf Open/close keyboard when search bar is made visible/gone 2024-07-15 16:10:32 +02:00
Sylvain Berfini
09bbe650e3 Improved bottom bar layouts 2024-07-15 16:10:32 +02:00
Sylvain Berfini
552459310c Added safe args navigation plugin + using shared element transition to maintain nav bar in place when navigating horizontally but it breaks the horizontal animation, removed them for now 2024-07-15 16:10:32 +02:00
Sylvain Berfini
137dba1bb4 Empty new contact fragment + nav from contacts to chats 2024-07-15 16:10:32 +02:00
Sylvain Berfini
f7f5a3cf50 Started contacts list 2024-07-15 16:10:32 +02:00
Sylvain Berfini
2aeaf330a8 Removed navigation from MainActivity 2024-07-15 16:10:32 +02:00
Sylvain Berfini
bd42eebdcb Do not use by lazy in Datas, will be probably called by UI thread instead of Core thread 2024-07-15 16:10:32 +02:00
Sylvain Berfini
78d55d6c78 Removed hardcoded account, improved things in chat rooms list 2024-07-15 16:10:32 +02:00
Sylvain Berfini
41ea5e4cc7 Moved files around, started bubbles 2024-07-15 16:10:32 +02:00
Sylvain Berfini
33a33867b5 Started chat room layout 2024-07-15 16:10:32 +02:00
Sylvain Berfini
7b71f32d18 Improvements 2024-07-15 16:10:32 +02:00
Sylvain Berfini
c77fae435f Added contacts avatars 2024-07-15 16:10:32 +02:00
Sylvain Berfini
369a6f1977 Finally found a way to do proper animation 2024-07-15 16:10:32 +02:00
Sylvain Berfini
d5c78f58c0 Started new conversation fragment 2024-07-15 16:10:32 +02:00
Sylvain Berfini
a2d038eb46 Fixes & improvements 2024-07-15 16:10:32 +02:00
Sylvain Berfini
09ca9b5351 Added menu 2024-07-15 16:10:32 +02:00
1011 changed files with 113632 additions and 1843 deletions

View file

@ -39,7 +39,7 @@ If you are using a SDK that isn't the latest release, please update first as it'
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.
Click on "Share logs" in Help -> Troubleshooting view and copy/paste the link here.
It's also explained [in the README](https://github.com/BelledonneCommunications/linphone-android#behavior-issue).

11
.gitignore vendored
View file

@ -1,15 +1,14 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
app/debug/
app/release/
.idea/
app/bc-android.keystore
.kotlin/

View file

@ -0,0 +1,35 @@
job-android:
stage: build
tags: [ "docker-android" ]
image: gitlab.linphone.org:4567/bc/public/linphone-android/bc-dev-android-36:20260120_trixie_java21_android36_gradle9
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
- if ! [ -z ${ANDROID_SETTINGS_GRADLE+x} ]; then echo "$ANDROID_SETTINGS_GRADLE" > settings.gradle.kts; fi
- git config --global --add safe.directory /builds/BC/public/linphone-android
script:
- 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 clean
- ./gradlew assembleDebug
- ./gradlew assembleRelease
artifacts:
paths:
- ./app/build/outputs/apk/release/linphone-android-release-*.apk
when: always
expire_in: 1 day
.scheduled-job-android:
extends: job-android
only:
- schedules

View file

@ -0,0 +1,20 @@
job-android-upload:
stage: deploy
tags: [ "docker-deploy" ]
only:
- schedules
dependencies:
- job-android
before_script:
- if ! [ -z ${SCP_PRIVATE_KEY+x} ] && ! [ -z ${DEPLOY_SERVER_HOST_KEYS+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
- if ! [ -z ${DEPLOY_SERVER_HOST_KEYS+x} ]; then mkdir -p ~/.ssh && chmod 700 ~/.ssh; fi
- if ! [ -z ${DEPLOY_SERVER_HOST_KEYS+x} ]; then echo "$DEPLOY_SERVER_HOST_KEYS" >> ~/.ssh/known_hosts; fi
script:
# Launches rsync in partial mode, which means that we are using a temp_dir in case of a transfer issue
# Upon a job relaunch, the files in temp_dir would then be re-used, and deleted if the transfer succeeds
- cd app/build/outputs/apk/ && rsync --partial --partial-dir=$CI_PIPELINE_ID_$CI_JOB_NAME ./release/*.apk $DEPLOY_SERVER:$ANDROID_DEPLOY_DIRECTORY

19
.gitlab-ci.yml Normal file
View 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

123
.idea/codeStyles/Project.xml generated Normal file
View file

@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

2
.idea/compiler.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
</component>
</project>

19
.idea/gradle.xml generated
View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

6
.idea/kotlinc.xml generated
View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0-RC" />
</component>
</project>

3
.idea/misc.xml generated
View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View file

@ -10,14 +10,436 @@ Group changes to describe their impact on the project, as follows:
Fixed for any bug fixes.
Security to invite users to upgrade in case of vulnerabilities.
## [6.1.0] - Unreleased
### Added
- Added the ability to edit/delete chat messages sent less than 24 hours ago.
- Added keyboard shortcuts on IncomingCallFragment: Ctrl + Shift + A to answer the call, Ctrl + Shift + D to decline it
- Added seeking feature to recordings & media player within app
- Added PDF preview in conversation (message bubble & documents list)
- Added hover effect when using a mouse (useful for tablets or devices with desktop mode)
- Support right click on some items to open bottom sheet/menu
- Added toggle speaker action in active call notification
- Increased text size for chat messages that only contains emoji(s)
- Use user-input to filter participants list after typing "@" in conversation send area
- Handle read-only CardDAV address books, disable edit/delete menus for contacts in read-only FriendList
- Added swipe/pull to refresh on contacts list of a CardDAV addressbook has been configured to force the synchronization
- Show information to user when filtering contacts doesn't show them all and user may have to refine it's search
- Show Android notification when an account goes to failed registration state (only when background mode is enabled)
- New settings:
- one for user to choose whether to sort contacts by first name or last name
- one to hide contacts that have neither a SIP address nor a phone number
- one to let app auto-answer call with video sending already enabled
- one to let edit native contacts Linphone copy in-app instead of opening native addressbook third party app
- Added a vu meter for recording & playback volumes (must be enabled in developer settings)
- Added support for HDMI audio devices
### Changed
- No longer follow TelecomManager audio endpoint during calls, using our own routing policy
- Join a conference using default layout instead of audio only when clicking on a meeting SIP URI
- Removing an account will also remove all related data in the local database (auth info, call logs, conversations, meetings, etc...)
- Hide SIP address/phone number picker dialog if contact has exactly one SIP address matching both the app default domain & the currently selected account domain
- Hide SIP address associated to phone number through presence mecanism in contact details & editor views.
- Improved UI on tablets with screen sw600dp and higher, will look more like our desktop app
- Improved navigation within app when using a keyboard
- Now loading media/documents contents in conversation by chunks (instead of all of them at once)
- Simplified audio device name in settings
- Reworked some settings (moved calls related ones from advanced settings to advanced calls settings)
- Increased shared media preview size in chat
- Un-encrypted conversation warning will be more visible for accounts that support end-to-end encrypted conversations
- Made numpad buttons larger by changing their shape
- All LDAP fields are mandatory now
- Improved how Android shortcuts are created
- Permission fragment will only show missing ones
- Added more info into StartupListener logs
- Updated password forgotten procedure, will use online account manager platform
### Fixed
- Copy raw message content instead of modified one when it contains a participant mention ("@username")
## [6.0.22] - 2026-01-20
### Changed
- Close search bar when opening bottom sheet and vice versa
### Fixed
- Sending a file from another app using Android shortcut not working if conversation was already opened
- Trying to workaround an issue where ForegroundService notification isn't displayed in the allowed timeframe, causing an Exception
## [6.0.21] - 2025-12-16
### Added
- Allow linphone-config: scheme URIs in in-app QR code scanner
### Changed
- Workaround for audio focus & audio manager mode on devices that do not support TelecomManager APIs
- Set front camera as default after using back camera when scanning a QR code
- Added back largeHeap flag in AndroidManifest.xml
### Fixed
- Fixed call recording indicator not showing local record in progress in case UPDATE isn't answered
- Fixed native addressbook reload when a contact is updated in the OS default app
- Fixed issue with linphone-config scheme URIs if scheme is followed by "//"
- Fixed Job & Company contact field not updated if field content was removed
- Fixed local avatar not displayed when calling ourselves
- Prevent crashes due to some ActivityNotFound exceptions
- Prevent crash due to empty clipboard on some devices
## [6.0.20] - 2025-11-21
### Changed
- Added shrink resources to release config in gradle
### Fixed
- Remove AuthInfo when configuring a CardDAV friend list if synchronization fails
- Added missing toast when starting a group call or meeting if there's an issue
- Fixed crash in RecordingPlayerFragment due to used lateinit property before it's initialized
## [6.0.19] - 2025-10-16
### Added
- Spanish and Slovakian translations thanks to Weblate contributors
### Changed
- SIP addresses domain hidden in Suggestions if it matches the currently selected account SIP identity domain
- Start proximity sensor when an incoming call is answered from the notification (disabling screen when device is near)
### Fixed
- Black screen when trying to scan a QR Code right after granting CAMERA permission (only happened on some devices)
- Possible crash due to ConcurrentModificationException
- Camera preview in conference that was black sometimes after switching layout
- Possibly wrong screen sharing participant name in conference
- Presence SUBSCRIBE that was only sent for sip.linphone.org accounts
- Keyboard suggestions in participant picker textfield
- Account labelled as Disabled instead of Disconnected when network isn't reachable
- Suggestions generated avatar if username starts with '+'
- Two LDAP fields label where swapped
## [6.0.18] - 2025-09-15
### Added
- Added menu icon next to currently selected account avatar to make the drawer menu easier to understand
- Added missing dialpad floating action button in the call transfer fragment
### Changed
- Improved bodyless friendlist presence process when it's received
### Fixed
- Fixed "End-to-end encrypted call" label while in conference, the call may be end-to-end encrypted but only to the conference server, not to all participants
- Fixed missing meeting subject when calling the conference SIP URI if the conference info doesn't exist yet
- Finish CallActivity if no call is found when trying to answer/decline a call from the IncomingCallFragment
- Prevent empty screen when rotating the device and clicking on the empty part next to the list while in landscape and then rotating the device back to portrait
## [6.0.17] - 2025-09-02
### Changed
- Portuguese translation updated from Weblate (still not complete)
### Fixed
- Vibrator not stopped when call is terminated sometimes (SDK fix)
- Chat conversation not visible sometimes (SDK fix)
## [6.0.16] - 2025-08-25
## Added
- Access to Help/Troubleshooting pages from Assistant
## Fixed
- Some Core methods being called from UI thread causing either a crash or a deadlock sometimes
- Scrolling issue when doing a search in a conversation with only one result
- Contacts not updated after body less presence notify was received
- VFS issue due to encrypted.pref file being backed up by Android OS
## [6.0.15] - 2025-08-11
### Fixed
- Crash due to changes in SDK triggering fatal error if linphone_core_stop() is called from linphone_core_iterate() loop (which was done when scanning QR code)
### Changed
- Prevent leaving assistant after doing a remote provisioning if there is still no account after it (if there was no account before and no account was provided in downloaded config)
## [6.0.14] - 2025-08-06
### Fixed
- Fixed ANR due to deadlock caused by method being called from wrong thread
- Fixed microphone not always recording audio while app in background or if screen is turned off
- Fixed missing favorites in start call / create conversation views
- Fixed outgoing call view in full screen
- Fixed generated avatar for SIP URIs without username
## [6.0.13] - 2025-07-31
### Fixed
- Missing favourites if contacts list size exceeds magic search max results setting
- Muted call on some devices due to Telecom Manager quickly muting/unmuting call
- Full screen without video during outgoing early media call if video has been declined by remote end
- Removed duplicated week label if "no meeting today" is the first entry for current week
- Prevent crash during file export if no app on the device can handle it
- Prevent crash that could happen with chat message notification if sender name (or group chat room subject) is empty
### Changed
- Back gesture / navigation button will close the numpad bottom sheet if it's open instead of leaving the page directly
- Updated bell and bell_slash icons
## [6.0.12] - 2025-07-18
### Fixed
- Reactions list in bottom sheet update while opened
- Crashes due to late init properties being used before initialized
## [6.0.11] - 2025-07-11
### Added
- Added toggle in LDAP configuration to allow to quickly enable/disable it
### Changed
- Reduced maximum number of contacts displayed in contacts list, new call/conversation, meeting participant selection etc...
- Updated translations
### Fixed
- Calls top bar wrong notification label when going from two calls to one.
## [6.0.10] - 2025-06-27
### Added
- Added a new top bar alert area for pending file/text sharing.
### Changed
- Reworked in-app top bar alerts, now can show both an account alert and an active call alert.
- Hide SIP address/phone number picker dialog if contact has exactly one SIP address matching the default domain and currently default account domain.
### Fixed
- Bluetooth not being used automatically when device is connected during a call.
- Call encryption status label stuck in "Waiting for encryption".
- Group chat room creation if LIME server URL isn't set.
- Participant mention if more than one in the same chat message.
- Force default account in call params when starting one.
## [6.0.9] - 2025-06-06
### Added
- German translation (88% complete)
- Link to user guide in Help section
- Missing scroll views for help & debug layouts
### Changed
- Prevent port from being set in the SIP identity address in third party account login + remove port (if any) from SIP identity for existing accounts
- Show last message timestamp instead of conversation last updated timestamp in conversations list
### Fixed
- Prevent blinking in conversations list when removing message from chat room
- Prevent empty (can even lead to crash) display name in call notification (using all identification fields from vCard)
## [6.0.8] - 2025-05-23
### Added
- Ukrainian & simplified Chinese translations from Weblate
- Sliding answer/decline button in incoming call fragment if device is locked (will help prevent calls from being unintentionally picked up or hung up while the device is being removed from a pocket)
### Changed
- Show files with square design when more than one (as it is for media files)
- Outgoing chat bubbles will now display the sent file size (as it is for received messages)
### Fixed
- Fixed issue with bluetooth hearing aids
- Fixed audio call being answered on speakerphone
- Fixed events related to joined/left conversation being briefly visible sometimes for 1-1 conversations
- Fixed files/media grid in chat bubble using more than 3 columns in landscape
- Fixed logs upload server URL setting
## [6.0.7] - 2025-05-16
### Added
- CS, NL and RU translations from Weblate
### Changed
- Improved find contact performances
- Make sure speaker audio device is used for playing the ringtone during early media
- Reworked bottom navigation bar in portrait and unread count indicators
- No longer delete conversations when deleting account (for now); causes user to leave group which is an issue when using multiple devices
### Fixed
- Fixed no default account after remote provisioning
- Prevent lists from refreshing too many times when using LDAP or remote CardDAV contact directories
- Fixed black miniatures in conference if bundle mode is disabled in account params
- Fixed long press on a chat message containing a SIP URI triggering call
- Disable IMDN bottom sheet for incoming messages in groups instead of showing it empty
- Refresh conversations list after clearing conversation history
- Fixed another race condition issue related to foreground call service
## [6.0.6] - 2025-05-02
### Added
- Added recover phone account when clicking on "Forgotten password" in the assistant
- Improved message when contacts list is empty depending on the currently selected filter and added a button to open the filter popup menu for users that didn't notice the icon on the top right corner of the screen when contacts list is empty and "SIP contacts only" filter is set.
- Added "Logs collection sharing server URL" setting in developper area
- Added "Disable sending logs to Crashlytics" advanced setting.
### Changed
- Improved VFS message in confirmation dialog
- Moved "Print logs in logcat" and "File sharing server URL" settings to developper area
### Fixed
- Fixed crash when opening a password protected PDF
- Fixed chat room lookup while in 1-1 call, using SDK method for getting chat room from conference
- Fixed newly created contact not being visible in contacts list without reloading it
- Fixed missing event icon for group conversations
- Another attempts at preventing crashes due to In-Call service not being started as foreground before being stopped
## [6.0.5] - 2025-04-18
### Changed
- When calling a SIP URI that looks like a phone number in the username and an IP in the domain, replace the domain with the one of the currently selected account to workaround issue with PBXs using IPs instead of domains in From header
- Improved account creation page UI when push notifications aren't available
- Improved called account display on incoming call screen when more than one account configured
- Updated telecom package from beta to release candidate
### Fixed
- Fixed transfer call view numpad button starting a new call instead of forwarding the current one
- Fixed incoming call not displayed in call history depending on how the From & To headers are formatted (SDK fix)
- Fixed crashes related to foreground service not being started
- Fixed crash due to lateinit property not being initialized before used
## [6.0.4] - 2025-04-11
### Changed
- Third party SIP accounts push notifications will be disabled and setting will be hidden unless if list of supported domains (to prevent issues, specifically when used with UDP transport protocol causing bigger packets getting lost)
### Fixed
- Prevent refresh of views due to contacts changes to happen to frequently at startup
- Prevent crash in Help view if app is built without Firebase
## [6.0.3] - 2025-04-04
### Added
- Show alert when default account is disabled
- Refesh list details when going back from background after one hour or more (when keep app alive using service is enabled)
- Click to copy SIP URI in call history shortcut
- Added developper settings, must click 8 times on version (in Help) to make it appear (E2E encryption for meetings & group calls setting was moved there)
- Circular indicator while search is in progress in contacts lists
### Changed
- Force some default values on notifications channels
- Contacts list filter is now applied to new call / conversation & other contact pickers
- Attach file icon stays visible while typing message in conversation instead of emoji picker icon
### Fixed
- No default account being selected if the default one is removed
- Navigation bar turning orange when opening search bar
- Incoming call showed as video even if video is disabled locally
- Concurrent modification crash in Contacts loader
- Meetings list not properly sorted when CCMP is used
- POST_NOTIFICATIONS permission check on old Android devices
## [6.0.2] - 2025-03-28
### Added
- Show on top bar if FULL_SCREEN_INTENT permission isn't granted, clicking on it sends to the matching settings so user can fix it easily, without it incoming call screen won't be displayed if screen is off
- Ring during incoming early media call setting added back
- Added a floating action button to open dialpad during outgoing early media call
### Changed
- Delete all related call history / conversations / meetings when removing an account
- Delay / use a separated thread for heavy contacts related tasks to ensure call is correctly handled and foreground service is started quickly enough
- Newly created account in app will be kept disabled until SMS code validation is done
- Keep app alive foreground service notification no shows a content message to ease clicking on it to open the app & workaround a crash on some devices
- Automatically show dialpad setting will now also work on new / transfer call while in call as well
### Fixed
- Improved POST_NOTIFICATIONS permission check on Android 13 and newer, should prevent crashes
- Fixed contact lookup if phone number starts by "00" instead of "+"
- Fixed "delete all call history" sometimes not removing all call logs
- Fixed LDAP / remote CardDAV contacts sometimes not displayed in contacts list when doing a search
- Fixed issue where contact filter could be set to only show sip.linphone.org contacts even when third party account was being selected
- Fixed sometimes wrong displayed SIP URI in detailed call history
- Fixed invisible meeting icon in status bar
- Fixed missed call count indicator behavior with some third party providers
- Prevent today indicator & meeting icon in bottom nav bar from blinking / briefly appearing
- Fixed bottom nav bar sometimes being hidden
- Fixed missing share logs server URL when migrating from 5.2 if that value was removed back then
- Other crashes fixed
## [6.0.1] - 2025-03-21
### Added
- Start at boot & auto answer settings added back
- Interface setting to have dialpad automatically opened in start call view
- Replace "+" by "00" and do not apply prefix for calls & chat account settings
- Setting to let user choose whether to record calls using MKV or SMFF format (the later allows to record H265/AV1 video but is a proprietary file format that can't be read outside of Linphone)
### Changed
- Reverted the way of playing incoming call ringone (you may have to configure your own ringtone again), was causing various issues depending on devices/firmwares
- Show all call history entries if only one account is configured (workaround for missing history for now until a proper fix will be done in SDK)
### Fixed
- Issue preventing bluetooth Hearing Aids from working properly (and fixed earpiece/hearing aids icon)
- Prevent Qr Code scanner to use static picture camera
- Prevent user from connecting the same account multiple times
- Quit menu visibility not updated when changing Keep Alive setting
- Participant selection in group when typing "@"
- Recordings order has been reversed to have newest ones at top
- Improved message when network is not reachable due to "Wifi only mode" being enabled
- Various crash & bug fixes
## [6.0.0] - 2025-03-11
6.0.0 release is a complete rework of Linphone Android, with a fully redesigned UI, so it is impossible to list everything here.
### Changed
- Separated threads: Contrary to previous versions, our SDK is now running in it's own thread, meaning it won't freeze the UI anymore in case of heavy work, thus reducing the number of ANR and greatly increasing the fluidity of the app.
- Asymmetrical video : you no longer need to send your own camera feed to receive the one from the remote end of the call, and vice versa.
- Improved multi account: you'll only see history, conversations, meetings etc... related to currently selected account, and you can switch the default account in two clicks.
- Call transfer: Blind & Attended call transfer have been merged into one: during a call, if you initiate a transfer action, either pick another call to do the attended transfer or select a contact from the list (you can input a SIP URI not already in the suggestions list) to start a blind transfer.
- User can only send up to 12 files in a single chat message.
- IMDNs are now only sent to the message sender, preventing huge traffic in large groups, and thus the delivery status icon for received messages is now hidden in groups (as it was in 1-1 conversations).
- Settings: a lot of them are gone, the one that are still there have been reworked to increase user friendliness.
- Default screen (between contacts, call history, conversations & meetings list) will change depending on where you were when the app was paused or killed, and you will return to that last visited screen on the next startup.
- Gradle files have been migrated from Groovy to Kotlin DSL, and dependencies are now in a separated file (libs.versions.toml).
- Account creation no longer allows you to use your phone number as username, but it is still required to provide it to receive activation code by SMS.
- Minimum supported Android OS version is now 9 (API level 28).
- Telecom Manager support is now based on androidx.core.core-telecom package.
- Some settings have changed name and/or section in linphonerc file.
### Added
- Contacts trust: contacts for which all devices have been validated through a ZRTP call with SAS exchange are now highlighted with a blue circle (and with a red one in case of mistrust). That trust is now handled at contact level (instead of conversation level in previous versions).
- Media & documents exchanged in a conversation can be easily found through a dedicated screen.
- A brand new chat message search feature has been added to conversations.
- You can now react to a chat message using any emoji.
- If next message is also a voice recording, playback will automatically start after the currently playing one ends.
- Chat while in call: a shortcut to a conversation screen with the remote.
- Chat while in a conference: if the conference has a text stream enabled, you can chat with the other participants of the conference while it lasts. At the end, you'll find the messages history in the call history (and not in the list of conversations).
- Auto export of media to native gallery even when auto download is enabled (but still not if VFS is enabled nor for ephemeral messages).
- Save / export document & media from ephemeral messages will be disabled, and secure policy that prevents screenshots will be enforced in file viewer even if the setting is disabled.
- Notification showing upload/download of files shared through chat will let user know the progress and keep the app alive during that process.
- Screen sharing in conference: only desktop app starting with 6.0 version is able to start it, but on mobiles you'll be able to see it.
- You can choose whatever ringtone you'd like for incoming calls (in Android notification channel settings).
- Security focus: security & trust is more visible than ever, and unsecure conversations & calls are even more visible than before.
- CardDAV: you can configure as many CardDAV servers you want to synchronize you contacts in Linphone (in addition or in replacement of native addressbook import).
- OpenID: when used with a SSO compliant SIP server (such as Flexisip), we support single-sign-on login.
- MWI support: display and allow to call your voicemail when you have new messages (if supported by your VoIP provider and properly configured in your account params).
- CCMP support: if you configure a CCMP server URL in your accounts params, it will be used when scheduling meetings & to fetch list of meetings you've organized/been invited to.
- Devices list: check on which device your sip.linphone.org account is connected and the last connection date & time (like on subscribe.linphone.org).
- Protobuf dependency to allow logging native crashes stack traces at next app startup.
- Android 15 startup listener, allowing us to log type of start (cold, warm, etc...) and some other useful info.
- Dialer & in-call numpad show letters under the digit.
### Removed
- Dialer: the previous home screen (dialer) has been removed, you'll find it as an input option in the new start call screen.
- Peer-to-peer: a SIP account (sip.linphone.org or other) is now required.
- Contacts: we no longer add contacts created in-app in the native addressbook (WRITE_CONTACTS permission was removed), but we still import them if you grant us the READ_CONTACTS permission.
### Fixed
- No longer trying to play vocal messages & call recordings using bluetooth when connected to an Android Auto car, causing playback issues.
- AAudio driver no longer causes delay when switching between devices (SDK fix).
## [5.2.5] - 2024-05-03
## Changed
### Changed
- Updated translations
## [5.2.4] - 2024-04-22
## Fixed
### Fixed
- Active speaker video hidden when you are the first one to join a meeting
- Show camera icon instead of microphone for incoming video calls
- SIP URI parsing from native contact due to international prefix being applied when it shouldn't

View file

@ -1,11 +1,12 @@
[![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)
[![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/widget/linphone/linphone-android-6-0/status-badge.png)](https://weblate.linphone.org/engage/linphone/)
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).
General description is available from [linphone web site](https://linphone.org).
### How to get it
@ -21,11 +22,11 @@ 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.
- under a proprietary license, for a fee, to be used in closed source applications. Contact [Belledonne Communications](https://linphone.org/contact) for any question about costs and services.
### Documentation
- Supported features and RFCs : https://www.linphone.org/technical-corner/linphone/features
- Supported features and RFCs : https://www.linphone.org/linphone-softphone/#linphone-fonctionnalites
- Linphone public wiki : https://wiki.linphone.org/xwiki/wiki/public/view/Linphone/
@ -33,16 +34,11 @@ Linphone is dual licensed, and is available either :
# 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.
6.0.0 release is a completely new version, designed with UX/UI experts and marks a turning point in design, features, and user experience. The improvements make this version smoother and simpler for both developers and users.
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.
You can take a look at the [CHANGELOG.md](CHANGELOG.md) file for a non-exhaustive list of changes of this new version and of the newly added features, the most exciting ones being the improved fluidity, a real multi-accounts support and asymmetrical video in calls.
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).
This release only works on Android OS 9.0 and newer.
# Building the app
@ -97,6 +93,8 @@ LinphoneSdkBuildDir=/home/<username>/linphone-sdk/build/
## Known issues
- If you have the following build issue `AAPT: error: resource drawable/linphone_logo_tinted (aka org.linphone:drawable/linphone_logo_tinted) not found`, delete the `app/src/main/res/xml/contacts.xml` file (you can do it simply with `git clean -f` command) and start the build again.
- 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.
@ -106,13 +104,9 @@ Also check you have built the SDK for the right CPU architecture using the `-DLI
### 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:
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).
Starting 6.0.0 release, logs are always enabled and stored locally on the device, you can clear them/upload them securely on our server for sharing by going into the Help → Troubleshooting page.
### Native crash
@ -120,16 +114,13 @@ First of all, to be able to get a symbolized stack trace, you need the debug ver
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.
1. Go to our [maven repository](https://download.linphone.org/maven_repository/org/linphone/linphone-sdk-android/) and find the directory that matches the version of our SDK that crashed.
2. Download the AAR file with **the exact same version** as the AAR that was used to generate the crash's stacktrace.
2. Download the linphone-sdk-android-<version>-libs-debug.zip archive.
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```.
3. Extract the symbolized libraries somewhere on your computer, it will create a ```libs-debug``` directory.
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.
@ -144,22 +135,22 @@ adb logcat -d | ndk-stack -sym ./libs-debug/`adb shell getprop ro.product.cpu.ab
```
Warning: This command won't print anything until you reproduce the crash!
Starting [NDK r29](https://github.com/android/ndk/wiki/Changelog-r29) you will be able to directly use the ```libs-debug.zip``` file in ```ndk-stack -sym``` argument.
## Create an APK with a different package name
Simply edit the app/build.gradle file and change the value of the ```packageName``` variable.
Simply edit the ```app/build.gradle.kts``` file and change the value of the ```packageName``` variable.
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```.
We no longer build the debug flavor with a different package name, but if you still want that behavior you only have to change the value of ```useDifferentPackageNameForDebugBuild``` to ```true```. When enabled, app built and installed by Android studio will have ```org.linphone.debug``` package name instead of ```org.linphone```.
If you encounter
```
Execution failed for task ':app:processDebugGoogleServices'.
> No matching client found for package name 'your package name'
```
error when building, make sure you have replaced ```app/google-services.json``` file by yours (containing your package name).
If you don't have such file, remove ours.
error when building, make sure you have replaced the ```app/google-services.json``` file by yours (containing your package name).
If you don't have such file because you don't rely on Firebase Cloud Messaging features nor Crashlytics, delete the file instead.
## Firebase push notifications
@ -177,8 +168,8 @@ We no longer use transifex for the translation process, instead we have deployed
Due to the full app rewrite we can't re-use previous translations, so we'll be very happy if you want to contribute.
<a href="https://weblate.linphone.org/engage/linphone/?utm_source=widget">
<img src="https://weblate.linphone.org/widgets/linphone/-/linphone-android/multi-auto.svg" alt="Translation status" />
<a href="https://weblate.linphone.org/engage/linphone/">
<img src="https://weblate.linphone.org/widget/linphone/linphone-android-6-0/multi-auto.svg" alt="Translation status" />
</a>
# CONTRIBUTIONS

View file

@ -1,94 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'org.jlleitschuh.gradle.ktlint' version '11.3.1'
id 'org.jetbrains.kotlin.android'
}
static def getPackageName() {
return "org.linphone"
}
android {
namespace 'org.linphone'
compileSdk 34
defaultConfig {
applicationId getPackageName()
minSdk 27
targetSdk 34
versionCode 60000
versionName "6.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
resValue "string", "file_provider", getPackageName() + ".fileprovider"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
resValue "string", "file_provider", getPackageName() + ".fileprovider"
}
}
compileOptions {
sourceCompatibility = 17
targetCompatibility = 17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
dataBinding true
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.7.0-alpha02'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1-rc01'
implementation 'androidx.core:core-ktx:+'
implementation 'androidx.core:core-ktx:+'
def nav_version = "2.6.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
def emoji_version = "1.4.0-beta05"
implementation "androidx.emoji2:emoji2:$emoji_version"
implementation "androidx.emoji2:emoji2-emojipicker:$emoji_version"
// https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.0
implementation 'com.google.android.material:material:1.9.0'
// https://github.com/google/flexbox-layout/blob/main/LICENSE Apache v2.0
implementation 'com.google.android.flexbox:flexbox:3.0.0'
// https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0
def coil_version = "2.4.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")
implementation platform('com.google.firebase:firebase-bom:30.3.2')
implementation 'com.google.firebase:firebase-messaging'
implementation 'org.linphone:linphone-sdk-android:5.3+'
}
ktlint {
android = true
ignoreFailures = true
}
project.tasks['preBuild'].dependsOn 'ktlintFormat'

320
app/build.gradle.kts Normal file
View file

@ -0,0 +1,320 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
import com.google.gms.googleservices.GoogleServicesPlugin
import java.io.BufferedReader
import java.io.FileInputStream
import java.util.Properties
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.kapt)
alias(libs.plugins.ktlint)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.navigation)
alias(libs.plugins.crashlytics)
}
val packageName = "org.linphone"
val useDifferentPackageNameForDebugBuild = false
val sdkPath = providers.gradleProperty("LinphoneSdkBuildDir").get()
val googleServices = File(projectDir.absolutePath + "/google-services.json")
val linphoneLibs = File("$sdkPath/libs/")
val linphoneDebugLibs = File("$sdkPath/libs-debug/")
val firebaseCloudMessagingAvailable = googleServices.exists()
val crashlyticsAvailable = googleServices.exists() && linphoneLibs.exists() && linphoneDebugLibs.exists()
if (firebaseCloudMessagingAvailable) {
println("google-services.json found, enabling CloudMessaging feature")
apply<GoogleServicesPlugin>()
} else {
println("google-services.json not found, disabling CloudMessaging feature")
}
var gitVersion = "6.1.0-alpha"
var gitBranch = ""
try {
val gitDescribe = ProcessBuilder()
.command("git", "describe", "--abbrev=0")
.directory(project.rootDir)
.start()
.inputStream.bufferedReader().use(BufferedReader::readText)
.trim()
println("Git describe: $gitDescribe")
val gitCommitsCount = ProcessBuilder()
.command("git", "rev-list", "$gitDescribe..HEAD", "--count")
.directory(project.rootDir)
.start()
.inputStream.bufferedReader().use(BufferedReader::readText)
.trim()
println("Git commits count: $gitCommitsCount")
val gitCommitHash = ProcessBuilder()
.command("git", "rev-parse", "--short", "HEAD")
.directory(project.rootDir)
.start()
.inputStream.bufferedReader().use(BufferedReader::readText)
.trim()
println("Git commit hash: $gitCommitHash")
gitBranch = ProcessBuilder()
.command("git", "name-rev", "--name-only", "HEAD")
.directory(project.rootDir)
.start()
.inputStream.bufferedReader().use(BufferedReader::readText)
.trim()
println("Git branch name: $gitBranch")
gitVersion =
if (gitCommitsCount.toInt() == 0) {
gitDescribe
} else {
"$gitDescribe.$gitCommitsCount+$gitCommitHash"
}
} catch (e: Exception) {
println("Git not found [$e], using $gitVersion")
}
println("Computed git version: $gitVersion")
configurations {
implementation { isCanBeResolved = true }
}
tasks.register("linphoneSdkSource") {
doLast {
configurations.implementation.get().incoming.resolutionResult.allComponents.forEach {
if (it.id.displayName.contains("linphone-sdk-android")) {
println("Linphone SDK used is ${it.moduleVersion?.version}")
}
}
}
}
project.tasks.preBuild.dependsOn("linphoneSdkSource")
android {
namespace = "org.linphone"
compileSdk = 36
defaultConfig {
applicationId = packageName
minSdk = 28
targetSdk = 36
versionCode = 601002 // 6.01.002
versionName = "6.1.0-alpha"
manifestPlaceholders["appAuthRedirectScheme"] = packageName
ndk {
//noinspection ChromeOsAbiSupport
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
}
}
applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
output.outputFileName = "linphone-android-${variant.buildType.name}-${project.version}.apk"
}
}
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
signingConfigs {
create("release") {
val keyStorePath = keystoreProperties["storeFile"] as String
val keyStore = project.file(keyStorePath)
if (keyStore.exists()) {
storeFile = keyStore
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
println("Signing config release is using keystore [$storeFile]")
} else {
println("Keystore [$storeFile] doesn't exists!")
}
}
}
buildTypes {
getByName("debug") {
if (useDifferentPackageNameForDebugBuild) {
applicationIdSuffix = ".debug"
}
isDebuggable = true
isJniDebuggable = true
val appVersion = gitVersion
val appBranch = gitBranch
println("Debug flavor app version is [$appVersion], app branch is [$appBranch]")
resValue("string", "linphone_app_version", appVersion)
resValue("string", "linphone_app_branch", appBranch)
if (useDifferentPackageNameForDebugBuild) {
resValue("string", "file_provider", "$packageName.debug.fileprovider")
} else {
resValue("string", "file_provider", "$packageName.fileprovider")
}
resValue("string", "linphone_openid_callback_scheme", packageName)
if (crashlyticsAvailable) {
val path = File("$sdkPath/libs-debug/").toString()
configure<CrashlyticsExtension> {
nativeSymbolUploadEnabled = true
unstrippedNativeLibsDir = path
}
}
buildConfigField("Boolean", "CRASHLYTICS_ENABLED", crashlyticsAvailable.toString())
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
signingConfig = signingConfigs.getByName("release")
val appVersion = gitVersion
val appBranch = gitBranch
println("Release flavor app version is [$appVersion], app branch is [$appBranch]")
resValue("string", "linphone_app_version", appVersion)
resValue("string", "linphone_app_branch", appBranch)
resValue("string", "file_provider", "$packageName.fileprovider")
resValue("string", "linphone_openid_callback_scheme", packageName)
if (crashlyticsAvailable) {
val path = File("$sdkPath/libs-debug/").toString()
configure<CrashlyticsExtension> {
nativeSymbolUploadEnabled = true
unstrippedNativeLibsDir = path
}
}
buildConfigField("Boolean", "CRASHLYTICS_ENABLED", crashlyticsAvailable.toString())
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
buildFeatures {
dataBinding = true
buildConfig = true
resValues = true
}
lint {
abortOnError = false
}
}
dependencies {
implementation(libs.androidx.annotations)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.constraint.layout)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.splashscreen)
implementation(libs.androidx.telecom)
implementation(libs.androidx.media)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.slidingpanelayout)
implementation(libs.androidx.window)
implementation(libs.androidx.gridlayout)
implementation(libs.androidx.security.crypto.ktx)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.emoji2)
implementation(libs.androidx.car)
// https://github.com/google/flexbox-layout/blob/main/LICENSE Apache v2.0
implementation(libs.google.flexbox)
// https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.0
implementation(libs.google.material)
// To be able to parse native crash tombstone and print them with SDK logs the next time the app will start
implementation(libs.google.protobuf)
implementation(platform(libs.google.firebase.bom))
implementation(libs.google.firebase.messaging)
implementation(libs.google.firebase.crashlytics)
// https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0
implementation(libs.coil)
implementation(libs.coil.gif)
implementation(libs.coil.svg)
implementation(libs.coil.video)
// https://github.com/tommybuonomo/dotsindicator/blob/master/LICENSE Apache v2.0
implementation(libs.dots.indicator)
// https://github.com/Baseflow/PhotoView/blob/master/LICENSE Apache v2.0
implementation(libs.photoview)
// https://github.com/openid/AppAuth-Android/blob/master/LICENSE Apache v2.0
implementation(libs.openid.appauth)
implementation(libs.linphone)
}
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
android.set(true)
ignoreFailures.set(true)
additionalEditorconfig.set(
mapOf(
"max_line_length" to "120",
"ktlint_standard_max-line-length" to "disabled",
"ktlint_standard_function-signature" to "disabled",
"ktlint_standard_no-blank-line-before-rbrace" to "disabled",
"ktlint_standard_no-empty-class-body" to "disabled",
"ktlint_standard_annotation-spacing" to "disabled",
"ktlint_standard_class-signature" to "disabled",
"ktlint_standard_function-expression-body" to "disabled",
"ktlint_standard_function-type-modifier-spacing" to "disabled",
"ktlint_standard_if-else-wrapping" to "disabled",
"ktlint_standard_argument-list-wrapping" to "disabled",
"ktlint_standard_trailing-comma-on-call-site" to "disabled",
"ktlint_standard_trailing-comma-on-declaration-site" to "disabled",
"ktlint_standard_no-empty-first-line-in-class-body" to "disabled",
"ktlint_standard_no-empty-first-line-in-method-block" to "disabled",
"ktlint_standard_no-trailing-spaces" to "disabled",
"ktlint_standard_no-blank-line-in-list" to "disabled",
"ktlint_standard_no-multi-spaces" to "disabled",
"ktlint_standard_try-catch-finally-spacing" to "disabled",
"ktlint_standard_block-comment-initial-star-alignment" to "disabled",
"ktlint_standard_spacing-between-declarations-with-comments" to "disabled",
"ktlint_standard_no-consecutive-comments" to "disabled",
"ktlint_standard_multiline-expression-wrapping" to "disabled",
"ktlint_standard_parameter-list-wrapping" to "disabled",
"ktlint_standard_comment-wrapping" to "disabled",
"ktlint_standard_discouraged-comment-location" to "disabled",
"ktlint_standard_string-template-indent" to "disabled",
"ktlint_standard_parameter-list-spacing" to "disabled",
"ktlint_standard_statement-wrapping" to "disabled",
"ktlint_standard_import-ordering" to "disabled",
"ktlint_standard_paren-spacing" to "disabled",
"ktlint_standard_curly-spacing" to "disabled",
"ktlint_standard_indent" to "disabled",
)
)
}
project.tasks.preBuild.dependsOn("ktlintFormat")
if (crashlyticsAvailable) {
afterEvaluate {
tasks.getByName("assembleDebug").finalizedBy(
tasks.getByName("uploadCrashlyticsSymbolFileDebug"),
)
tasks.getByName("packageDebug").finalizedBy(
tasks.getByName("uploadCrashlyticsSymbolFileDebug"),
)
tasks.getByName("assembleRelease").finalizedBy(
tasks.getByName("uploadCrashlyticsSymbolFileRelease"),
)
tasks.getByName("packageRelease").finalizedBy(
tasks.getByName("uploadCrashlyticsSymbolFileRelease"),
)
}
}

View file

@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

View file

@ -2,6 +2,46 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- To be able to display contacts list & match calling/called numbers -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- 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 while receiving an incoming call -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Needed for foreground service
(https://developer.android.com/guide/components/foreground-services) -->
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Needed for Android 14
https://developer.android.com/about/versions/14/behavior-changes-14#fgs-types -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<!-- Required for foreground service started when a push is being received,
without it app won't be able to access network if data saver is ON (for example) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Needed to keep a permanent foreground service and keep app alive to be able to receive
messages & calls for third party accounts for which push notifications aren't available,
and starting Android 15 dataSync is limited to 6 hours per day
and can't be used with RECEIVE_BOOT_COMPLETED intent either -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- Needed for auto start at boot if keep alive service is enabled -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".LinphoneApplication"
android:allowBackup="true"
@ -11,21 +51,164 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:enableOnBackInvokedCallback="true"
android:localeConfig="@xml/locales_config"
android:theme="@style/Theme.Linphone"
tools:targetApi="34">
android:appCategory="social"
android:largeHeap="true"
tools:targetApi="35">
<!-- Required for chat message & call notifications to be displayed in Android auto -->
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
<!--<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="1"/>-->
<activity
android:name=".ui.MainActivity"
android:name=".ui.main.MainActivity"
android:theme="@style/AppSplashScreenTheme"
android:windowSoftInputMode="adjustResize"
android:exported="true"
android:label="@string/app_name">
android:launchMode="singleTask">
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<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.DIAL" />
<action android:name="android.intent.action.VIEW" />
<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="callto" />
<data android:scheme="sips" />
<data android:scheme="sip-linphone" />
<data android:scheme="sips-linphone" />
<data android:scheme="linphone-sip" />
<data android:scheme="linphone-sips" />
<data android:scheme="linphone-config" />
</intent-filter>
</activity>
<activity
android:name=".ui.welcome.WelcomeActivity"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
android:resizeableActivity="true" />
<activity
android:name=".ui.assistant.AssistantActivity"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
android:resizeableActivity="true" />
<activity
android:name=".ui.fileviewer.MediaViewerActivity"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
android:resizeableActivity="true" />
<activity
android:name=".ui.fileviewer.FileViewerActivity"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
android:resizeableActivity="true" />
<activity
android:name=".ui.call.CallActivity"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.LinphoneInCall"
android:launchMode="singleInstance"
android:turnScreenOn="true"
android:showWhenLocked="true"
android:resizeableActivity="true"
android:supportsPictureInPicture="true" />
<!-- Services -->
<service
android:name=".core.CoreInCallService"
android:exported="false"
android:foregroundServiceType="phoneCall|camera|microphone"
android:stopWithTask="false"
android:label="@string/app_name" />
<service
android:name=".core.CorePushService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="false"
android:label="@string/app_name" />
<service
android:name=".core.CoreFileTransferService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="false"
android:label="@string/app_name" />
<service android:name="org.linphone.core.tools.firebase.FirebaseMessaging"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service
android:name=".core.CoreKeepAliveThirdPartyAccountsService"
android:exported="false"
android:foregroundServiceType="specialUse"
android:stopWithTask="false"
android:label="@string/app_name">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Needed to keep app alive to be able to receive messages and calls from third party SIP servers for which push notifications aren't available." />
</service>
<!--<service
android:name=".telecom.auto.AndroidAutoService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService"/>
<category android:name="androidx.car.app.category.CALLING"/>
</intent-filter>
</service>-->
<!-- Receivers -->
<receiver android:name=".core.CorePushReceiver"
@ -35,6 +218,19 @@
</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

View file

@ -21,22 +21,15 @@
<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>
<entry name="lime_algo" overwrite="true">c25519</entry>
<entry name="supported" overwrite="true"></entry>
</section>
<section name="nat_policy_default_values">
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
<entry name="protocols" overwrite="true">stun,ice</entry>
</section>
<section name="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 name="sip">
<entry name="media_encryption" overwrite="true">srtp</entry>
<entry name="media_encryption_mandatory">1</entry>
</section>
</config>

View file

@ -21,22 +21,18 @@
<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>
<entry name="lime_algo" overwrite="true"></entry>
<entry name="supported" overwrite="true">outbound</entry>
</section>
<section name="nat_policy_default_values">
<entry name="stun_server" overwrite="true"></entry>
<entry name="protocols" overwrite="true"></entry>
<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">0</entry>
<section name="sip">
<entry name="media_encryption">srtp</entry>
<entry name="media_encryption_mandatory" 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 name="ui">
<entry name="automatically_show_dialpad" overwrite="true">1</entry>
</section>
</config>

View file

@ -11,6 +11,9 @@ sip_tcp_port=-1
sip_tls_port=-1
media_encryption=none
update_presence_model_timestamp_before_publish_expires_refresh=1
use_rfc2833=1
use_info=1
rls_uri=sips:rls@sip.linphone.org
[net]
#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit"
@ -19,26 +22,38 @@ upload_bw=0
[video]
size=vga
automatically_accept=1
automatically_initiate=0
automatically_accept_direction=2 #receive only
[app]
tunnel=disabled
auto_start=1
record_aware=1
auto_download_incoming_voice_recordings=1
auto_download_incoming_icalendars=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
log_collection_upload_server_url=https://files.linphone.org/http-file-transfer-server/hft.php
file_transfer_server_url=https://files.linphone.org/http-file-transfer-server/hft.php
version_check_url_root=https://download.linphone.org/releases
max_calls=10
history_max_size=100
conference_layout=1
hide_empty_chat_rooms=1
[in-app-purchase]
server_url=https://subscribe.linphone.org:444/inapp.php
purchasable_items_ids=test_account_subscription
[fec]
fec_enabled=1
[magic_search]
return_empty_friends=1
[chat]
imdn_to_everybody_threshold=1
[ui]
contacts_filter=sip.linphone.org
## End of default rc

View file

@ -18,13 +18,19 @@ 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
zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_MLK512,MS_ZRTP_KEY_AGREEMENT_K255_KYB512
chat_messages_aggregation_delay=1000
chat_messages_aggregation=1
update_presence_model_timestamp_before_publish_expires_refresh=1
[sound]
#remove this property for any application that is not Linphone public version itself
ec_calibrator_cool_tones=1
disable_ringing=0
[audio]
android_disable_audio_focus_requests=1
android_monitor_audio_devices=0
[video]
displaytype=MSAndroidTextureDisplay
@ -36,63 +42,18 @@ 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
store_friends=0
[app]
activation_code_length=4
prefer_basic_chat_room=1
record_aware=1
[account_creator]
backend=1
# 1 means FlexiAPI, 0 is XMLRPC
url=https://subscribe.linphone.org/api/
# replace above URL by https://staging-subscribe.linphone.org/api/ for testing
[lime]
lime_update_threshold=86400
[nat_policy_0]
ref=HQ0DK7mVDOPAY3i
stun_server=stun.linphone.org
protocols=stun,ice
[proxy_0]
reg_proxy=<sip:sip.linphone.org;transport=tls>
reg_route=sip:sip.linphone.org;transport=tls
reg_identity="Sylvain Berfini" <sip:sylvain@sip.linphone.org>
realm=sip.linphone.org
contact_parameters=message-expires=604800
quality_reporting_collector=sip:voip-metrics@sip.linphone.org;transport=tls
push_parameters=pn-silent=1;pn-timeout=0;
quality_reporting_enabled=1
quality_reporting_interval=180
reg_expires=600
reg_sendregister=1
publish=1
avpf=1
avpf_rr_interval=1
dial_escape_plus=0
dial_prefix=33
use_dial_prefix_for_calls_and_chats=1
privacy=32768
push_notification_allowed=1
remote_push_notification_allowed=0
cpim_in_basic_chat_rooms_enabled=1
idkey=proxy_config_WSik0NIEZbTW4fM
publish_expires=120
nat_policy_ref=-ulaFqPYu2HOZ90
conference_factory_uri=sip:conference-factory@sip.linphone.org
audio_video_conference_factory_uri=sip:videoconference-factory@sip.linphone.org
rtp_bundle=1
rtp_bundle_assumption=0
lime_server_url=https://lime.linphone.org/lime-server/lime-server.php
[auth_info_0]
username=sylvain
ha1=4028ae98f54e8ffd1ab5c90985a6e89752aa0228b3e14b7ffcc40e42b4787e56
realm=sip.linphone.org
domain=sip.linphone.org
algorithm=SHA-256
available_algorithms=SHA-256
[alerts]
alerts_enabled=1
## End of factory rc

View file

@ -21,24 +21,34 @@ package org.linphone
import android.annotation.SuppressLint
import android.app.Application
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 android.content.Context
import android.os.PowerManager
import androidx.annotation.MainThread
import coil3.ImageLoader
import coil3.SingletonImageLoader
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.imageLoader
import coil3.memory.MemoryCache
import coil3.request.CachePolicy
import coil3.request.crossfade
import coil3.svg.SvgDecoder
import coil3.video.VideoFrameDecoder
import com.google.android.material.color.DynamicColors
import org.linphone.compatibility.Compatibility
import org.linphone.core.CoreContext
import org.linphone.core.CorePreferences
import org.linphone.core.Factory
import org.linphone.core.LogCollectionState
import org.linphone.core.LogLevel
import org.linphone.mediastream.Version
import org.linphone.core.VFS
import org.linphone.core.tools.Log
class LinphoneApplication : Application(), ImageLoaderFactory {
@MainThread
class LinphoneApplication : Application(), SingletonImageLoader.Factory {
companion object {
private const val TAG = "[Linphone Application]"
@SuppressLint("StaticFieldLeak")
lateinit var corePreferences: CorePreferences
@ -50,6 +60,13 @@ class LinphoneApplication : Application(), ImageLoaderFactory {
super.onCreate()
val context = applicationContext
val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager
val wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"Linphone:AppCreation"
)
wakeLock.acquire(20000L) // 20 seconds
Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)
Factory.instance().enableLogCollection(LogCollectionState.Enabled)
// For VFS
@ -57,6 +74,11 @@ class LinphoneApplication : Application(), ImageLoaderFactory {
corePreferences = CorePreferences(context)
corePreferences.copyAssetsFromPackage()
if (VFS.isEnabled(context)) {
VFS.setup(context)
}
val config = Factory.instance().createConfigWithFactory(
corePreferences.configPath,
corePreferences.factoryConfigPath
@ -65,37 +87,77 @@ class LinphoneApplication : Application(), ImageLoaderFactory {
val appName = context.getString(R.string.app_name)
Factory.instance().setLoggerDomain(appName)
Factory.instance().enableLogcatLogs(true)
Factory.instance().loggingService.setLogLevel(LogLevel.Message)
Factory.instance().enableLogcatLogs(corePreferences.printLogsInLogcat)
Log.i("$TAG Report Core preferences initialized")
Compatibility.setupAppStartupListener(context)
coreContext = CoreContext(context)
coreContext.start()
DynamicColors.applyToActivitiesIfAvailable(this)
wakeLock.release()
}
override fun newImageLoader(): ImageLoader {
override fun onTrimMemory(level: Int) {
Log.w("$TAG onTrimMemory called with level [${trimLevelToString(level)}]($level) !")
when (level) {
TRIM_MEMORY_RUNNING_LOW,
TRIM_MEMORY_RUNNING_CRITICAL,
TRIM_MEMORY_MODERATE,
TRIM_MEMORY_COMPLETE -> {
Log.i("$TAG Memory trim required, clearing imageLoader memory cache")
imageLoader.memoryCache?.clear()
}
else -> {}
}
super.onTrimMemory(level)
}
override fun newImageLoader(context: Context): ImageLoader {
// When VFS is enabled, prevent Coil from keeping plain version of files on disk
val diskCachePolicy = if (VFS.isEnabled(applicationContext)) {
CachePolicy.DISABLED
} else {
CachePolicy.ENABLED
}
return ImageLoader.Builder(this)
.crossfade(false)
.components {
add(VideoFrameDecoder.Factory())
// add(GifDecoder.Factory) // Do not add it, GIFs are properly rendered without it and adding it breaks resizing...
add(SvgDecoder.Factory())
if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.memoryCache {
MemoryCache.Builder(this)
.maxSizePercent(0.25)
MemoryCache.Builder()
.maxSizePercent(context, 0.25)
.build()
}
.diskCache {
val cache = cacheDir.resolve("image_cache")
DiskCache.Builder()
.directory(cacheDir.resolve("image_cache"))
.directory(cache)
.maxSizePercent(0.02)
.build()
}
.networkCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(diskCachePolicy)
.memoryCachePolicy(CachePolicy.ENABLED)
.build()
}
private fun trimLevelToString(level: Int): String {
return when (level) {
TRIM_MEMORY_UI_HIDDEN -> "Hidden UI"
TRIM_MEMORY_RUNNING_MODERATE -> "Moderate (Running)"
TRIM_MEMORY_RUNNING_LOW -> "Low"
TRIM_MEMORY_RUNNING_CRITICAL -> "Critical"
TRIM_MEMORY_BACKGROUND -> "Background"
TRIM_MEMORY_MODERATE -> "Moderate"
TRIM_MEMORY_COMPLETE -> "Complete"
else -> level.toString()
}
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2010-2023 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.compatibility
import android.app.Activity
import android.app.Notification
import android.app.PictureInPictureParams
import android.app.Service
import android.net.Uri
import android.provider.MediaStore
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
class Api28Compatibility {
companion object {
private const val TAG = "[API 28 Compatibility]"
fun startServiceForeground(service: Service, id: Int, notification: Notification) {
try {
service.startForeground(
id,
notification
)
} catch (e: Exception) {
Log.e("$TAG Can't start service as foreground! $e")
}
}
fun enterPipMode(activity: Activity): Boolean {
val params = PictureInPictureParams.Builder()
.setAspectRatio(AppUtils.getPipRatio(activity))
.build()
try {
if (!activity.enterPictureInPictureMode(params)) {
Log.e("$TAG Failed to enter PiP mode")
} else {
Log.i("$TAG Entered PiP mode")
return true
}
} catch (e: Exception) {
Log.e("$TAG Can't build PiP params: $e")
}
return false
}
fun getMediaCollectionUri(isImage: Boolean, isVideo: Boolean, isAudio: Boolean): Uri {
return when {
isImage -> {
MediaStore.Images.Media.getContentUri("external")
}
isVideo -> {
MediaStore.Video.Media.getContentUri("external")
}
isAudio -> {
MediaStore.Audio.Media.getContentUri("external")
}
else -> Uri.EMPTY
}
}
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2010-2023 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.compatibility
import android.content.Intent
import android.net.InetAddresses.isNumericAddress
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.view.View
import android.view.contentcapture.ContentCaptureContext
import android.view.contentcapture.ContentCaptureSession
import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.Q)
class Api29Compatibility {
companion object {
fun getMediaCollectionUri(isImage: Boolean, isVideo: Boolean, isAudio: Boolean): Uri {
return when {
isImage -> {
MediaStore.Images.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY
)
}
isVideo -> {
MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY
)
}
isAudio -> {
MediaStore.Audio.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY
)
}
else -> Uri.EMPTY
}
}
fun extractLocusIdFromIntent(intent: Intent): String? {
return intent.getStringExtra(Intent.EXTRA_LOCUS_ID)
}
fun setLocusIdInContentCaptureSession(root: View, conversationId: String) {
val session: ContentCaptureSession? = root.contentCaptureSession
if (session != null) {
session.contentCaptureContext = ContentCaptureContext.forLocusId(conversationId)
}
}
fun isIpAddress(string: String): Boolean {
return isNumericAddress(string)
}
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2010-2023 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.compatibility
import android.app.Activity
import android.app.PictureInPictureParams
import android.app.UiModeManager
import android.content.Context
import android.graphics.RenderEffect
import android.graphics.Shader
import android.os.Build
import android.os.Environment
import android.view.View
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
@RequiresApi(Build.VERSION_CODES.S)
class Api31Compatibility {
companion object {
private const val TAG = "[API 31 Compatibility]"
fun enableAutoEnterPiP(activity: Activity, enable: Boolean) {
try {
activity.setPictureInPictureParams(
PictureInPictureParams.Builder()
.setAspectRatio(AppUtils.getPipRatio(activity))
.setAutoEnterEnabled(enable)
.build()
)
Log.i("$TAG PiP auto enter has been [${if (enable) "enabled" else "disabled"}]")
} catch (iae: IllegalArgumentException) {
Log.e("$TAG Can't set PiP params: $iae")
} catch (ise: IllegalStateException) {
Log.e("$TAG Can't set PiP params: $ise")
}
}
fun setBlurRenderEffect(view: View) {
val blurEffect = RenderEffect.createBlurEffect(16F, 16F, Shader.TileMode.MIRROR)
view.setRenderEffect(blurEffect)
}
fun removeBlurRenderEffect(view: View) {
view.setRenderEffect(null)
}
fun forceDarkMode(context: Context) {
val uiManager = ContextCompat.getSystemService(context, UiModeManager::class.java)
if (uiManager == null) {
Log.e("$TAG Failed to get UiModeManager system service!")
}
uiManager?.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES)
}
fun forceLightMode(context: Context) {
val uiManager = ContextCompat.getSystemService(context, UiModeManager::class.java)
if (uiManager == null) {
Log.e("$TAG Failed to get UiModeManager system service!")
}
uiManager?.setApplicationNightMode(UiModeManager.MODE_NIGHT_NO)
}
fun setAutoLightDarkMode(context: Context) {
val uiManager = ContextCompat.getSystemService(context, UiModeManager::class.java)
if (uiManager == null) {
Log.e("$TAG Failed to get UiModeManager system service!")
}
uiManager?.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO)
}
fun getRecordingsDirectory(): String {
return Environment.DIRECTORY_RECORDINGS
}
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2010-2023 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.compatibility
import android.Manifest
import android.app.ActivityOptions
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
class Api33Compatibility {
companion object {
fun getAllRequiredPermissionsArray(): Array<String> {
return arrayOf(
Manifest.permission.POST_NOTIFICATIONS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA
)
}
fun isPostNotificationsPermissionGranted(context: Context): Boolean {
return context.checkSelfPermission(
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
}
fun getPendingIntentActivityOptions(): ActivityOptions {
val options = ActivityOptions.makeBasic()
options.isPendingIntentBackgroundActivityLaunchAllowed = true
return options
}
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2010-2023 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.compatibility
import android.app.ActivityOptions
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.annotation.RequiresApi
import org.linphone.core.tools.Log
import androidx.core.net.toUri
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class Api34Compatibility {
companion object {
private const val TAG = "[API 34 Compatibility]"
fun startServiceForeground(
service: Service,
id: Int,
notification: Notification,
foregroundServiceType: Int
) {
try {
service.startForeground(
id,
notification,
foregroundServiceType
)
} catch (e: Exception) {
Log.e("$TAG Can't start service as foreground! $e")
}
}
fun hasFullScreenIntentPermission(context: Context): Boolean {
val notificationManager = context.getSystemService(NotificationManager::class.java) as NotificationManager
// See https://developer.android.com/reference/android/app/NotificationManager#canUseFullScreenIntent%28%29
val granted = notificationManager.canUseFullScreenIntent()
if (granted) {
Log.i("$TAG Full screen intent permission is granted")
} else {
Log.w("$TAG Full screen intent permission isn't granted yet!")
}
return granted
}
fun requestFullScreenIntentPermission(context: Context) {
val intent = Intent()
// See https://developer.android.com/reference/android/provider/Settings#ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT
intent.action = Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT
intent.data = "package:${context.packageName}".toUri()
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
Log.i("$TAG Starting ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT")
try {
context.startActivity(intent, null)
} catch (anfe: ActivityNotFoundException) {
Log.e("$TAG Failed to start intent for granting full screen intent permission: $anfe")
}
}
fun sendPendingIntent(pendingIntent: PendingIntent, bundle: Bundle) {
pendingIntent.send(bundle)
}
fun getPendingIntentActivityOptions(creator: Boolean): ActivityOptions {
val options = ActivityOptions.makeBasic()
if (creator) {
options.pendingIntentCreatorBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
} else {
options.pendingIntentBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
}
return options
}
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright (c) 2010-2024 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.compatibility
import android.app.ActivityManager
import android.app.ApplicationStartInfo
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import java.util.concurrent.Executors
import org.linphone.core.tools.Log
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class Api35Compatibility {
companion object {
private const val TAG = "[API 35 Compatibility]"
fun setupAppStartupListener(context: Context) {
try {
val activityManager = context.getSystemService(ActivityManager::class.java)
activityManager.addApplicationStartInfoCompletionListener(
Executors.newSingleThreadExecutor()
) { info ->
Log.i("==== Current startup information dump ====")
logAppStartupInfo(info)
}
Log.i("==== Fetching last three startup reasons if available ====")
val lastStartupInfo = activityManager.getHistoricalProcessStartReasons(3)
for (info in lastStartupInfo) {
Log.i("==== Previous startup information dump ====")
logAppStartupInfo(info)
}
} catch (iae: IllegalArgumentException) {
Log.e("$TAG Can't add application start info completion listener: $iae")
}
}
private fun logAppStartupInfo(info: ApplicationStartInfo) {
Log.i("TYPE = ${startupTypeToString(info.startType)}")
Log.i("STATE = ${startupStateToString(info.startupState)}")
Log.i("REASON = ${startupReasonToString(info.reason)}")
Log.i("START COMPONENT = ${startComponentToString(info.launchMode)}")
Log.i("INTENT = ${info.intent}")
Log.i("FORCE STOPPED = ${if (info.wasForceStopped()) "yes" else "no"}")
Log.i("PROCESS NAME = ${info.processName}")
Log.i("=========================================")
}
private fun startComponentToString(component: Int): String {
return when (component) {
ApplicationStartInfo.START_COMPONENT_ACTIVITY -> "Activity"
ApplicationStartInfo.START_COMPONENT_BROADCAST -> "Broadcast"
ApplicationStartInfo.START_COMPONENT_CONTENT_PROVIDER -> "Content Provider"
ApplicationStartInfo.START_COMPONENT_SERVICE -> "Service"
ApplicationStartInfo.START_COMPONENT_OTHER -> "Other"
else -> "Unexpected ($component)"
}
}
private fun startupTypeToString(type: Int): String {
return when (type) {
ApplicationStartInfo.START_TYPE_COLD -> "Cold"
ApplicationStartInfo.START_TYPE_HOT -> "Hot"
ApplicationStartInfo.START_TYPE_UNSET -> "Unset"
ApplicationStartInfo.START_TYPE_WARM -> "Warm"
else -> "Unexpected ($type)"
}
}
private fun startupStateToString(state: Int): String {
return when (state) {
ApplicationStartInfo.STARTUP_STATE_STARTED -> "Started"
ApplicationStartInfo.STARTUP_STATE_ERROR -> "Error"
ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN -> "First frame drawn"
else -> "Unexpected ($state)"
}
}
private fun startupReasonToString(reason: Int): String {
return when (reason) {
ApplicationStartInfo.START_REASON_ALARM -> "Alarm"
ApplicationStartInfo.START_REASON_BACKUP -> "Backup"
ApplicationStartInfo.START_REASON_BOOT_COMPLETE -> "Boot complete"
ApplicationStartInfo.START_REASON_BROADCAST -> "Broadcast"
ApplicationStartInfo.START_REASON_CONTENT_PROVIDER -> "Content provider"
ApplicationStartInfo.START_REASON_JOB -> "Job"
ApplicationStartInfo.START_REASON_LAUNCHER -> "Launcher"
ApplicationStartInfo.START_REASON_LAUNCHER_RECENTS -> "Launcher (recents)"
ApplicationStartInfo.START_REASON_OTHER -> "Other"
ApplicationStartInfo.START_REASON_PUSH -> "Push"
ApplicationStartInfo.START_REASON_SERVICE -> "Service"
ApplicationStartInfo.START_REASON_START_ACTIVITY -> "Start Activity"
else -> "Unexpected ($reason)"
}
}
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2010-2025 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.compatibility
import android.app.ActivityOptions
import android.os.Build
import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
class Api36Compatibility {
companion object {
private const val TAG = "[API 36 Compatibility]"
fun getPendingIntentActivityOptions(creator: Boolean): ActivityOptions {
val options = ActivityOptions.makeBasic()
if (creator) {
options.pendingIntentCreatorBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
} else {
options.pendingIntentBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
}
return options
}
}
}

View file

@ -0,0 +1,220 @@
/*
* Copyright (c) 2010-2023 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.compatibility
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityOptions
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.util.Patterns
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import org.linphone.core.tools.Log
import org.linphone.mediastream.Version
@SuppressLint("NewApi")
class Compatibility {
companion object {
private const val TAG = "[Compatibility]"
const val FOREGROUND_SERVICE_TYPE_PHONE_CALL = 4 // ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
const val FOREGROUND_SERVICE_TYPE_CAMERA = 64 // ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
const val FOREGROUND_SERVICE_TYPE_MICROPHONE = 128 // ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
const val FOREGROUND_SERVICE_TYPE_SPECIAL_USE = 1073741824 // ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
fun startServiceForeground(
service: Service,
id: Int,
notification: Notification,
foregroundServiceType: Int
) {
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
Api34Compatibility.startServiceForeground(
service,
id,
notification,
foregroundServiceType
)
} else {
Api28Compatibility.startServiceForeground(service, id, notification)
}
}
fun setBlurRenderEffect(view: View) {
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.setBlurRenderEffect(view)
}
}
fun removeBlurRenderEffect(view: View) {
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.removeBlurRenderEffect(view)
}
}
fun getMediaCollectionUri(
isImage: Boolean = false,
isVideo: Boolean = false,
isAudio: Boolean = false
): Uri {
return if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) {
Api29Compatibility.getMediaCollectionUri(isImage, isVideo, isAudio)
} else {
Api28Compatibility.getMediaCollectionUri(isImage, isVideo, isAudio)
}
}
fun getAllRequiredPermissionsArray(): Array<String> {
if (Version.sdkAboveOrEqual(Version.API33_ANDROID_13_TIRAMISU)) {
return Api33Compatibility.getAllRequiredPermissionsArray()
}
return arrayOf(
Manifest.permission.READ_CONTACTS,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA
)
}
fun hasFullScreenIntentPermission(context: Context): Boolean {
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
return Api34Compatibility.hasFullScreenIntentPermission(context)
}
return true
}
fun requestFullScreenIntentPermission(context: Context): Boolean {
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
Api34Compatibility.requestFullScreenIntentPermission(context)
return true
}
return false
}
fun isPostNotificationsPermissionGranted(context: Context): Boolean {
if (Version.sdkAboveOrEqual(Version.API33_ANDROID_13_TIRAMISU)) {
return Api33Compatibility.isPostNotificationsPermissionGranted(context)
}
return true
}
fun enterPipMode(activity: Activity): Boolean {
if (Version.sdkStrictlyBelow(Version.API31_ANDROID_12)) {
return Api28Compatibility.enterPipMode(activity)
}
return activity.isInPictureInPictureMode
}
fun enableAutoEnterPiP(activity: Activity, enable: Boolean) {
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.enableAutoEnterPiP(activity, enable)
}
}
fun forceDarkMode(context: Context) {
Log.i("$TAG Forcing dark/night theme")
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.forceDarkMode(context)
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}
fun forceLightMode(context: Context) {
Log.i("$TAG Forcing light/day theme")
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.forceLightMode(context)
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
}
fun setAutoLightDarkMode(context: Context) {
Log.i("$TAG Following Android's choice for light/dark theme")
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.setAutoLightDarkMode(context)
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
fun extractLocusIdFromIntent(intent: Intent): String? {
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) {
return Api29Compatibility.extractLocusIdFromIntent(intent)
}
return null
}
fun setLocusIdInContentCaptureSession(root: View, conversationId: String) {
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) {
return Api29Compatibility.setLocusIdInContentCaptureSession(
root,
conversationId
)
}
}
fun getRecordingsDirectory(): String {
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
return Api31Compatibility.getRecordingsDirectory()
}
return Environment.DIRECTORY_PODCASTS
}
fun setupAppStartupListener(context: Context) {
if (Version.sdkAboveOrEqual(Version.API35_ANDROID_15_VANILLA_ICE_CREAM)) {
Api35Compatibility.setupAppStartupListener(context)
}
}
fun isIpAddress(string: String): Boolean {
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) {
return Api29Compatibility.isIpAddress(string)
}
return Patterns.IP_ADDRESS.matcher(string).matches()
}
fun sendPendingIntent(pendingIntent: PendingIntent, bundle: Bundle) {
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
return Api34Compatibility.sendPendingIntent(pendingIntent, bundle)
}
pendingIntent.send()
}
fun getPendingIntentActivityOptions(creator: Boolean): ActivityOptions {
if (Version.sdkAboveOrEqual(Version.API36_ANDROID_16_BAKLAVA)) {
return Api36Compatibility.getPendingIntentActivityOptions(creator)
} else if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
return Api34Compatibility.getPendingIntentActivityOptions(creator)
} else if (Version.sdkAboveOrEqual(Version.API33_ANDROID_13_TIRAMISU)) {
return Api33Compatibility.getPendingIntentActivityOptions()
}
return ActivityOptions.makeBasic()
}
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2010-2023 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.contacts
import androidx.lifecycle.MutableLiveData
import org.linphone.core.ConsolidatedPresence
import org.linphone.core.SecurityLevel
abstract class AbstractAvatarModel {
val trust = MutableLiveData<SecurityLevel>()
val showTrust = MutableLiveData<Boolean>()
val initials = MutableLiveData<String>()
val picturePath = MutableLiveData<String>()
val forceConversationIcon = MutableLiveData<Boolean>()
val forceConferenceIcon = MutableLiveData<Boolean>()
val defaultToConversationIcon = MutableLiveData<Boolean>()
val defaultToConferenceIcon = MutableLiveData<Boolean>()
val skipInitials = MutableLiveData<Boolean>()
val presenceStatus = MutableLiveData<ConsolidatedPresence>()
}

View file

@ -0,0 +1,119 @@
/*
* 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.contacts
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.text.TextPaint
import android.util.TypedValue
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.IconCompat
import org.linphone.R
import org.linphone.utils.AppUtils
import androidx.core.graphics.createBitmap
class AvatarGenerator(private val context: Context) {
private var textSize: Float = AppUtils.getDimension(R.dimen.avatar_initials_text_size)
private var textColor: Int = ContextCompat.getColor(context, R.color.gray_main2_600)
private var avatarSize: Int = AppUtils.getDimension(R.dimen.avatar_list_cell_size).toInt()
private var initials = " "
private var transparentColor: Int = ContextCompat.getColor(context, R.color.transparent_color)
private var backgroundColor: Int = ContextCompat.getColor(context, R.color.gray_main2_200)
init {
val textTypedValue = TypedValue()
context.theme.resolveAttribute(R.attr.color_avatar_text, textTypedValue, true)
// This will fail for notifications
if (textTypedValue.data != 0) {
textColor = textTypedValue.data
}
val backgroundTypedValue = TypedValue()
context.theme.resolveAttribute(R.attr.color_avatar_background, backgroundTypedValue, true)
// This will fail for notifications
if (backgroundTypedValue.data != 0) {
backgroundColor = backgroundTypedValue.data
}
}
fun setTextSize(size: Float) = apply {
textSize = size
}
fun setAvatarSize(size: Int) = apply {
avatarSize = size
}
fun setInitials(label: String) = apply {
initials = label
}
fun buildBitmap(useTransparentBackground: Boolean): Bitmap {
val textPainter = getTextPainter()
val painter = if (useTransparentBackground) getTransparentPainter() else getBackgroundPainter()
val bitmap = createBitmap(avatarSize, avatarSize)
val canvas = Canvas(bitmap)
val areaRect = Rect(0, 0, avatarSize, avatarSize)
val bounds = RectF(areaRect)
bounds.right = textPainter.measureText(initials, 0, initials.length)
bounds.bottom = textPainter.descent() - textPainter.ascent()
bounds.left += (areaRect.width() - bounds.right) / 2.0f
bounds.top += (areaRect.height() - bounds.bottom) / 2.0f
val halfSize = (avatarSize / 2).toFloat()
canvas.drawCircle(halfSize, halfSize, halfSize, painter)
canvas.drawText(initials, bounds.left, bounds.top - textPainter.ascent(), textPainter)
return bitmap
}
fun buildIcon(): IconCompat {
return IconCompat.createWithAdaptiveBitmap(buildBitmap(false))
}
private fun getTextPainter(): TextPaint {
val textPainter = TextPaint()
textPainter.isAntiAlias = true
textPainter.textSize = textSize
textPainter.color = textColor
textPainter.typeface = ResourcesCompat.getFont(context, R.font.noto_sans_800)
return textPainter
}
private fun getTransparentPainter(): Paint {
val painter = Paint()
painter.isAntiAlias = true
painter.color = transparentColor
return painter
}
private fun getBackgroundPainter(): Paint {
val painter = Paint()
painter.isAntiAlias = true
painter.color = backgroundColor
return painter
}
}

View file

@ -0,0 +1,345 @@
/*
* Copyright (c) 2010-2023 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.contacts
import android.database.Cursor
import android.database.StaleDataException
import android.os.Bundle
import android.provider.ContactsContract
import android.util.Patterns
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.loader.app.LoaderManager
import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.lang.Exception
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Core
import org.linphone.core.Factory
import org.linphone.core.Friend
import org.linphone.core.FriendList
import org.linphone.core.GlobalState
import org.linphone.core.SubscribePolicy
import org.linphone.core.tools.Log
import org.linphone.utils.PhoneNumberUtils
class ContactLoader : LoaderManager.LoaderCallbacks<Cursor> {
companion object {
val projection = arrayOf(
ContactsContract.Data.CONTACT_ID,
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
ContactsContract.Data.MIMETYPE,
ContactsContract.Contacts.STARRED,
ContactsContract.Contacts.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.CommonDataKinds.Phone.LABEL,
ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER
)
private const val TAG = "[Contacts Loader]"
const val NATIVE_ADDRESS_BOOK_FRIEND_LIST = "Native address-book"
const val LINPHONE_ADDRESS_BOOK_FRIEND_LIST = "Linphone address-book"
private const val MIN_INTERVAL_TO_WAIT_BEFORE_REFRESH = 300000L // 5 minutes
}
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@MainThread
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
Log.i("$TAG Creating and starting cursor loader")
val mimeType = ContactsContract.Data.MIMETYPE
val mimeSelection = "$mimeType = ? OR $mimeType = ? OR $mimeType = ? OR $mimeType = ?"
val selection = if (args?.getBoolean("defaultDirectory", true) == true) {
Log.i("$TAG Only fetching contacts from default directory")
ContactsContract.Data.IN_DEFAULT_DIRECTORY + " == 1 AND ($mimeSelection)"
} else {
Log.i("$TAG Fetching all available contacts")
mimeSelection
}
val selectionArgs = arrayOf(
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE,
ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE
)
val loader = CursorLoader(
coreContext.context,
ContactsContract.Data.CONTENT_URI,
projection,
selection,
selectionArgs,
ContactsContract.Data.CONTACT_ID + " ASC"
)
// WARNING: this doesn't prevent to be called again in onLoadFinished,
// it will only have for effect that the notified cursor will be the same as before
// instead of a new one with updated content!
// loader.setUpdateThrottle(MIN_INTERVAL_TO_WAIT_BEFORE_REFRESH)
return loader
}
@MainThread
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
if (cursor == null) {
Log.e("$TAG Cursor is null!")
return
} else if (cursor.isClosed) {
Log.e("$TAG Cursor is closed!")
return
}
Log.i("$TAG Load finished, found ${cursor.count} entries in cursor")
if (cursor.isAfterLast) {
Log.w("$TAG Cursor position is after last, it was probably already used, nothing to do")
return
}
coreContext.postOnCoreThread {
val core = coreContext.core
val state = core.globalState
if (state == GlobalState.Shutdown || state == GlobalState.Off) {
Log.w("$TAG Core is being stopped or already destroyed, abort")
} else {
scope.launch {
parseFriends(core, cursor)
}
}
}
}
@MainThread
override fun onLoaderReset(loader: Loader<Cursor>) {
Log.i("$TAG Loader reset")
scope.cancel()
}
@WorkerThread
private fun parseFriends(core: Core, cursor: Cursor) {
try {
val contactIdColumn = cursor.getColumnIndexOrThrow(ContactsContract.Data.CONTACT_ID)
val mimetypeColumn = cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE)
val displayNameColumn = cursor.getColumnIndexOrThrow(
ContactsContract.Data.DISPLAY_NAME_PRIMARY
)
val starredColumn = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.STARRED)
val lookupColumn = cursor.getColumnIndexOrThrow(
ContactsContract.Contacts.LOOKUP_KEY
)
val phoneNumberColumn = cursor.getColumnIndexOrThrow(
ContactsContract.CommonDataKinds.Phone.NUMBER
)
val phoneTypeColumn = cursor.getColumnIndexOrThrow(
ContactsContract.CommonDataKinds.Phone.TYPE
)
val phoneLabelColumn = cursor.getColumnIndexOrThrow(
ContactsContract.CommonDataKinds.Phone.LABEL
)
val normalizedPhoneColumn = cursor.getColumnIndexOrThrow(
ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER
)
val sipAddressColumn = cursor.getColumnIndexOrThrow(
ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS
)
val companyColumn = cursor.getColumnIndexOrThrow(
ContactsContract.CommonDataKinds.Organization.COMPANY
)
val jobTitleColumn = cursor.getColumnIndexOrThrow(
ContactsContract.CommonDataKinds.Organization.TITLE
)
val givenNameColumn = cursor.getColumnIndexOrThrow(
ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME
)
val familyNameColumn = cursor.getColumnIndexOrThrow(
ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME
)
val friends = HashMap<String, Friend>()
while (!cursor.isClosed && cursor.moveToNext()) {
try {
val id: String = cursor.getString(contactIdColumn)
val mime: String? = cursor.getString(mimetypeColumn)
val friend = friends[id] ?: core.createFriend()
friend.refKey = id
if (friend.name.isNullOrEmpty()) {
val displayName: String? = cursor.getString(displayNameColumn)
if (!displayName.isNullOrEmpty()) {
friend.name = displayName
val uri = friend.getNativeContactPictureUri()
if (uri != null) {
friend.photo = uri.toString()
}
val starred = cursor.getInt(starredColumn) == 1
friend.starred = starred
val lookupKey =
cursor.getString(lookupColumn)
friend.nativeUri =
"${ContactsContract.Contacts.CONTENT_LOOKUP_URI}/$lookupKey"
friend.isSubscribesEnabled = false
// Disable peer to peer short term presence
friend.incSubscribePolicy = SubscribePolicy.SPDeny
}
}
when (mime) {
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> {
val data1: String? = cursor.getString(phoneNumberColumn)
val data2: String? = cursor.getString(phoneTypeColumn)
val data3: String? = cursor.getString(phoneLabelColumn)
val data4: String? = cursor.getString(normalizedPhoneColumn)
val label =
PhoneNumberUtils.addressBookLabelTypeToVcardParamString(
data2?.toInt()
?: ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM,
data3
)
val number =
if (data1.isNullOrEmpty() ||
!Patterns.PHONE.matcher(data1).matches()
) {
data4 ?: data1
} else {
data1
}
if (!number.isNullOrEmpty()) {
val phoneNumber = Factory.instance()
.createFriendPhoneNumber(number, label)
friend.addPhoneNumberWithLabel(phoneNumber)
}
}
ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> {
val sipAddress: String? = cursor.getString(sipAddressColumn)
if (!sipAddress.isNullOrEmpty()) {
val address = core.interpretUrl(sipAddress, false)
if (address != null) {
friend.addAddress(address)
}
}
}
ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> {
val organization: String? = cursor.getString(companyColumn)
if (!organization.isNullOrEmpty()) {
friend.organization = organization
}
val job: String? = cursor.getString(jobTitleColumn)
if (!job.isNullOrEmpty()) {
friend.jobTitle = job
}
}
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
val givenName: String? = cursor.getString(givenNameColumn)
if (!givenName.isNullOrEmpty()) {
friend.firstName = givenName
}
val familyName: String? = cursor.getString(familyNameColumn)
if (!familyName.isNullOrEmpty()) {
friend.lastName = familyName
}
}
}
friends[id] = friend
} catch (e: Exception) {
Log.e("$TAG Exception: $e")
}
}
Log.i("$TAG Contacts parsed, posting another task to handle adding them (or not)")
// Re-post another task to allow other tasks on Core thread
coreContext.postOnCoreThreadWhenAvailableForHeavyTask({
addFriendsIfNeeded(friends)
}, "add friends to Core")
} catch (sde: StaleDataException) {
Log.e("$TAG State Data Exception: $sde")
} catch (ise: IllegalStateException) {
Log.e("$TAG Illegal State Exception: $ise")
} catch (e: Exception) {
Log.e("$TAG Exception: $e")
}
}
@WorkerThread
private fun addFriendsIfNeeded(friends: HashMap<String, Friend>) {
val core = coreContext.core
if (core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off) {
Log.w("$TAG Core is being stopped or already destroyed, abort")
} else if (friends.isEmpty()) {
Log.w("$TAG No friend created!")
} else {
Log.i("$TAG ${friends.size} friends fetched")
val friendsList = core.getFriendListByName(NATIVE_ADDRESS_BOOK_FRIEND_LIST)
?: core.createFriendList()
if (friendsList.displayName.isNullOrEmpty()) {
Log.i(
"$TAG Friend list [$NATIVE_ADDRESS_BOOK_FRIEND_LIST] didn't exist yet, let's create it"
)
friendsList.isDatabaseStorageEnabled =
true // Store them to keep presence info available for push notifications & favorites
friendsList.type = FriendList.Type.Default
friendsList.displayName = NATIVE_ADDRESS_BOOK_FRIEND_LIST
core.addFriendList(friendsList)
for (friend in friends.values) {
friendsList.addLocalFriend(friend)
}
Log.i("$TAG Friends added")
} else {
val friendsArray = friends.values.toTypedArray()
Log.i(
"$TAG Friend list [$NATIVE_ADDRESS_BOOK_FRIEND_LIST] found, synchronizing existing friends with new ones"
)
val changes = friendsList.synchronizeFriendsWith(friendsArray)
if (changes) {
Log.i("$TAG Locally stored friends synchronized with native address book")
} else {
Log.i("$TAG No changes detected between native address book and local friends storage")
}
}
friends.clear()
friendsList.updateSubscriptions()
Log.i("$TAG Subscription(s) updated")
coreContext.contactsManager.onNativeContactsLoaded()
}
}
}

View file

@ -0,0 +1,888 @@
/*
* Copyright (c) 2010-2023 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.contacts
import android.content.ContentUris
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.provider.ContactsContract
import androidx.annotation.MainThread
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.app.Person
import androidx.core.graphics.drawable.IconCompat
import androidx.core.text.isDigitsOnly
import androidx.loader.app.LoaderManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.Account
import org.linphone.core.Address
import org.linphone.core.ConferenceInfo
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.Factory
import org.linphone.core.Friend
import org.linphone.core.FriendList
import org.linphone.core.FriendListListenerStub
import org.linphone.core.MagicSearch
import org.linphone.core.MagicSearchListenerStub
import org.linphone.core.SecurityLevel
import org.linphone.core.tools.Log
import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.model.isEndToEndEncryptionMandatory
import org.linphone.utils.AppUtils
import org.linphone.utils.ImageUtils
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.PhoneNumberUtils
import org.linphone.utils.ShortcutUtils
import java.io.FileNotFoundException
class ContactsManager
@UiThread
constructor() {
companion object {
private const val TAG = "[Contacts Manager]"
private const val DELAY_BEFORE_RELOADING_CONTACTS_AFTER_PRESENCE_RECEIVED = 1000L // 1 second
private const val DELAY_BEFORE_RELOADING_CONTACTS_AFTER_MAGIC_SEARCH_RESULT = 1000L // 1 second
private const val FRIEND_LIST_TEMPORARY_STORED_REMOTE_DIRECTORY = "TempRemoteDirectoryContacts"
}
private var nativeContactsLoaded = false
private val listeners = arrayListOf<ContactsListener>()
private val knownContactsAvatarsMap = hashMapOf<String, ContactAvatarModel>()
private val unknownContactsAvatarsMap = hashMapOf<String, ContactAvatarModel>()
private val conferenceAvatarMap = hashMapOf<String, ContactAvatarModel>()
private val magicSearchMap = hashMapOf<String, MagicSearch>()
private val unknownRemoteContactDirectoriesContactsMap = arrayListOf<String>()
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var reloadPresenceContactsJob: Job? = null
private var reloadRemoteContactsJob: Job? = null
private var loadContactsOnlyFromDefaultDirectory = true
private val magicSearchListener = object : MagicSearchListenerStub() {
@WorkerThread
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
var queriedSipUri = ""
for ((key, value) in magicSearchMap.entries) {
if (value == magicSearch) {
queriedSipUri = key
}
}
val results = magicSearch.lastSearch
Log.i(
"$TAG [${results.size}] magic search results available for query upon SIP URI [$queriedSipUri]"
)
var found = false
if (results.isNotEmpty()) {
val result = results.first { it.friend != null }
if (result != null) {
val friend = result.friend!!
Log.i("$TAG Found matching friend in source [${result.sourceFlags}]")
val address = result.address?.asStringUriOnly().orEmpty()
if (address.isEmpty() || (queriedSipUri.isNotEmpty() && queriedSipUri != address)) {
Log.w("$TAG Received friend [${friend.name}] with SIP URI [$address] doesn't match queried SIP URI [$queriedSipUri]")
} else {
found = true
reloadRemoteContactsJob?.cancel()
// Store friend in app's cache to be re-used in call history, conversations, etc...
val temporaryFriendList = getRemoteContactDirectoriesCacheFriendList()
temporaryFriendList.addFriend(friend)
newContactAdded(friend)
Log.i(
"$TAG Stored discovered friend [${friend.name}] in temporary friend list, for later use"
)
for (listener in listeners) {
listener.onContactFoundInRemoteDirectory(friend)
}
reloadRemoteContactsJob = coroutineScope.launch {
delay(DELAY_BEFORE_RELOADING_CONTACTS_AFTER_MAGIC_SEARCH_RESULT)
coreContext.postOnCoreThread {
Log.i("$TAG At least a new SIP address was discovered, reloading contacts")
conferenceAvatarMap.values.forEach(ContactAvatarModel::destroy)
conferenceAvatarMap.clear()
notifyContactsListChanged()
}
}
}
}
}
if (queriedSipUri.isNotEmpty()) {
magicSearchMap.remove(queriedSipUri)
if (!found) {
Log.i(
"$TAG SIP URI [$queriedSipUri] wasn't found in remote directories, adding it to unknown list to prevent further queries"
)
unknownRemoteContactDirectoriesContactsMap.add(queriedSipUri)
}
}
magicSearch.removeListener(this)
}
}
private val friendListListener: FriendListListenerStub = object : FriendListListenerStub() {
@WorkerThread
override fun onPresenceReceived(friendList: FriendList, friends: Array<out Friend?>) {
if (friendList.isSubscriptionBodyless) {
Log.i("$TAG Bodyless friendlist [${friendList.displayName}] presence received")
var atLeastOneFriendAdded = false
for (friend in friends) {
if (friend != null) {
val address = friend.address
if (address != null) {
Log.i(
"$TAG Newly discovered SIP Address [${address.asStringUriOnly()}] for friend [${friend.name}] in bodyless list [${friendList.displayName}]"
)
newContactAddedWithSipUri(friend, address)
atLeastOneFriendAdded = true
}
}
}
if (atLeastOneFriendAdded) {
notifyContactsListChanged()
} else {
Log.w("$TAG No new friend detected in the received bodyless friendlist, not refreshing contacts in app")
}
}
}
@WorkerThread
override fun onNewSipAddressDiscovered(
friendList: FriendList,
friend: Friend,
sipUri: String
) {
reloadPresenceContactsJob?.cancel()
Log.d(
"$TAG Newly discovered SIP Address [$sipUri] for friend [${friend.name}] in list [${friendList.displayName}]"
)
val address = Factory.instance().createAddress(sipUri)
if (address != null) {
Log.i("$TAG Storing discovered SIP URI inside Friend")
friend.edit()
friend.addAddress(address)
friend.done()
newContactAddedWithSipUri(friend, address)
} else {
Log.e("$TAG Failed to parse SIP URI [$sipUri] as Address!")
}
reloadPresenceContactsJob = coroutineScope.launch {
delay(DELAY_BEFORE_RELOADING_CONTACTS_AFTER_PRESENCE_RECEIVED)
coreContext.postOnCoreThread {
Log.i("$TAG At least a new SIP address was discovered, reloading contacts")
conferenceAvatarMap.values.forEach(ContactAvatarModel::destroy)
conferenceAvatarMap.clear()
notifyContactsListChanged()
}
}
}
@WorkerThread
override fun onContactCreated(friendList: FriendList, linphoneFriend: Friend) {
for (address in linphoneFriend.addresses) {
removeUnknownAddressFromMap(address)
}
}
@WorkerThread
override fun onContactDeleted(friendList: FriendList, linphoneFriend: Friend) {
for (address in linphoneFriend.addresses) {
removeKnownAddressFromMap(address)
}
}
@WorkerThread
override fun onContactUpdated(
friendList: FriendList,
newFriend: Friend,
oldFriend: Friend
) {
for (address in oldFriend.addresses) {
removeKnownAddressFromMap(address)
}
for (address in newFriend.addresses) {
removeUnknownAddressFromMap(address)
}
}
@WorkerThread
override fun onSyncStatusChanged(
friendList: FriendList,
status: FriendList.SyncStatus?,
message: String?
) {
Log.i("$TAG Friend list [${friendList.displayName}] sync status changed to [$status]")
when (status) {
FriendList.SyncStatus.Successful -> {
notifyContactsListChanged()
}
FriendList.SyncStatus.Failure -> {
Log.e("$TAG Friend list [${friendList.displayName}] sync failed: $message")
}
else -> {}
}
}
}
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
@WorkerThread
override fun onFriendListCreated(core: Core, friendList: FriendList) {
Log.i("$TAG Friend list [${friendList.displayName}] created")
friendList.addListener(friendListListener)
}
@WorkerThread
override fun onFriendListRemoved(core: Core, friendList: FriendList) {
Log.i("$TAG Friend list [${friendList.displayName}] removed")
friendList.removeListener(friendListListener)
}
@WorkerThread
override fun onDefaultAccountChanged(core: Core, account: Account?) {
Log.i("$TAG Default account changed, update all contacts' model showTrust value")
updateContactsModelDependingOnDefaultAccountMode()
}
}
@MainThread
fun loadContacts(activity: MainActivity) {
Log.i("$TAG Starting contacts loader")
val manager = LoaderManager.getInstance(activity)
val args = Bundle()
args.putBoolean("defaultDirectory", loadContactsOnlyFromDefaultDirectory)
manager.restartLoader(0, args, ContactLoader())
}
@WorkerThread
fun addListener(listener: ContactsListener) {
// Post again to prevent ConcurrentModificationException
coreContext.postOnCoreThread {
try {
listeners.add(listener)
} catch (cme: ConcurrentModificationException) {
Log.e("$TAG Can't add listener: $cme")
}
}
}
@WorkerThread
fun removeListener(listener: ContactsListener) {
if (coreContext.isReady()) {
// Post again to prevent ConcurrentModificationException
coreContext.postOnCoreThread {
try {
listeners.remove(listener)
} catch (cme: ConcurrentModificationException) {
Log.e("$TAG Can't remove listener: $cme")
}
}
}
}
@WorkerThread
fun removeKnownAddressFromMap(address: Address) {
val key = address.asStringUriOnly()
val wasKnown = knownContactsAvatarsMap.remove(key)
if (wasKnown != null) {
Log.d("$TAG Removed address [$key] from knownContactsAvatarsMap")
}
}
@WorkerThread
fun removeUnknownAddressFromMap(address: Address) {
val key = address.asStringUriOnly()
val wasUnknown = unknownContactsAvatarsMap.remove(key)
if (wasUnknown != null) {
Log.d("$TAG Removed address [$key] from unknownContactsAvatarsMap")
}
}
@WorkerThread
private fun newContactAddedWithSipUri(friend: Friend, address: Address) {
val sipUri = address.asStringUriOnly()
if (unknownContactsAvatarsMap.keys.contains(sipUri)) {
Log.d("$TAG Found SIP URI [$sipUri] in unknownContactsAvatarsMap, removing it")
val oldModel = unknownContactsAvatarsMap[sipUri]
oldModel?.destroy()
unknownContactsAvatarsMap.remove(sipUri)
} else if (knownContactsAvatarsMap.keys.contains(sipUri)) {
Log.d(
"$TAG Found SIP URI [$sipUri] in knownContactsAvatarsMap, forcing presence update"
)
val oldModel = knownContactsAvatarsMap[sipUri]
oldModel?.update(address)
} else {
Log.i(
"$TAG New contact added with SIP URI [$sipUri] but no avatar yet, let's create it"
)
val model = ContactAvatarModel(friend)
knownContactsAvatarsMap[sipUri] = model
}
}
@WorkerThread
fun newContactAdded(friend: Friend) {
for (sipAddress in friend.addresses) {
newContactAddedWithSipUri(friend, sipAddress)
}
}
@WorkerThread
fun contactRemoved(friend: Friend) {
val refKey = friend.refKey.orEmpty()
if (refKey.isNotEmpty() && knownContactsAvatarsMap.keys.contains(refKey)) {
Log.d("$TAG Found RefKey [$refKey] in knownContactsAvatarsMap, removing it")
val oldModel = knownContactsAvatarsMap[refKey]
oldModel?.destroy()
knownContactsAvatarsMap.remove(refKey)
}
for (sipAddress in friend.addresses) {
val sipUri = sipAddress.asStringUriOnly()
if (knownContactsAvatarsMap.keys.contains(sipUri)) {
Log.d("$TAG Found SIP URI [$sipUri] in knownContactsAvatarsMap, removing it")
val oldModel = knownContactsAvatarsMap[sipUri]
oldModel?.destroy()
knownContactsAvatarsMap.remove(sipUri)
}
}
conferenceAvatarMap.values.forEach(ContactAvatarModel::destroy)
conferenceAvatarMap.clear()
notifyContactsListChanged()
}
@WorkerThread
fun onNativeContactsLoaded() {
nativeContactsLoaded = true
Log.i("$TAG Native contacts have been loaded, cleaning avatars maps")
knownContactsAvatarsMap.values.forEach(ContactAvatarModel::destroy)
knownContactsAvatarsMap.clear()
unknownContactsAvatarsMap.values.forEach(ContactAvatarModel::destroy)
unknownContactsAvatarsMap.clear()
conferenceAvatarMap.values.forEach(ContactAvatarModel::destroy)
conferenceAvatarMap.clear()
unknownRemoteContactDirectoriesContactsMap.clear()
notifyContactsListChanged()
Log.i("$TAG Native contacts have been loaded")
// No longer create chat room shortcuts depending on most recents ones, create it when a message is sent or received instead
// ShortcutUtils.createShortcutsToChatRooms(coreContext.context)
}
@WorkerThread
fun notifyContactsListChanged() {
for (listener in listeners) {
listener.onContactsLoaded()
}
}
@WorkerThread
fun findContactById(id: String): Friend? {
Log.d("$TAG Looking for a friend with ref key [$id]")
for (friendList in coreContext.core.friendsLists) {
val found = friendList.findFriendByRefKey(id)
if (found != null) {
Log.d("$TAG Found friend [${found.name}] matching ref key [$id]")
return found
}
}
Log.w("$TAG No friend matching ref key [$id] has been found")
return null
}
@WorkerThread
fun findContactByAddress(address: Address): Friend? {
Log.i("$TAG Looking for friend matching SIP address [${address.asStringUriOnly()}]")
val found = coreContext.core.findFriend(address)
if (found != null) {
Log.i("$TAG Found friend [${found.name}] matching SIP address [${address.asStringUriOnly()}]")
return found
}
val username = address.username
val sipUri = LinphoneUtils.getAddressAsCleanStringUriOnly(address)
// Start an async query in Magic Search in case LDAP or remote CardDAV is configured
val remoteContactDirectories = coreContext.core.remoteContactDirectories
if (remoteContactDirectories.isNotEmpty() && !magicSearchMap.keys.contains(sipUri) && !unknownRemoteContactDirectoriesContactsMap.contains(
sipUri
)
) {
Log.i(
"$TAG SIP URI [$sipUri] not found in locally stored Friends, trying LDAP/CardDAV remote directory"
)
val magicSearch = coreContext.core.createMagicSearch()
magicSearch.addListener(magicSearchListener)
magicSearchMap[sipUri] = magicSearch
magicSearch.getContactsListAsync(
username,
address.domain,
MagicSearch.Source.LdapServers.toInt() or MagicSearch.Source.RemoteCardDAV.toInt(),
MagicSearch.Aggregation.Friend
)
}
return if (!username.isNullOrEmpty() && (username.startsWith("+") || username.isDigitsOnly())) {
Log.i("$TAG Looking for friend using phone number [$username]")
val foundUsingPhoneNumber = coreContext.core.findFriendByPhoneNumber(username)
if (foundUsingPhoneNumber != null) {
Log.i("$TAG Found friend [${foundUsingPhoneNumber.name}] matching phone number [$username]")
}
foundUsingPhoneNumber
} else {
null
}
}
@WorkerThread
fun findDisplayName(address: Address): String {
return getContactAvatarModelForAddress(address).friend.name ?: LinphoneUtils.getDisplayName(
address
)
}
@WorkerThread
fun getContactAvatarModelForAddress(address: Address?): ContactAvatarModel {
if (address == null) {
Log.w("$TAG Address is null, generic model will be used")
val fakeFriend = coreContext.core.createFriend()
return ContactAvatarModel(fakeFriend)
}
val clone = address.clone()
clone.clean()
val key = clone.asStringUriOnly()
val foundInMap = getAvatarModelFromCache(key)
if (foundInMap != null) {
Log.d("$TAG Avatar model found in map for SIP URI [$key]")
return foundInMap
}
val localAccount = coreContext.core.accountList.find {
it.params.identityAddress?.weakEqual(clone) == true
}
val avatar = if (localAccount != null) {
Log.d("$TAG [$key] SIP URI matches one of the local account")
val fakeFriend = coreContext.core.createFriend()
fakeFriend.address = clone
fakeFriend.name = LinphoneUtils.getDisplayName(localAccount.params.identityAddress)
fakeFriend.photo = localAccount.params.pictureUri
val model = ContactAvatarModel(fakeFriend)
model.trust.postValue(SecurityLevel.EndToEndEncryptedAndVerified)
unknownContactsAvatarsMap[key] = model
model
} else {
Log.d("$TAG Looking for friend matching SIP URI [$key]")
val friend = findContactByAddress(clone)
if (friend != null) {
Log.d("$TAG Matching friend [${friend.name}] found for SIP URI [$key]")
val model = ContactAvatarModel(friend, address)
knownContactsAvatarsMap[key] = model
model
} else {
Log.d("$TAG No matching friend found for SIP URI [$key]...")
val fakeFriend = coreContext.core.createFriend()
fakeFriend.name = LinphoneUtils.getDisplayName(address)
fakeFriend.address = clone
val model = ContactAvatarModel(fakeFriend)
unknownContactsAvatarsMap[key] = model
model
}
}
return avatar
}
@WorkerThread
fun getContactAvatarModelForFriend(friend: Friend?): ContactAvatarModel {
if (friend == null) {
Log.w("$TAG Friend is null, using generic avatar model")
val fakeFriend = coreContext.core.createFriend()
return ContactAvatarModel(fakeFriend)
}
val avatar = ContactAvatarModel(friend)
return avatar
}
@WorkerThread
fun getContactAvatarModelForConferenceInfo(conferenceInfo: ConferenceInfo): ContactAvatarModel {
// Do not clean parameters!
val key = conferenceInfo.uri?.asStringUriOnly()
if (key == null) {
val fakeFriend = coreContext.core.createFriend()
fakeFriend.name = conferenceInfo.subject
val model = ContactAvatarModel(fakeFriend)
model.showTrust.postValue(false)
return model
}
val foundInMap = conferenceAvatarMap[key] ?: conferenceAvatarMap[key]
if (foundInMap != null) return foundInMap
val avatar = LinphoneUtils.getAvatarModelForConferenceInfo(conferenceInfo)
conferenceAvatarMap[key] = avatar
return avatar
}
@WorkerThread
fun isContactAvailable(friend: Friend): Boolean {
return !friend.refKey.isNullOrEmpty() && !isContactTemporary(friend)
}
@WorkerThread
fun isContactTemporary(friend: Friend, allowNullFriendList: Boolean = false): Boolean {
val friendList = friend.friendList
if (friendList == null && !allowNullFriendList) return true
return friendList?.type == FriendList.Type.ApplicationCache
}
@WorkerThread
fun onCoreStarted(core: Core) {
Log.i("$TAG Core has been started")
loadContactsOnlyFromDefaultDirectory = corePreferences.fetchContactsFromDefaultDirectory
core.addListener(coreListener)
for (list in core.friendsLists) {
Log.i("$TAG Found existing friend list [${list.displayName}]")
list.addListener(friendListListener)
}
val context = coreContext.context
ShortcutUtils.removeLegacyShortcuts(context)
// No longer create chat room shortcuts depending on most recents ones, create it when a message is sent or received instead
/*if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.READ_CONTACTS
) != PackageManager.PERMISSION_GRANTED
) {
Log.w("$TAG READ_CONTACTS permission was denied, creating chat rooms shortcuts now")
ShortcutUtils.createShortcutsToChatRooms(context)
}*/
for (list in core.friendsLists) {
if (list.type == FriendList.Type.CardDAV && !list.uri.isNullOrEmpty()) {
Log.i(
"$TAG Found a CardDAV friend list with name [${list.displayName}] and URI [${list.uri}], synchronizing it"
)
list.synchronizeFriendsFromServer()
}
}
}
@WorkerThread
fun onCoreStopped(core: Core) {
Log.w("$TAG Core has been stopped")
coroutineScope.cancel()
core.removeListener(coreListener)
for (list in core.friendsLists) {
list.removeListener(friendListListener)
}
}
@WorkerThread
fun getRemoteContactDirectoriesCacheFriendList(): FriendList {
val core = coreContext.core
val name = FRIEND_LIST_TEMPORARY_STORED_REMOTE_DIRECTORY
val temporaryFriendList = core.getFriendListByName(name) ?: core.createFriendList()
if (temporaryFriendList.displayName.isNullOrEmpty()) {
temporaryFriendList.isDatabaseStorageEnabled = false
temporaryFriendList.displayName = name
temporaryFriendList.type = FriendList.Type.ApplicationCache
core.addFriendList(temporaryFriendList)
Log.i(
"$TAG Created temporary friend list with name [$name]"
)
}
return temporaryFriendList
}
@WorkerThread
fun getMePerson(localAddress: Address): Person {
val account = coreContext.core.accountList.find {
it.params.identityAddress?.weakEqual(localAddress) == true
}
val name = account?.params?.identityAddress?.displayName ?: LinphoneUtils.getDisplayName(
localAddress
)
val personBuilder = Person.Builder().setName(name.ifEmpty { "Unknown" })
val photo = account?.params?.pictureUri.orEmpty()
val bm = ImageUtils.getBitmap(coreContext.context, photo)
personBuilder.setIcon(
if (bm == null) {
AvatarGenerator(coreContext.context).setInitials(AppUtils.getInitials(name)).buildIcon()
} else {
IconCompat.createWithAdaptiveBitmap(bm)
}
)
val identity = account?.params?.identityAddress?.asStringUriOnly() ?: localAddress.asStringUriOnly()
personBuilder.setKey(identity)
personBuilder.setImportant(true)
return personBuilder.build()
}
@WorkerThread
fun updateContactsModelDependingOnDefaultAccountMode() {
val showTrust = true
Log.i(
"$TAG Default account mode is [${if (showTrust) "end-to-end encryption mandatory" else "interoperable"}], update all contact models showTrust value"
)
knownContactsAvatarsMap.forEach { (_, contactAvatarModel) ->
contactAvatarModel.showTrust.postValue(showTrust)
}
unknownContactsAvatarsMap.forEach { (_, contactAvatarModel) ->
contactAvatarModel.showTrust.postValue(showTrust)
}
conferenceAvatarMap.forEach { (_, contactAvatarModel) ->
contactAvatarModel.showTrust.postValue(showTrust)
}
}
@WorkerThread
private fun getAvatarModelFromCache(key: String): ContactAvatarModel? {
return knownContactsAvatarsMap[key] ?: unknownContactsAvatarsMap[key]
}
interface ContactsListener {
fun onContactsLoaded()
fun onContactFoundInRemoteDirectory(friend: Friend)
}
}
@WorkerThread
fun Friend.getAvatarBitmap(round: Boolean = false): Bitmap? {
try {
return ImageUtils.getBitmap(
coreContext.context,
photo ?: getNativeContactPictureUri()?.toString(),
round
)
} catch (_: NumberFormatException) {
// Expected for contacts created by Linphone
}
return null
}
@WorkerThread
fun Friend.getNativeContactPictureUri(): Uri? {
val contactId = refKey
if (contactId != null) {
try {
val lookupUri = ContentUris.withAppendedId(
ContactsContract.Contacts.CONTENT_URI,
contactId.toLong()
)
val pictureUri = Uri.withAppendedPath(
lookupUri,
ContactsContract.Contacts.Photo.DISPLAY_PHOTO
)
// Check that the URI points to a real file
val contentResolver = coreContext.context.contentResolver
try {
val fd = contentResolver.openAssetFileDescriptor(pictureUri, "r")
if (fd != null) {
fd.close()
return pictureUri
}
} catch (fnfe: FileNotFoundException) {
Log.w("[Contacts Manager] Can't open [$pictureUri] for contact [$name]: $fnfe")
} catch (e: Exception) {
Log.e("[Contacts Manager] Can't open [$pictureUri] for contact [$name]: $e")
}
// Fallback to thumbnail
return Uri.withAppendedPath(
lookupUri,
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
)
} catch (_: NumberFormatException) {
// Expected for contacts created by Linphone
}
}
return null
}
@WorkerThread
fun Friend.getPerson(): Person {
val personBuilder = Person.Builder()
val personName = if (name.orEmpty().isNotEmpty()) {
name
} else {
if (!lastName.isNullOrEmpty() || !firstName.isNullOrEmpty()) {
Log.w("[Friend] Name is null or empty, using first and last name")
"$firstName $lastName".trim()
} else if (!organization.isNullOrEmpty()) {
Log.w("[Friend] Name, first name & last name are null or empty, using organization instead")
organization
} else if (!jobTitle.isNullOrEmpty()) {
Log.w("[Friend] Name, first and last names & organization are null or empty, using job title instead")
jobTitle
} else {
Log.e("[Friend] No identification field filled for this friend!")
"Unknown"
}
}
personBuilder.setName(personName.orEmpty().ifEmpty { "Unknown" })
val bm: Bitmap? = getAvatarBitmap()
personBuilder.setIcon(
if (bm == null) {
Log.i(
"[Friend] Can't use friend [$name] picture path, generating avatar based on initials"
)
AvatarGenerator(coreContext.context).setInitials(AppUtils.getInitials(personName.orEmpty())).buildIcon()
} else {
IconCompat.createWithAdaptiveBitmap(bm)
}
)
personBuilder.setKey(refKey)
personBuilder.setUri(nativeUri)
personBuilder.setImportant(true)
return personBuilder.build()
}
@WorkerThread
fun Friend.getListOfSipAddresses(): ArrayList<Address> {
val addressesList = arrayListOf<Address>()
if (corePreferences.hideSipAddresses) return addressesList
for (address in addresses) {
if (addressesList.find { it.weakEqual(address) } == null) {
addressesList.add(address)
}
}
return addressesList
}
@WorkerThread
fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddressClickListener): ArrayList<ContactNumberOrAddressModel> {
val addressesAndNumbers = arrayListOf<ContactNumberOrAddressModel>()
// Will return an empty list if corePreferences.hideSipAddresses == true
for (address in getListOfSipAddresses()) {
if (LinphoneUtils.isSipAddressLinkedToPhoneNumberByPresence(this, address.asStringUriOnly())) {
continue
}
val data = ContactNumberOrAddressModel(
this,
address,
address.asStringUriOnly(),
true, // SIP addresses are always enabled
listener,
true
)
addressesAndNumbers.add(data)
}
if (corePreferences.hidePhoneNumbers) {
return addressesAndNumbers
}
for (number in phoneNumbersWithLabel) {
val phoneNumber = number.phoneNumber
val presenceModel = getPresenceModelForUriOrTel(phoneNumber)
val hasPresenceInfo = !presenceModel?.contact.isNullOrEmpty()
var presenceAddress: Address? = null
if (presenceModel != null && hasPresenceInfo) {
val contact = presenceModel.contact
if (!contact.isNullOrEmpty()) {
val address = core.interpretUrl(contact, false)
if (address != null) {
address.clean() // To remove ;user=phone
presenceAddress = address
} else {
Log.e("[Contacts Manager] Failed to parse phone number [$phoneNumber] contact address [$contact] from presence model!")
}
}
}
// phone numbers are disabled is secure mode unless linked to a SIP address
val defaultAccount = LinphoneUtils.getDefaultAccount()
val enablePhoneNumbers = hasPresenceInfo || !isEndToEndEncryptionMandatory()
val address = presenceAddress ?: core.interpretUrl(
phoneNumber,
LinphoneUtils.applyInternationalPrefix(defaultAccount)
)
address ?: continue
val label = PhoneNumberUtils.vcardParamStringToAddressBookLabel(
coreContext.context.resources,
number.label ?: ""
)
Log.d("[Contacts Manager] Parsed phone number [$phoneNumber] with label [$label] into address [${address.asStringUriOnly()}], presence address is [${presenceAddress?.asStringUriOnly()}]")
val data = ContactNumberOrAddressModel(
this,
address,
phoneNumber,
enablePhoneNumbers,
listener,
false,
label,
presenceAddress != null
)
addressesAndNumbers.add(data)
}
return addressesAndNumbers
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2010-2024 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.core
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.tools.Log
class BootReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "[Boot Receiver]"
}
override fun onReceive(context: Context, intent: Intent) {
val keepAlive = corePreferences.keepServiceAlive
if (intent.action.equals(Intent.ACTION_BOOT_COMPLETED, ignoreCase = true)) {
Log.i(
"$TAG Device boot completed, keep alive service is ${if (keepAlive) "enabled" else "disabled"}"
)
} else if (intent.action.equals(Intent.ACTION_MY_PACKAGE_REPLACED, ignoreCase = true)) {
Log.i(
"$TAG App has been updated, keep alive service is ${if (keepAlive) "enabled" else "disabled"}"
)
}
// Starting the keep alive service will be done by CoreContext directly
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,188 @@
/*
* Copyright (c) 2010-2024 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.core
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.os.IBinder
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.core.tools.service.FileTransferService
import org.linphone.ui.main.MainActivity
@MainThread
class CoreFileTransferService : FileTransferService() {
companion object {
private const val TAG = "[Core File Transfer Service]"
}
var builder = NotificationCompat.Builder(this, SERVICE_NOTIFICATION_CHANNEL_ID)
var listenerAdded = false
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onRemainingNumberOfFileTransferChanged(
core: Core,
downloadCount: Int,
uploadCount: Int
) {
updateNotificationContent(downloadCount, uploadCount)
}
}
override fun onCreate() {
super.onCreate()
if (!listenerAdded && coreContext.isCoreAvailable()) {
coreContext.core.addListener(coreListener)
listenerAdded = true
}
Log.i("$TAG Created")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("$TAG onStartCommand")
if (!listenerAdded && coreContext.isCoreAvailable()) {
coreContext.core.addListener(coreListener)
listenerAdded = true
}
return super.onStartCommand(intent, flags, startId)
}
override fun onTaskRemoved(rootIntent: Intent?) {
Log.i("$TAG Task removed, doing nothing")
super.onTaskRemoved(rootIntent)
}
override fun onDestroy() {
Log.i("$TAG onDestroy")
coreContext.core.removeListener(coreListener)
listenerAdded = false
super.onDestroy()
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun createServiceNotification() {
Log.i("$TAG Creating notification")
buildNotification()
postNotification()
coreContext.postOnCoreThread { core ->
val downloadingFilesCount = core.remainingDownloadFileCount
val uploadingFilesCount = core.remainingUploadFileCount
updateNotificationContent(downloadingFilesCount, uploadingFilesCount)
}
}
@AnyThread
private fun buildNotification() {
val intent = Intent(applicationContext, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
mServiceNotification = builder.setContentTitle(
getString(R.string.notification_file_transfer_title)
)
.setContentText(getString(R.string.notification_file_transfer_startup_message))
.setSmallIcon(R.drawable.linphone_notification)
.setAutoCancel(false)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setWhen(System.currentTimeMillis())
.setShowWhen(false)
.setOngoing(true)
.setProgress(0, 0, true)
.setContentIntent(pendingIntent)
.build()
}
@WorkerThread
private fun updateNotificationContent(downloadingFilesCount: Int, uploadingFilesCount: Int) {
Log.i(
"$TAG [$downloadingFilesCount] file(s) being downloaded, [$uploadingFilesCount] file(s) being uploaded"
)
if (downloadingFilesCount == 0 && uploadingFilesCount == 0) {
Log.i("$TAG No more files being transferred, do not alter the notification")
return
}
val downloadText = resources.getQuantityString(
R.plurals.notification_file_transfer_download,
downloadingFilesCount,
"$downloadingFilesCount"
)
val uploadText = resources.getQuantityString(
R.plurals.notification_file_transfer_upload,
uploadingFilesCount,
"$uploadingFilesCount"
)
val message = if (downloadingFilesCount > 0 && uploadingFilesCount > 0) {
getString(
R.string.notification_file_transfer_upload_download_message,
downloadText,
uploadText
)
} else if (downloadingFilesCount > 0) {
downloadText
} else {
uploadText
}
if (mServiceNotification == null) {
buildNotification()
}
mServiceNotification = builder.setContentText(message).build()
postNotification()
}
@SuppressLint("MissingPermission")
@AnyThread
private fun postNotification() {
val notificationsManager = NotificationManagerCompat.from(this)
if (Compatibility.isPostNotificationsPermissionGranted(this)) {
if (mServiceNotification != null) {
Log.i("$TAG Sending notification to manager")
notificationsManager.notify(SERVICE_NOTIF_ID, mServiceNotification)
} else {
Log.e("$TAG Notification content hasn't been computed yet!")
}
} else {
Log.e("$TAG POST_NOTIFICATIONS permission wasn't granted!")
}
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2010-2023 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.core
import android.content.Intent
import android.os.IBinder
import androidx.annotation.MainThread
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.tools.Log
import org.linphone.core.tools.service.CoreService
@MainThread
class CoreInCallService : CoreService() {
companion object {
private const val TAG = "[Core InCall Service]"
}
override fun onCreate() {
super.onCreate()
Log.i("$TAG Created")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("$TAG onStartCommand")
coreContext.notificationsManager.onInCallServiceStarted(this)
return super.onStartCommand(intent, flags, startId)
}
override fun onTaskRemoved(rootIntent: Intent?) {
Log.i("$TAG Task removed, doing nothing")
super.onTaskRemoved(rootIntent)
}
override fun onDestroy() {
Log.i("$TAG onDestroy")
coreContext.notificationsManager.onInCallServiceDestroyed()
super.onDestroy()
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun createServiceNotificationChannel() {
// Do nothing, app's Notifications Manager will do the job
}
override fun createServiceNotification() {
// Do nothing, app's Notifications Manager will do the job
}
override fun showForegroundServiceNotification(isVideoCall: Boolean) {
// Do nothing, app's Notifications Manager will do the job
}
override fun hideForegroundServiceNotification() {
// Do nothing, app's Notifications Manager will do the job
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2010-2023 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.core
import android.app.Service
import android.content.Intent
import android.os.IBinder
import androidx.annotation.MainThread
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.tools.Log
@MainThread
class CoreKeepAliveThirdPartyAccountsService : Service() {
companion object {
private const val TAG = "[Core Keep Alive Third Party Accounts Service]"
}
override fun onCreate() {
super.onCreate()
Log.i("$TAG Created")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("$TAG onStartCommand")
coreContext.notificationsManager.onKeepAliveServiceStarted(this)
return super.onStartCommand(intent, flags, startId)
}
override fun onTaskRemoved(rootIntent: Intent?) {
Log.i("$TAG Task removed, doing nothing")
super.onTaskRemoved(rootIntent)
}
override fun onDestroy() {
Log.i("$TAG onDestroy")
coreContext.notificationsManager.onKeepAliveServiceDestroyed()
super.onDestroy()
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
}

View file

@ -20,62 +20,521 @@
package org.linphone.core
import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.AnyThread
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import org.linphone.BuildConfig
import java.io.File
import java.io.FileOutputStream
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.contacts.ContactLoader.Companion.LINPHONE_ADDRESS_BOOK_FRIEND_LIST
class CorePreferences
@UiThread
constructor(private val context: Context) {
companion object {
private const val TAG = "[Preferences]"
const val CONFIG_FILE_NAME = ".linphonerc"
}
class CorePreferences constructor(private val context: Context) {
private var _config: Config? = null
@get:AnyThread @set:WorkerThread
var config: Config
get() = _config ?: coreContext.core.config
set(value) {
_config = value
}
fun chatRoomMuted(id: String): Boolean {
val sharedPreferences: SharedPreferences = coreContext.context.getSharedPreferences(
"notifications",
Context.MODE_PRIVATE
)
return sharedPreferences.getBoolean(id, false)
}
@get:AnyThread @set:WorkerThread
var printLogsInLogcat: Boolean
get() = config.getBool("app", "debug", BuildConfig.DEBUG)
set(value) {
config.setBool("app", "debug", value)
}
fun muteChatRoom(id: String, mute: Boolean) {
val sharedPreferences: SharedPreferences = coreContext.context.getSharedPreferences(
"notifications",
Context.MODE_PRIVATE
)
val editor = sharedPreferences.edit()
editor.putBoolean(id, mute)
editor.apply()
}
@get:AnyThread @set:WorkerThread
var sendLogsToCrashlytics: Boolean
get() = config.getBool("app", "send_logs_to_crashlytics", BuildConfig.CRASHLYTICS_ENABLED)
set(value) {
config.setBool("app", "send_logs_to_crashlytics", value)
}
@get:AnyThread @set:WorkerThread
var firstLaunch: Boolean
get() = config.getBool("app", "first_6.0_launch", true)
set(value) {
config.setBool("app", "first_6.0_launch", value)
}
@get:AnyThread @set:WorkerThread
var linphoneConfigurationVersion: Int
get() = config.getInt("app", "config_version", 52005)
set(value) {
config.setInt("app", "config_version", value)
}
@get:AnyThread @set:WorkerThread
var autoStart: Boolean
get() = config.getBool("app", "auto_start", true)
set(value) {
config.setBool("app", "auto_start", value)
}
@get:AnyThread @set:WorkerThread
var checkForUpdateServerUrl: String
get() = config.getString("misc", "version_check_url_root", "").orEmpty()
set(value) {
config.setString("misc", "version_check_url_root", value)
}
@get:AnyThread @set:WorkerThread
var conditionsAndPrivacyPolicyAccepted: Boolean
get() = config.getBool("app", "read_and_agree_terms_and_privacy", false)
set(value) {
config.setBool("app", "read_and_agree_terms_and_privacy", value)
}
@get:AnyThread @set:WorkerThread
var publishPresence: Boolean
get() = config.getBool("app", "publish_presence", true)
set(value) {
config.setBool("app", "publish_presence", value)
}
@get:AnyThread @set:WorkerThread
var keepServiceAlive: Boolean
get() = config.getBool("app", "keep_service_alive", false)
set(value) {
config.setBool("app", "keep_service_alive", value)
}
@get:AnyThread @set:WorkerThread
var deviceName: String
get() = config.getString("app", "device", "").orEmpty().trim()
set(value) {
config.setString("app", "device", value.trim())
}
@get:AnyThread @set:WorkerThread
var showDeveloperSettings: Boolean
get() = config.getBool("ui", "show_developer_settings", false)
set(value) {
config.setBool("ui", "show_developer_settings", value)
}
// Call settings
// This won't be done if bluetooth or wired headset is used
@get:AnyThread @set:WorkerThread
var routeAudioToBluetoothWhenPossible: Boolean
get() = config.getBool("app", "route_audio_to_bluetooth_when_possible", true)
set(value) {
config.setBool("app", "route_audio_to_bluetooth_when_possible", value)
}
@get:AnyThread @set:WorkerThread
var routeAudioToSpeakerWhenVideoIsEnabled: Boolean
get() = config.getBool("app", "route_audio_to_speaker_when_video_enabled", true)
set(value) {
config.setBool("app", "route_audio_to_speaker_when_video_enabled", value)
}
@get:AnyThread @set:WorkerThread
var callRecordingUseSmffFormat: Boolean
get() = config.getBool("app", "use_smff_for_call_recording", false)
set(value) {
config.setBool("app", "use_smff_for_call_recording", value)
}
@get:AnyThread @set:WorkerThread
var automaticallyStartCallRecording: Boolean
get() = config.getBool("app", "auto_start_call_record", false)
set(value) {
config.setBool("app", "auto_start_call_record", value)
}
@get:AnyThread @set:WorkerThread
var showDialogWhenCallingDeviceUuidDirectly: Boolean
get() = config.getBool("app", "show_confirmation_dialog_zrtp_trust_call", true)
set(value) {
config.setBool("app", "show_confirmation_dialog_zrtp_trust_call", value)
}
@get:AnyThread @set:WorkerThread
var acceptEarlyMedia: Boolean
get() = config.getBool("sip", "incoming_calls_early_media", false)
set(value) {
config.setBool("sip", "incoming_calls_early_media", value)
}
@get:AnyThread @set:WorkerThread
var allowOutgoingEarlyMedia: Boolean
get() = config.getBool("misc", "real_early_media", false)
set(value) {
config.setBool("misc", "real_early_media", value)
}
@get:AnyThread @set:WorkerThread
var autoAnswerEnabled: Boolean
get() = config.getBool("app", "auto_answer", false)
set(value) {
config.setBool("app", "auto_answer", value)
}
@get:AnyThread @set:WorkerThread
var autoAnswerDelay: Int
get() = config.getInt("app", "auto_answer_delay", 0)
set(value) {
config.setInt("app", "auto_answer_delay", value)
}
@get:AnyThread @set:WorkerThread
var autoAnswerVideoCallsWithVideoDirectionSendReceive: Boolean
get() = config.getBool("app", "auto_answer_video_send_receive", false)
set(value) {
config.setBool("app", "auto_answer_video_send_receive", value)
}
// Conversation related
@get:AnyThread @set:WorkerThread
var markConversationAsReadWhenDismissingMessageNotification: Boolean
get() = config.getBool("app", "mark_as_read_notif_dismissal", false)
set(value) {
config.setBool("app", "mark_as_read_notif_dismissal", value)
}
@get:AnyThread @set:WorkerThread
var makePublicMediaFilesDownloaded: Boolean
// Keep old name for backward compatibility
get() = config.getBool("app", "make_downloaded_images_public_in_gallery", false)
set(value) {
config.setBool("app", "make_downloaded_images_public_in_gallery", value)
}
// Conference related
@get:AnyThread @set:WorkerThread
var createEndToEndEncryptedMeetingsAndGroupCalls: Boolean
get() = config.getBool("app", "create_e2e_encrypted_conferences", false)
set(value) {
config.setBool("app", "create_e2e_encrypted_conferences", value)
}
// Contacts related
@get:AnyThread @set:WorkerThread
var sortContactsByFirstName: Boolean
get() = config.getBool("ui", "sort_contacts_by_first_name", true) // If disabled, last name will be used
set(value) {
config.setBool("ui", "sort_contacts_by_first_name", value)
}
@get:AnyThread
var hideContactsWithoutPhoneNumberOrSipAddress: Boolean
get() = config.getBool("ui", "hide_contacts_without_phone_number_or_sip_address", false)
set(value) {
config.setBool("ui", "hide_contacts_without_phone_number_or_sip_address", value)
}
@get:AnyThread @set:WorkerThread
var contactsFilter: String
get() = config.getString("ui", "contacts_filter", "")!! // Default value must be empty!
set(value) {
config.setString("ui", "contacts_filter", value)
}
@get:AnyThread @set:WorkerThread
var showFavoriteContacts: Boolean
get() = config.getBool("ui", "show_favorites_contacts", true)
set(value) {
config.setBool("ui", "show_favorites_contacts", value)
}
@get:AnyThread @set:WorkerThread
var friendListInWhichStoreNewlyCreatedFriends: String
get() = config.getString(
"app",
"friend_list_to_store_newly_created_contacts",
LINPHONE_ADDRESS_BOOK_FRIEND_LIST
)!!
set(value) {
config.setString("app", "friend_list_to_store_newly_created_contacts", value)
}
@get:AnyThread @set:WorkerThread
var editNativeContactsInLinphone: Boolean
get() = config.getBool("ui", "edit_native_contact_in_linphone", false)
set(value) {
config.setBool("ui", "edit_native_contact_in_linphone", value)
}
@get:AnyThread @set:WorkerThread
var disableAddContact: Boolean
get() = config.getBool("ui", "disable_add_contact", false)
set(value) {
config.setBool("ui", "disable_add_contact", value)
}
// Voice recordings related
@get:AnyThread @set:WorkerThread
var voiceRecordingMaxDuration: Int
get() = config.getInt("app", "voice_recording_max_duration", 600000) // in ms
set(value) = config.setInt("app", "voice_recording_max_duration", value)
// User interface related
// -1 means auto, 0 no, 1 yes
@get:AnyThread @set:WorkerThread
var darkMode: Int
get() {
if (!darkModeAllowed) return 0
return config.getInt("app", "dark_mode", -1)
}
set(value) {
config.setInt("app", "dark_mode", value)
}
// Allows to make screenshots
@get:AnyThread @set:WorkerThread
var enableSecureMode: Boolean
get() = config.getBool("ui", "enable_secure_mode", true)
set(value) {
config.setBool("ui", "enable_secure_mode", value)
}
@get:AnyThread @set:WorkerThread
var automaticallyShowDialpad: Boolean
get() = config.getBool("ui", "automatically_show_dialpad", false)
set(value) {
config.setBool("ui", "automatically_show_dialpad", value)
}
@get:AnyThread @set:WorkerThread
var themeMainColor: String
get() = config.getString("ui", "theme_main_color", "orange")!!
set(value) {
config.setString("ui", "theme_main_color", value)
}
// Customization options
@get:AnyThread @set:WorkerThread
var showMicrophoneAndSpeakerVuMeters: Boolean
get() = config.getBool("ui", "show_mic_speaker_vu_meter", false)
set(value) {
config.setBool("ui", "show_mic_speaker_vu_meter", value)
}
@get:AnyThread @set:WorkerThread
var pushNotificationCompatibleDomains: Array<String>
get() = config.getStringList("app", "push_notification_domains", arrayOf("sip.linphone.org"))
set(value) {
config.setStringList("app", "push_notification_domains", value)
}
@get:AnyThread
val defaultDomain: String
get() = config.getString("app", "default_domain", "sip.linphone.org")!!
@get:AnyThread
val darkModeAllowed: Boolean
get() = config.getBool("ui", "dark_mode_allowed", true)
@get:AnyThread
val changeMainColorAllowed: Boolean
get() = config.getBool("ui", "change_main_color_allowed", false)
@get:AnyThread
val onlyDisplaySipUriUsername: Boolean
get() = config.getBool("ui", "only_display_sip_uri_username", false)
@get:AnyThread
val hideSipAddresses: Boolean
get() = config.getBool("ui", "hide_sip_addresses", false)
@get:AnyThread
val disableChat: Boolean
get() = config.getBool("ui", "disable_chat_feature", false)
@get:AnyThread
val disableMeetings: Boolean
get() = config.getBool("ui", "disable_meetings_feature", false)
@get:AnyThread
val disableBroadcasts: Boolean
get() = config.getBool("ui", "disable_broadcast_feature", true) // TODO FIXME: not implemented yet
@get:AnyThread
val disableCallRecordings: Boolean
get() = config.getBool("ui", "disable_call_recordings_feature", false)
@get:AnyThread
val maxAccountsCount: Int
get() = config.getInt("ui", "max_account", 0) // 0 means no max
@get:AnyThread
val hidePhoneNumbers: Boolean
get() = config.getBool("ui", "hide_phone_numbers", false)
@get:AnyThread
val hideSettings: Boolean
get() = config.getBool("ui", "hide_settings", false)
@get:AnyThread
val hideAccountSettings: Boolean
get() = config.getBool("ui", "hide_account_settings", false)
@get:AnyThread
val hideAdvancedSettings: Boolean
get() = config.getBool("ui", "hide_advanced_settings", false)
@get:AnyThread
val hideAssistantCreateAccount: Boolean
get() = config.getBool("ui", "assistant_hide_create_account", false)
@get:AnyThread
val hideAssistantScanQrCode: Boolean
get() = config.getBool("ui", "assistant_disable_qr_code", false)
@get:AnyThread
val hideAssistantThirdPartySipAccount: Boolean
get() = config.getBool("ui", "assistant_hide_third_party_account", false)
@get:AnyThread
val magicSearchResultsLimit: Int
get() = config.getInt("ui", "max_number_of_magic_search_results", 300)
@get:AnyThread
val singleSignOnClientId: String
get() = config.getString("app", "oidc_client_id", "linphone")!!
@get:AnyThread
val useUsernameAsSingleSignOnLoginHint: Boolean
get() = config.getBool("ui", "use_username_as_sso_login_hint", false)
@get:AnyThread
val thirdPartySipAccountDefaultTransport: String
get() = config.getString("ui", "assistant_third_party_sip_account_transport", "tls")!!
@get:AnyThread
val thirdPartySipAccountDefaultDomain: String
get() = config.getString("ui", "assistant_third_party_sip_account_domain", "")!!
@get:AnyThread
val assistantDirectlyGoToThirdPartySipAccountLogin: Boolean
get() = config.getBool(
"ui",
"assistant_go_directly_to_third_party_sip_account_login",
false
)
@get:AnyThread
val fetchContactsFromDefaultDirectory: Boolean
get() = config.getBool("app", "fetch_contacts_from_default_directory", true)
@get:AnyThread
val showLettersOnDialpad: Boolean
get() = config.getBool("ui", "show_letters_on_dialpad", true)
// Paths
@get:AnyThread
val configPath: String
get() = context.filesDir.absolutePath + "/.linphonerc"
get() = context.filesDir.absolutePath + "/" + CONFIG_FILE_NAME
@get:AnyThread
val factoryConfigPath: String
get() = context.filesDir.absolutePath + "/linphonerc"
@get:AnyThread
val linphoneDefaultValuesPath: String
get() = context.filesDir.absolutePath + "/assistant_linphone_default_values"
@get:AnyThread
val thirdPartyDefaultValuesPath: String
get() = context.filesDir.absolutePath + "/assistant_third_party_default_values"
@get:AnyThread
val vfsCachePath: String
get() = context.cacheDir.absolutePath + "/evfs/"
@get:AnyThread
val ssoCacheFile: String
get() = context.filesDir.absolutePath + "/auth_state.json"
@get:AnyThread
val messageReceivedInVisibleConversationNotificationSound: String
get() = context.filesDir.absolutePath + "/share/sounds/linphone/incoming_chat.wav"
@UiThread
fun copyAssetsFromPackage() {
copy("linphonerc_default", configPath)
copy("linphonerc_factory", factoryConfigPath, true)
copy("assistant_linphone_default_values", linphoneDefaultValuesPath, true)
copy("assistant_third_party_default_values", thirdPartyDefaultValuesPath, true)
}
@AnyThread
fun clearPreviousGrammars() {
val cpimGrammar = File("${context.filesDir.absolutePath}/share/belr/grammars/cpim_grammar")
if (cpimGrammar.exists()) {
cpimGrammar.delete()
}
val icsGrammar = File("${context.filesDir.absolutePath}/share/belr/grammars/ics_grammar")
if (icsGrammar.exists()) {
icsGrammar.delete()
}
val identityGrammar = File(
"${context.filesDir.absolutePath}/share/belr/grammars/identity_grammar"
)
if (identityGrammar.exists()) {
identityGrammar.delete()
}
val mwiGrammar = File("${context.filesDir.absolutePath}/share/belr/grammars/mwi_grammar")
if (mwiGrammar.exists()) {
mwiGrammar.delete()
}
val sdpGrammar = File("${context.filesDir.absolutePath}/share/belr/grammars/sdp_grammar")
if (sdpGrammar.exists()) {
sdpGrammar.delete()
}
val sipGrammar = File("${context.filesDir.absolutePath}/share/belr/grammars/sip_grammar")
if (sipGrammar.exists()) {
sipGrammar.delete()
}
val vcard3Grammar = File(
"${context.filesDir.absolutePath}/share/belr/grammars/vcard3_grammar"
)
if (vcard3Grammar.exists()) {
vcard3Grammar.delete()
}
val vcardGrammar = File(
"${context.filesDir.absolutePath}/share/belr/grammars/vcard_grammar"
)
if (vcardGrammar.exists()) {
vcardGrammar.delete()
}
}
@AnyThread
private fun copy(from: String, to: String, overrideIfExists: Boolean = false) {
val outFile = File(to)
if (outFile.exists()) {
if (!overrideIfExists) {
android.util.Log.i(
context.getString(org.linphone.R.string.app_name),
"[Preferences] File $to already exists"
"$TAG File $to already exists"
)
return
}
}
android.util.Log.i(
context.getString(org.linphone.R.string.app_name),
"[Preferences] Overriding $to by $from asset"
"$TAG Overriding $to by $from asset"
)
val outStream = FileOutputStream(outFile)

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2010-2023 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.core
import android.app.PendingIntent
import android.content.Intent
import android.os.IBinder
import androidx.annotation.MainThread
import androidx.core.app.NotificationCompat
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.core.tools.service.PushService
import org.linphone.ui.main.MainActivity
@MainThread
class CorePushService : PushService() {
companion object {
private const val TAG = "[Core Push Service]"
}
override fun onCreate() {
super.onCreate()
Log.i("$TAG Created")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("$TAG onStartCommand")
return super.onStartCommand(intent, flags, startId)
}
override fun onTaskRemoved(rootIntent: Intent?) {
Log.i("$TAG Task removed, doing nothing")
super.onTaskRemoved(rootIntent)
}
override fun onDestroy() {
Log.i("$TAG onDestroy")
super.onDestroy()
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun createServiceNotification() {
Log.i("$TAG Creating notification")
val intent = Intent(applicationContext, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
mServiceNotification = NotificationCompat.Builder(
this,
SERVICE_NOTIFICATION_CHANNEL_ID
)
.setContentTitle(getString(R.string.notification_push_received_title))
.setContentText(getString(R.string.notification_push_received_message))
.setSmallIcon(R.drawable.linphone_notification)
.setAutoCancel(false)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setWhen(System.currentTimeMillis())
.setShowWhen(false)
.setOngoing(true)
.setContentIntent(pendingIntent)
.build()
}
}

View file

@ -0,0 +1,224 @@
/*
* Copyright (c) 2010-2023 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.core
import android.content.Context
import android.content.SharedPreferences
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Pair
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import org.linphone.LinphoneApplication.Companion.corePreferences
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.MessageDigest
import java.util.UUID
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import org.linphone.core.tools.Log
import androidx.core.content.edit
class VFS {
companion object {
private const val TAG = "[VFS]"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val ALIAS = "vfs"
private const val LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256 = 2
private const val VFS_IV = "vfsiv"
private const val VFS_KEY = "vfskey"
private const val ENCRYPTED_SHARED_PREFS_FILE = "encrypted.pref"
fun isEnabled(context: Context): Boolean {
val preferences = getEncryptedSharedPreferences(context)
if (preferences == null) {
Log.e("$TAG Failed to get encrypted shared preferences!")
return false
}
return preferences.getBoolean("vfs_enabled", false)
}
fun enable(context: Context): Boolean {
val preferences = getEncryptedSharedPreferences(context)
if (preferences == null) {
Log.e("$TAG Failed to get encrypted shared preferences, VFS won't be enabled!")
return false
}
if (preferences.getBoolean("vfs_enabled", false)) {
Log.w("$TAG VFS is already enabled, skipping...")
return false
}
preferences.edit { putBoolean("vfs_enabled", true) }
if (corePreferences.makePublicMediaFilesDownloaded) {
Log.w("$TAG VFS is now enabled, disabling auto export of media files to native gallery")
corePreferences.makePublicMediaFilesDownloaded = false
}
return true
}
fun setup(context: Context) {
// Use Android logger as our isn't ready yet
try {
android.util.Log.i(TAG, "$TAG Initializing...")
val preferences = getEncryptedSharedPreferences(context)
if (preferences == null) {
Log.e("$TAG Failed to get encrypted shared preferences, can't initialize VFS!")
return
}
if (preferences.getString(VFS_IV, null) == null) {
android.util.Log.i(TAG, "$TAG No initialization vector found, generating it")
generateSecretKey()
encryptToken(generateToken()).let { data ->
preferences
.edit(commit = true) {
putString(VFS_IV, data.first)
.putString(VFS_KEY, data.second)
}
}
}
Factory.instance().setVfsEncryption(
LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256,
getVfsKey(preferences).toByteArray().copyOfRange(0, 32),
32
)
android.util.Log.i(TAG, "$TAG Initialized")
} catch (e: Exception) {
android.util.Log.wtf(TAG, "$TAG Unable to activate VFS encryption: $e")
}
}
private fun getEncryptedSharedPreferences(context: Context): SharedPreferences? {
return try {
val masterKey: MasterKey = MasterKey.Builder(
context,
MasterKey.DEFAULT_MASTER_KEY_ALIAS
).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
EncryptedSharedPreferences.create(
context,
ENCRYPTED_SHARED_PREFS_FILE,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
} catch (kse: KeyStoreException) {
Log.e("[VFS] Keystore exception: $kse")
null
} catch (e: Exception) {
Log.e("[VFS] Exception: $e")
null
}
}
@Throws(java.lang.Exception::class)
private fun generateSecretKey() {
val keyGenerator =
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
keyGenerator.init(
KeyGenParameterSpec.Builder(
ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
keyGenerator.generateKey()
}
@Throws(java.lang.Exception::class)
private fun getSecretKey(): SecretKey? {
val ks = KeyStore.getInstance(ANDROID_KEY_STORE)
ks.load(null)
val entry = ks.getEntry(ALIAS, null) as KeyStore.SecretKeyEntry
return entry.secretKey
}
@Throws(java.lang.Exception::class)
private fun generateToken(): String {
return sha512(UUID.randomUUID().toString())
}
@Throws(java.lang.Exception::class)
private fun encryptData(textToEncrypt: String): Pair<ByteArray, ByteArray> {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
val iv = cipher.iv
return Pair(
iv,
cipher.doFinal(textToEncrypt.toByteArray(StandardCharsets.UTF_8))
)
}
@Throws(java.lang.Exception::class)
private fun decryptData(encrypted: String?, encryptionIv: ByteArray): String {
val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(128, encryptionIv)
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
val encryptedData = Base64.decode(encrypted, Base64.DEFAULT)
return String(cipher.doFinal(encryptedData), StandardCharsets.UTF_8)
}
@Throws(java.lang.Exception::class)
private fun encryptToken(token: String): Pair<String?, String?> {
val encryptedData = encryptData(token)
return Pair(
Base64.encodeToString(encryptedData.first, Base64.DEFAULT),
Base64.encodeToString(encryptedData.second, Base64.DEFAULT)
)
}
@Throws(java.lang.Exception::class)
private fun sha512(input: String): String {
val md = MessageDigest.getInstance("SHA-512")
val messageDigest = md.digest(input.toByteArray())
val no = BigInteger(1, messageDigest)
var hashtext = no.toString(16)
while (hashtext.length < 32) {
hashtext = "0$hashtext"
}
return hashtext
}
@Throws(java.lang.Exception::class)
private fun getVfsKey(sharedPreferences: SharedPreferences): String {
val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
keyStore.load(null)
return decryptData(
sharedPreferences.getString(VFS_KEY, null),
Base64.decode(sharedPreferences.getString(VFS_IV, null), Base64.DEFAULT)
)
}
}
}

View file

@ -0,0 +1,181 @@
/*
* Copyright (c) 2010-2023 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.notifications
import android.app.NotificationManager
import android.app.RemoteInput
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Address
import org.linphone.core.AudioDevice
import org.linphone.core.ConferenceParams
import org.linphone.core.tools.Log
import org.linphone.utils.AudioUtils
class NotificationBroadcastReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "[Notification Broadcast Receiver]"
}
override fun onReceive(context: Context, intent: Intent) {
val notificationId = intent.getIntExtra(NotificationsManager.INTENT_NOTIF_ID, 0)
val action = intent.action
Log.i("$TAG Got notification broadcast for ID [$notificationId] with action [$action]")
// Wait for coreContext to be ready to handle intent
while (!coreContext.isReady()) {
Thread.sleep(50)
}
if (
action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION ||
action == NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION ||
action == NotificationsManager.INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION
) {
handleCallIntent(intent, notificationId, action)
} else if (
action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION ||
action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION
) {
handleChatIntent(context, intent, notificationId, action)
}
}
private fun handleCallIntent(intent: Intent, notificationId: Int, action: String) {
val remoteSipUri = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_SIP_URI)
if (remoteSipUri == null) {
Log.e("$TAG Remote SIP address is null for call notification ID [$notificationId]")
return
}
coreContext.postOnCoreThread { core ->
val call = core.calls.find {
it.remoteAddress.asStringUriOnly() == remoteSipUri
}
if (call == null) {
Log.e("$TAG Couldn't find call from remote address [$remoteSipUri]")
} else {
when (action) {
NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION -> {
Log.i("$TAG Answering call with remote address [$remoteSipUri]")
coreContext.answerCall(call)
}
NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION -> {
Log.i("$TAG Declining/terminating call with remote address [$remoteSipUri]")
coreContext.terminateCall(call)
}
NotificationsManager.INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION -> {
val audioDevice = call.outputAudioDevice
val isUsingSpeaker = audioDevice?.type == AudioDevice.Type.Speaker
if (isUsingSpeaker) {
Log.i("$TAG Routing audio to earpiece for call [$remoteSipUri]")
AudioUtils.routeAudioToEarpiece(call)
} else {
Log.i("$TAG Routing audio to speaker for call [$remoteSipUri]")
AudioUtils.routeAudioToSpeaker(call)
}
}
}
}
}
}
private fun handleChatIntent(context: Context, intent: Intent, notificationId: Int, action: String) {
val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_SIP_URI)
if (remoteSipAddress == null) {
Log.e("$TAG Remote SIP address is null for notification ID [$notificationId]")
return
}
val localIdentity = intent.getStringExtra(NotificationsManager.INTENT_LOCAL_IDENTITY)
if (localIdentity == null) {
Log.e("$TAG Local identity is null for notification ID [$notificationId]")
return
}
val reply = getMessageText(intent)?.toString()
if (action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) {
if (reply == null) {
Log.e("$TAG Couldn't get reply text")
return
}
}
coreContext.postOnCoreThread { core ->
val remoteAddress = core.interpretUrl(remoteSipAddress, false)
if (remoteAddress == null) {
Log.e(
"$TAG Couldn't interpret remote address [$remoteSipAddress]"
)
return@postOnCoreThread
}
val localAddress = core.interpretUrl(localIdentity, false)
if (localAddress == null) {
Log.e(
"$TAG Couldn't interpret local address [$localIdentity]"
)
return@postOnCoreThread
}
val params: ConferenceParams? = null
val room = core.searchChatRoom(
params,
localAddress,
remoteAddress,
arrayOfNulls<Address>(
0
)
)
if (room == null) {
Log.e(
"$TAG Couldn't find conversation for remote address [$remoteSipAddress] and local address [$localIdentity]"
)
return@postOnCoreThread
}
if (action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) {
val msg = room.createMessageFromUtf8(reply)
msg.userData = notificationId
msg.addListener(coreContext.notificationsManager.chatMessageListener)
msg.send()
Log.i("$TAG Reply sent for notif id [$notificationId]")
} else if (action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION) {
Log.i("$TAG Marking chat room from notification id [$notificationId] as read")
room.markAsRead()
if (!coreContext.notificationsManager.dismissChatNotification(room)) {
Log.w(
"$TAG Notifications Manager failed to cancel notification"
)
val notificationManager = context.getSystemService(
NotificationManager::class.java
)
notificationManager.cancel(NotificationsManager.CHAT_TAG, notificationId)
}
}
}
}
private fun getMessageText(intent: Intent): CharSequence? {
val remoteInput = RemoteInput.getResultsFromIntent(intent)
return remoteInput?.getCharSequence(NotificationsManager.KEY_TEXT_REPLY)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,251 @@
/*
* Copyright (c) 2010-2023 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.telecom
import android.telecom.DisconnectCause
import androidx.annotation.WorkerThread
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.CallControlResult
import androidx.core.telecom.CallControlScope
import androidx.core.telecom.CallEndpointCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.Call
import org.linphone.core.CallListenerStub
import org.linphone.core.Reason
import org.linphone.core.tools.Log
import org.linphone.utils.AudioUtils
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
class TelecomCallControlCallback(
private val call: Call,
private val callControl: CallControlScope,
private val scope: CoroutineScope
) {
companion object {
private const val TAG = "[Telecom Call Control Callback]"
}
private var mutedByTelecomManager = false
private val callListener = object : CallListenerStub() {
@WorkerThread
override fun onStateChanged(call: Call, state: Call.State?, message: String) {
Log.i("$TAG Call [${call.remoteAddress.asStringUriOnly()}] state changed [$state]")
if (state == Call.State.Connected) {
if (call.dir == Call.Dir.Incoming) {
answerCall()
} else {
scope.launch {
Log.i("$TAG Setting call active")
val result = callControl.setActive()
if (result is CallControlResult.Error) {
Log.e("$TAG Failed to set call control active: $result")
}
}
}
} else if (state == Call.State.End) {
callEnded()
} else if (state == Call.State.Error) {
callError(message)
} else if (state == Call.State.Pausing) {
scope.launch {
Log.i("$TAG Pausing call")
val result = callControl.setInactive()
if (result is CallControlResult.Error) {
Log.e("$TAG Failed to set call control inactive: $result")
}
}
} else if (state == Call.State.Resuming) {
scope.launch {
Log.i("$TAG Resuming call")
val result = callControl.setActive()
if (result is CallControlResult.Error) {
Log.e("$TAG Failed to set call control active: $result")
}
}
}
}
}
init {
// NEVER CALL ANY METHOD FROM callControl OBJECT IN HERE!
Log.i("$TAG Created callback for call")
coreContext.postOnCoreThread {
call.addListener(callListener)
}
}
fun onCallControlCallbackSet() {
Log.i(
"$TAG Callback have been set for call, Telecom call ID is [${callControl.getCallId()}]"
)
coreContext.postOnCoreThread {
val state = call.state
Log.i("$TAG Call state currently is [$state]")
when (state) {
Call.State.Connected, Call.State.StreamsRunning -> answerCall()
Call.State.End -> callEnded()
Call.State.Error -> callError("")
Call.State.Released -> callEnded()
else -> {} // doing nothing
}
}
callControl.availableEndpoints.onEach { list ->
Log.i("$TAG New available audio endpoints list but ignoring it")
}.launchIn(scope)
callControl.currentCallEndpoint.onEach { endpoint ->
Log.i("$TAG Android requests us to use [${endpoint.name}] audio endpoint with type [${endpointTypeToString(endpoint.type)}], ignoring it")
}.launchIn(scope)
callControl.isMuted.onEach { muted ->
coreContext.postOnCoreThread {
val callState = call.state
Log.i(
"$TAG We're asked to [${if (muted) "mute" else "unmute"}] the call in state [$callState]"
)
// Only follow un-mute requests for not outgoing calls (such as joining a conference muted)
// and if connected to Android Auto that has a way to let user mute/unmute from the car directly
// or if we muted the call previously following Telecom Manager request.
if (muted || mutedByTelecomManager || (!LinphoneUtils.isCallOutgoing(callState, false) && coreContext.isConnectedToAndroidAuto)) {
mutedByTelecomManager = muted
call.microphoneMuted = muted
coreContext.refreshMicrophoneMuteStateEvent.postValue(Event(true))
} else {
if (coreContext.isConnectedToAndroidAuto) {
Log.w(
"$TAG Not following unmute request because call is in state [$callState]"
)
} else {
Log.w(
"$TAG Not following unmute request because user isn't connected to Android Auto and call is in state [$callState]"
)
}
}
}
}.launchIn(scope)
}
private fun answerCall() {
val isVideo = LinphoneUtils.isVideoEnabled(call)
val type = if (isVideo) {
CallAttributesCompat.CALL_TYPE_VIDEO_CALL
} else {
CallAttributesCompat.CALL_TYPE_AUDIO_CALL
}
scope.launch {
Log.i("$TAG Answering [${if (isVideo) "video" else "audio"}] call")
val result = callControl.answer(type)
if (result is CallControlResult.Error) {
Log.e("$TAG Failed to answer call control: $result")
}
}
if (isVideo && corePreferences.routeAudioToSpeakerWhenVideoIsEnabled) {
Log.i("$TAG Answering video call, routing audio to speaker")
AudioUtils.routeAudioToSpeaker(call)
}
}
private fun callEnded() {
val reason = call.reason
val direction = call.dir
scope.launch {
val disconnectCause = when (reason) {
Reason.NotAnswered -> DisconnectCause.REMOTE
Reason.Declined -> DisconnectCause.REJECTED
Reason.Busy -> {
if (direction == Call.Dir.Incoming) {
DisconnectCause.MISSED
} else {
DisconnectCause.BUSY
}
}
else -> DisconnectCause.LOCAL
}
Log.i("$TAG Disconnecting [${if (direction == Call.Dir.Incoming)"incoming" else "outgoing"}] call with cause [${disconnectCauseToString(disconnectCause)}] because it has ended with reason [$reason]")
try {
val result = callControl.disconnect(DisconnectCause(disconnectCause))
if (result is CallControlResult.Error) {
Log.e("$TAG Failed to disconnect call control: $result")
}
} catch (ise: IllegalArgumentException) {
Log.e("$TAG Couldn't disconnect call control with cause [${disconnectCauseToString(disconnectCause)}]: $ise")
}
}
}
private fun callError(message: String) {
val reason = call.reason
scope.launch {
// For some reason DisconnectCause.ERROR or DisconnectCause.BUSY triggers an IllegalArgumentException with following message
// Valid DisconnectCause codes are limited to [DisconnectCause.LOCAL, DisconnectCause.REMOTE, DisconnectCause.MISSED, or DisconnectCause.REJECTED]
val disconnectCause = DisconnectCause.REJECTED
Log.w("$TAG Disconnecting call with cause [${disconnectCauseToString(disconnectCause)}] due to error [$message] and reason [$reason]")
try {
val result = callControl.disconnect(DisconnectCause(disconnectCause))
if (result is CallControlResult.Error) {
Log.e("$TAG Failed to disconnect call control: $result")
}
} catch (ise: IllegalArgumentException) {
Log.e("$TAG Couldn't disconnect call control with cause [${disconnectCauseToString(disconnectCause)}]: $ise")
}
}
}
private fun disconnectCauseToString(cause: Int): String {
return when (cause) {
DisconnectCause.UNKNOWN -> "UNKNOWN"
DisconnectCause.ERROR -> "ERROR"
DisconnectCause.LOCAL -> "LOCAL"
DisconnectCause.REMOTE -> "REMOTE"
DisconnectCause.CANCELED -> "CANCELED"
DisconnectCause.MISSED -> "MISSED"
DisconnectCause.REJECTED -> "REJECTED"
DisconnectCause.BUSY -> "BUSY"
DisconnectCause.RESTRICTED -> "RESTRICTED"
DisconnectCause.OTHER -> "OTHER"
DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED -> "CONNECTION_MANAGER_NOT_SUPPORTED"
DisconnectCause.ANSWERED_ELSEWHERE -> "ANSWERED_ELSEWHERE"
DisconnectCause.CALL_PULLED -> "CALL_PULLED"
else -> "UNEXPECTED: $cause"
}
}
private fun endpointTypeToString(type: Int): String {
return when (type) {
CallEndpointCompat.TYPE_UNKNOWN -> "UNKNOWN"
CallEndpointCompat.TYPE_EARPIECE -> "EARPIECE"
CallEndpointCompat.TYPE_BLUETOOTH -> "BLUETOOTH"
CallEndpointCompat.TYPE_WIRED_HEADSET -> "WIRED HEADSET"
CallEndpointCompat.TYPE_SPEAKER -> "SPEAKER"
CallEndpointCompat.TYPE_STREAMING -> "STREAMING"
else -> "UNEXPECTED: $type"
}
}
}

View file

@ -0,0 +1,211 @@
/*
* Copyright (c) 2010-2023 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.telecom
import android.content.Context
import androidx.annotation.WorkerThread
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.CallException
import androidx.core.telecom.CallsManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Call
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
import androidx.core.net.toUri
class TelecomManager
@WorkerThread
constructor(context: Context) {
companion object {
private const val TAG = "[Telecom Manager]"
}
private val callsManager = CallsManager(context)
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val map = HashMap<String, TelecomCallControlCallback>()
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onCallStateChanged(
core: Core,
call: Call,
state: Call.State?,
message: String
) {
if (state == Call.State.IncomingReceived || state == Call.State.OutgoingProgress) {
onCallCreated(call)
}
}
@WorkerThread
override fun onLastCallEnded(core: Core) {
currentlyFollowedCalls = 0
}
}
private val hasTelecomFeature = context.packageManager.hasSystemFeature("android.software.telecom")
private var currentlyFollowedCalls: Int = 0
init {
Log.i(
"$TAG android.software.telecom feature is [${if (hasTelecomFeature) "available" else "not available"}]"
)
try {
callsManager.registerAppWithTelecom(CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING)
Log.i("$TAG App has been registered with Telecom")
} catch (e: Exception) {
Log.e("$TAG Can't init TelecomManager: $e")
}
}
@WorkerThread
fun onCallCreated(call: Call) {
Log.i("$TAG Call to [${call.remoteAddress.asStringUriOnly()}] created in state [${call.state}]")
val address = call.callLog.remoteAddress
val uri = address.asStringUriOnly().toUri()
val direction = if (call.dir == Call.Dir.Outgoing) {
CallAttributesCompat.DIRECTION_OUTGOING
} else {
CallAttributesCompat.DIRECTION_INCOMING
}
val capabilities = CallAttributesCompat.SUPPORTS_SET_INACTIVE or CallAttributesCompat.SUPPORTS_TRANSFER
val conferenceInfo = LinphoneUtils.getConferenceInfoIfAny(call)
val displayName = if (call.conference != null || conferenceInfo != null) {
conferenceInfo?.subject ?: call.conference?.subject ?: LinphoneUtils.getDisplayName(address)
} else {
val friend = coreContext.contactsManager.findContactByAddress(address)
friend?.name ?: LinphoneUtils.getDisplayName(address)
}
// Always set type to video (if enabled in Core) as it indicates that video is supported, not that it's being used at the time
// https://developer.android.com/reference/kotlin/androidx/core/telecom/CallAttributesCompat#CALL_TYPE_VIDEO_CALL()
val type = if (!call.core.isVideoEnabled) {
CallAttributesCompat.CALL_TYPE_AUDIO_CALL
} else {
CallAttributesCompat.CALL_TYPE_VIDEO_CALL
}
scope.launch {
try {
val callAttributes = CallAttributesCompat(
displayName,
uri,
direction,
type,
capabilities
)
Log.i("$TAG Adding call to Telecom's CallsManager with attributes [$callAttributes]")
callsManager.addCall(
callAttributes,
{ callType -> // onAnswer
Log.i("$TAG We're asked to answer the call with type [$callType]")
coreContext.postOnCoreThread {
if (LinphoneUtils.isCallIncoming(call.state)) {
Log.i("$TAG Answering call")
coreContext.answerCall(call)
}
}
},
{ disconnectCause -> // onDisconnect
Log.i(
"$TAG We're asked to terminate the call with reason [$disconnectCause]"
)
coreContext.postOnCoreThread {
coreContext.terminateCall(call)
}
currentlyFollowedCalls -= 1
},
{ // onSetActive
Log.i("$TAG We're asked to resume the call")
coreContext.postOnCoreThread {
Log.i("$TAG Resuming call")
call.resume()
}
},
{ // onSetInactive
Log.i("$TAG We're asked to pause the call")
coreContext.postOnCoreThread {
Log.i("$TAG Pausing call")
call.pause()
}
}
) {
val callbacks = TelecomCallControlCallback(call, this, scope)
// We must first call setCallback on callControlScope before using it
callbacks.onCallControlCallbackSet()
currentlyFollowedCalls += 1
Log.i("$TAG Call added to Telecom's CallsManager")
coreContext.postOnCoreThread {
val callId = call.callLog.callId.orEmpty()
if (callId.isNotEmpty()) {
Log.i("$TAG Storing our callbacks for call ID [$callId]")
map[callId] = callbacks
}
}
}
} catch (ce: CallException) {
Log.e("$TAG Failed to add call to Telecom's CallsManager: $ce")
} catch (se: SecurityException) {
Log.e("$TAG Security exception trying to add call to Telecom's CallsManager: $se")
} catch (ise: IllegalArgumentException) {
Log.e("$TAG Illegal argument exception trying to add call to Telecom's CallsManager: $ise")
} catch (e: Exception) {
Log.e("$TAG Exception trying to add call to Telecom's CallsManager: $e")
}
}
}
@WorkerThread
fun onCoreStarted(core: Core) {
Log.i("$TAG Core has been started")
if (hasTelecomFeature) {
core.addListener(coreListener)
} else {
Log.w(
"$TAG android.software.telecom feature is not available, enable audio focus requests in Linphone SDK"
)
coreContext.core.config.setBool("audio", "android_disable_audio_focus_requests", false)
}
}
@WorkerThread
fun onCoreStopped(core: Core) {
Log.i("$TAG Core is being stopped")
if (hasTelecomFeature) {
core.removeListener(coreListener)
}
}
}

View file

@ -0,0 +1,134 @@
/*
* Copyright (c) 2010-2024 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.telecom.auto
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon
import androidx.car.app.model.GridItem
import androidx.car.app.model.GridTemplate
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.contacts.AvatarGenerator
import org.linphone.contacts.getAvatarBitmap
import org.linphone.core.MagicSearch
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.LinphoneUtils
class AndroidAutoScreen(context: CarContext) : Screen(context) {
companion object {
private const val TAG = "[Android Auto Screen]"
}
private val favoritesList = arrayListOf<GridItem>()
private var loading = true
init {
Log.i(
"$TAG Creating favorites contacts list template for host with API level [${carContext.carAppApiLevel}]"
)
coreContext.postOnCoreThread { core ->
val magicSearch = core.createMagicSearch()
val results = magicSearch.getContactsList(
"",
LinphoneUtils.getDefaultAccount()?.params?.domain.orEmpty(),
MagicSearch.Source.FavoriteFriends.toInt(),
MagicSearch.Aggregation.Friend
)
val favorites = arrayListOf<GridItem>()
for (result in results) {
val builder = GridItem.Builder()
val friend = result.friend ?: continue
builder.setTitle(friend.name)
Log.i("$TAG Creating car icon for friend [${friend.name}]")
try {
val bitmap = friend.getAvatarBitmap(true) ?: AvatarGenerator(
coreContext.context
).setInitials(
AppUtils.getInitials(friend.name.orEmpty())
).buildBitmap(useTransparentBackground = false)
builder.setImage(
CarIcon.Builder(IconCompat.createWithBitmap(bitmap))
.build(),
GridItem.IMAGE_TYPE_LARGE
)
} catch (e: Exception) {
Log.e("$TAG Exception trying to create CarIcon: $e")
}
builder.setOnClickListener {
val address = friend.address ?: friend.addresses.firstOrNull()
if (address != null) {
Log.i("$TAG Starting audio call to [${address.asStringUriOnly()}]")
coreContext.startAudioCall(address)
}
}
try {
val item = builder.build()
favorites.add(item)
} catch (e: Exception) {
Log.e("$TAG Failed to build grid item: $e")
}
}
loading = false
Log.i("$TAG Processed [${favorites.size}] favorites")
coreContext.postOnMainThread {
favoritesList.addAll(favorites)
invalidate()
}
}
}
override fun onGetTemplate(): Template {
Log.i("$TAG onGetTemplate called, favorites are [${if (loading) "loading" else "loaded"}]")
val listBuilder = ItemList.Builder()
listBuilder.setNoItemsMessage(
carContext.getString(R.string.car_favorites_contacts_list_empty)
)
for (favorite in favoritesList) {
listBuilder.addItem(favorite)
}
val list = listBuilder.build()
val header = Header.Builder()
.setTitle(carContext.getString(R.string.car_favorites_contacts_title))
.setStartHeaderAction(Action.APP_ICON)
.build()
val gridBuilder = GridTemplate.Builder()
gridBuilder.setHeader(header)
gridBuilder.setLoading(loading)
if (!loading) {
Log.i("$TAG Added [${favoritesList.size}] favorites items to grid")
gridBuilder.setSingleList(list)
}
return gridBuilder.build()
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2010-2024 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.telecom.auto
import android.content.pm.ApplicationInfo
import androidx.car.app.CarAppService
import androidx.car.app.Session
import androidx.car.app.validation.HostValidator
import org.linphone.R
import org.linphone.core.tools.Log
class AndroidAutoService : CarAppService() {
companion object {
private const val TAG = "[Android Auto Service]"
}
override fun createHostValidator(): HostValidator {
val host = hostInfo
Log.i("$TAG Host is [${host?.packageName}] with UID [${host?.uid}]")
val validator = HostValidator.Builder(applicationContext)
.addAllowedHosts(R.array.hosts_allowlist_sample_copy) // androidx.car.app.R.array.hosts_allowlist_sample
.build()
if (host != null) {
val allowed = validator.isValidHost(host)
Log.i("$TAG Host is [${if (allowed) "allowed" else "not allowed"}] in our validator")
} else {
Log.w("$TAG Host is null!")
}
return if ((applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
Log.w("$TAG App is in debug mode, allowing all hosts")
HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
} else {
validator
}
}
override fun onCreateSession(): Session {
Log.i("$TAG Creating Session object")
return AndroidAutoSession()
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2010-2024 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.telecom.auto
import android.content.Intent
import androidx.car.app.Screen
import androidx.car.app.Session
import org.linphone.core.tools.Log
class AndroidAutoSession : Session() {
companion object {
private const val TAG = "[Android Auto Session]"
}
override fun onCreateScreen(intent: Intent): Screen {
Log.i("$TAG Creating Screen object for host with API level [${carContext.carAppApiLevel}]")
return AndroidAutoScreen(carContext)
}
}

View file

@ -0,0 +1,271 @@
/*
* Copyright (c) 2010-2023 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.ui
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.ViewGroup
import android.view.WindowManager
import androidx.annotation.DrawableRes
import androidx.annotation.MainThread
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.utils.ToastUtils
import org.linphone.utils.slideInToastFromTop
import org.linphone.utils.slideInToastFromTopForDuration
@MainThread
open class GenericActivity : AppCompatActivity() {
companion object {
private const val TAG = "[Generic Activity]"
}
private lateinit var toastsArea: ViewGroup
private var mainColor: String = "orange"
override fun getTheme(): Resources.Theme {
mainColor = corePreferences.themeMainColor
val theme = super.theme
when (mainColor) {
"yellow" -> theme.applyStyle(R.style.Theme_LinphoneYellow, true)
"green" -> theme.applyStyle(R.style.Theme_LinphoneGreen, true)
"blue" -> theme.applyStyle(R.style.Theme_LinphoneBlue, true)
"red" -> theme.applyStyle(R.style.Theme_LinphoneRed, true)
"pink" -> theme.applyStyle(R.style.Theme_LinphonePink, true)
"purple" -> theme.applyStyle(R.style.Theme_LinphonePurple, true)
else -> theme.applyStyle(R.style.Theme_Linphone, true)
}
return theme
}
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
enableWindowSecureMode(corePreferences.enableSecureMode)
val nightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
val darkModeEnabled = corePreferences.darkMode
Log.i(
"$TAG Theme selected in config file is [${if (darkModeEnabled == -1) "auto" else if (darkModeEnabled == 0) "light" else "dark"}]"
)
when (nightMode) {
Configuration.UI_MODE_NIGHT_NO, Configuration.UI_MODE_NIGHT_UNDEFINED -> {
if (darkModeEnabled == 1) {
Compatibility.forceDarkMode(this)
}
}
Configuration.UI_MODE_NIGHT_YES -> {
if (darkModeEnabled == 0) {
Compatibility.forceLightMode(this)
}
}
}
super.onCreate(savedInstanceState)
}
protected fun checkMainColorTheme() {
if (mainColor != corePreferences.themeMainColor) {
Log.i("$TAG Main color setting has changed, re-creating activity")
recreate()
}
}
fun setUpToastsArea(viewGroup: ViewGroup) {
toastsArea = viewGroup
}
fun showGreenToast(
message: String,
@DrawableRes icon: Int,
duration: Long = 4000,
doNotTint: Boolean = false
) {
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val greenToast = ToastUtils.getGreenToast(
this@GenericActivity,
toastsArea,
message,
icon,
doNotTint
)
toastsArea.addView(greenToast.root)
greenToast.root.slideInToastFromTopForDuration(
toastsArea,
lifecycleScope,
duration
)
}
}
}
fun showBlueToast(
message: String,
@DrawableRes icon: Int,
duration: Long = 4000,
doNotTint: Boolean = false
) {
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val blueToast = ToastUtils.getBlueToast(
this@GenericActivity,
toastsArea,
message,
icon,
doNotTint
)
toastsArea.addView(blueToast.root)
blueToast.root.slideInToastFromTopForDuration(
toastsArea,
lifecycleScope,
duration
)
}
}
}
fun showRedToast(
message: String,
@DrawableRes icon: Int,
duration: Long = 4000,
doNotTint: Boolean = false
) {
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val redToast = ToastUtils.getRedToast(
this@GenericActivity,
toastsArea,
message,
icon,
doNotTint
)
toastsArea.addView(redToast.root)
redToast.root.slideInToastFromTopForDuration(
toastsArea,
lifecycleScope,
duration
)
}
}
}
fun showPersistentRedToast(
message: String,
@DrawableRes icon: Int,
tag: String,
doNotTint: Boolean = false
) {
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val redToast =
ToastUtils.getRedToast(
this@GenericActivity,
toastsArea,
message,
icon,
doNotTint
)
redToast.root.tag = tag
toastsArea.addView(redToast.root)
redToast.root.slideInToastFromTop(
toastsArea,
true
)
}
}
}
fun removePersistentRedToast(tag: String) {
lifecycleScope.launch {
withContext(Dispatchers.Main) {
for (child in toastsArea.children) {
if (child.tag == tag) {
toastsArea.removeView(child)
}
}
}
}
}
fun goToAndroidPermissionSettings() {
Log.i("$TAG Going into Android settings for our app")
try {
val intent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts(
"package",
packageName, null
)
)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
} catch (anfe: ActivityNotFoundException) {
Log.e("$TAG Failed to go to android settings: $anfe")
}
}
protected fun enableWindowSecureMode(enable: Boolean) {
val flags: Int = window.attributes.flags
if ((enable && flags and WindowManager.LayoutParams.FLAG_SECURE != 0) ||
(!enable && flags and WindowManager.LayoutParams.FLAG_SECURE == 0)
) {
Log.d(
"$TAG Secure flag is already ${if (enable) "enabled" else "disabled"}, skipping..."
)
return
}
if (enable) {
Log.i("$TAG Secure flag added to window")
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
Log.w("$TAG Secure flag cleared from window")
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
if (window.decorView.isAttachedToWindow) {
Log.d("$TAG Redrawing window decorView to apply flag")
try {
windowManager.updateViewLayout(window.decorView, window.attributes)
} catch (ise: IllegalStateException) {
Log.e("$TAG Failed to update window's decorView layout: $ise")
}
}
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2010-2023 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.ui
import androidx.annotation.UiThread
import androidx.fragment.app.Fragment
@UiThread
abstract class GenericFragment : Fragment() {
protected fun observeToastEvents(viewModel: GenericViewModel) {
viewModel.showRedToastEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val message = getString(pair.first)
val icon = pair.second
(requireActivity() as GenericActivity).showRedToast(message, icon)
}
}
viewModel.showFormattedRedToastEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val message = pair.first
val icon = pair.second
(requireActivity() as GenericActivity).showRedToast(message, icon)
}
}
viewModel.showGreenToastEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val message = getString(pair.first)
val icon = pair.second
(requireActivity() as GenericActivity).showGreenToast(message, icon)
}
}
viewModel.showFormattedGreenToastEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val message = pair.first
val icon = pair.second
(requireActivity() as GenericActivity).showGreenToast(message, icon)
}
}
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2010-2024 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.ui
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.utils.Event
open class GenericViewModel : ViewModel() {
// Message res id, icon
val showGreenToastEvent: MutableLiveData<Event<Pair<Int, Int>>> by lazy {
MutableLiveData<Event<Pair<Int, Int>>>()
}
val showFormattedGreenToastEvent: MutableLiveData<Event<Pair<String, Int>>> by lazy {
MutableLiveData<Event<Pair<String, Int>>>()
}
// Message res id, icon
val showRedToastEvent: MutableLiveData<Event<Pair<Int, Int>>> by lazy {
MutableLiveData<Event<Pair<Int, Int>>>()
}
val showFormattedRedToastEvent: MutableLiveData<Event<Pair<String, Int>>> by lazy {
MutableLiveData<Event<Pair<String, Int>>>()
}
fun showGreenToast(@StringRes message: Int, @DrawableRes icon: Int) {
showGreenToastEvent.postValue(Event(Pair(message, icon)))
}
fun showFormattedGreenToast(message: String, @DrawableRes icon: Int) {
showFormattedGreenToastEvent.postValue(Event(Pair(message, icon)))
}
fun showRedToast(@StringRes message: Int, @DrawableRes icon: Int) {
showRedToastEvent.postValue(Event(Pair(message, icon)))
}
fun showFormattedRedToast(message: String, @DrawableRes icon: Int) {
showFormattedRedToastEvent.postValue(Event(Pair(message, icon)))
}
}

View file

@ -1,83 +0,0 @@
/*
* Copyright (c) 2010-2023 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.ui
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.navigation.NavigationBarView
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: MainViewModel
private val onNavDestinationChangedListener =
NavController.OnDestinationChangedListener { _, destination, _ ->
binding.mainNavView?.visibility = View.VISIBLE
}
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, true)
super.onCreate(savedInstanceState)
while (!coreContext.isReady()) {
Thread.sleep(20)
}
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
binding.viewModel = viewModel
viewModel.unreadMessagesCount.observe(this) { count ->
if (count > 0) {
getNavBar()?.getOrCreateBadge(R.id.conversationsFragment)?.apply {
isVisible = true
number = count
}
} else {
getNavBar()?.removeBadge(R.id.conversationsFragment)
}
}
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
binding.mainNavHostFragment.findNavController()
.addOnDestinationChangedListener(onNavDestinationChangedListener)
getNavBar()?.setupWithNavController(binding.mainNavHostFragment.findNavController())
}
private fun getNavBar(): NavigationBarView? {
return binding.mainNavView ?: binding.mainNavRail
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2024 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.ui
import androidx.annotation.FontRes
import org.linphone.R
enum class NotoSansFont(@FontRes val fontRes: Int) {
// NotoSansLight(R.font.noto_sans_light), // 300
NotoSansRegular(R.font.noto_sans_regular), // 400
NotoSansMedium(R.font.noto_sans_medium), // 500
// NotoSansSemiBold(R.font.noto_sans_semi_bold), // 600
NotoSansBold(R.font.noto_sans_bold), // 700
NotoSansExtraBold(R.font.noto_sans_extra_bold) // 800
}

View file

@ -0,0 +1,123 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.activity.enableEdgeToEdge
import androidx.annotation.UiThread
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnPreDraw
import androidx.core.view.updatePadding
import androidx.databinding.DataBindingUtil
import androidx.navigation.findNavController
import kotlin.math.max
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantActivityBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.assistant.fragment.PermissionsFragmentDirections
@UiThread
class AssistantActivity : GenericActivity() {
companion object {
private const val TAG = "[Assistant Activity]"
const val SKIP_LANDING_EXTRA = "SkipLandingIfAtLeastAnAccount"
}
private lateinit var binding: AssistantActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.assistant_activity)
binding.lifecycleOwner = this
setUpToastsArea(binding.toastsArea)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val keyboard = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
v.updatePadding(
insets.left,
insets.top,
insets.right,
max(insets.bottom, keyboard.bottom)
)
WindowInsetsCompat.CONSUMED
}
coreContext.postOnCoreThread { core ->
if (core.accountList.isEmpty()) {
Log.i("$TAG No account configured, disabling back gesture")
coreContext.postOnMainThread {
// Disable back gesture / button
onBackPressedDispatcher.addCallback { }
}
}
}
(binding.root as? ViewGroup)?.doOnPreDraw {
if (!areAllPermissionsGranted()) {
Log.w("$TAG Not all required permissions are granted, showing Permissions fragment")
val action = PermissionsFragmentDirections.actionGlobalPermissionsFragment()
binding.assistantNavContainer.findNavController().navigate(action)
} else if (intent.getBooleanExtra(SKIP_LANDING_EXTRA, false)) {
Log.w(
"$TAG We were asked to leave assistant if at least an account is already configured"
)
coreContext.postOnCoreThread { core ->
if (core.accountList.isNotEmpty()) {
coreContext.postOnMainThread {
try {
Log.w("$TAG At least one account was found, leaving assistant")
finish()
} catch (ise: IllegalStateException) {
Log.e("$TAG Can't finish activity: $ise")
}
}
}
}
}
}
}
private fun areAllPermissionsGranted(): Boolean {
for (permission in Compatibility.getAllRequiredPermissionsArray()) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
Log.w("$TAG Permission [$permission] hasn't been granted yet!")
return false
}
}
val granted = Compatibility.hasFullScreenIntentPermission(this)
if (granted) {
Log.i("$TAG All permissions have been granted!")
}
return granted
}
}

View file

@ -0,0 +1,246 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.fragment
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.telephony.TelephonyManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantLandingFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.GenericFragment
import org.linphone.ui.assistant.model.AcceptConditionsAndPolicyDialogModel
import org.linphone.ui.assistant.viewmodel.AccountLoginViewModel
import org.linphone.utils.DialogUtils
import org.linphone.utils.PhoneNumberUtils
import androidx.core.net.toUri
@UiThread
class LandingFragment : GenericFragment() {
companion object {
private const val TAG = "[Landing Fragment]"
}
private lateinit var binding: AssistantLandingFragmentBinding
private val viewModel: AccountLoginViewModel by navGraphViewModels(
R.id.assistant_nav_graph
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantLandingFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
requireActivity().finish()
}
binding.setHelpClickListener {
if (findNavController().currentDestination?.id == R.id.landingFragment) {
val action =
LandingFragmentDirections.actionLandingFragmentToHelpFragment()
findNavController().navigate(action)
}
}
binding.setRegisterClickListener {
if (viewModel.conditionsAndPrivacyPolicyAccepted) {
goToRegisterFragment()
} else {
showAcceptConditionsAndPrivacyDialog(goToAccountCreate = true)
}
}
binding.setQrCodeClickListener {
if (findNavController().currentDestination?.id == R.id.landingFragment) {
val action =
LandingFragmentDirections.actionLandingFragmentToQrCodeScannerFragment()
findNavController().navigate(action)
}
}
binding.setThirdPartySipAccountLoginClickListener {
if (viewModel.conditionsAndPrivacyPolicyAccepted) {
goToLoginThirdPartySipAccountFragment(false)
} else {
showAcceptConditionsAndPrivacyDialog(goToThirdPartySipAccountLogin = true)
}
}
binding.setForgottenPasswordClickListener {
if (findNavController().currentDestination?.id == R.id.landingFragment) {
val action =
LandingFragmentDirections.actionLandingFragmentToRecoverAccountFragment()
findNavController().navigate(action)
}
}
viewModel.showPassword.observe(viewLifecycleOwner) {
lifecycleScope.launch {
delay(50)
binding.password.setSelection(binding.password.text?.length ?: 0)
}
}
viewModel.accountLoggedInEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Account successfully logged-in, leaving assistant")
requireActivity().finish()
}
}
viewModel.accountLoginErrorEvent.observe(viewLifecycleOwner) {
it.consume { message ->
(requireActivity() as GenericActivity).showRedToast(
message,
R.drawable.warning_circle
)
}
}
viewModel.skipLandingToThirdPartySipAccountEvent.observe(viewLifecycleOwner) {
it.consume {
goToLoginThirdPartySipAccountFragment(true)
}
}
val telephonyManager = requireContext().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val countryIso = telephonyManager.networkCountryIso
coreContext.postOnCoreThread {
val dialPlan = PhoneNumberUtils.getDeviceDialPlan(countryIso)
if (dialPlan != null) {
viewModel.internationalPrefix.postValue(dialPlan.countryCallingCode)
viewModel.internationalPrefixIsoCountryCode.postValue(dialPlan.isoCountryCode)
}
}
}
private fun goToRegisterFragment() {
if (findNavController().currentDestination?.id == R.id.landingFragment) {
val action = LandingFragmentDirections.actionLandingFragmentToRegisterFragment()
findNavController().navigate(action)
}
}
private fun goToLoginThirdPartySipAccountFragment(skipWarning: Boolean) {
if (findNavController().currentDestination?.id == R.id.landingFragment) {
val action = if (skipWarning) {
LandingFragmentDirections.actionLandingFragmentToThirdPartySipAccountLoginFragment()
} else {
LandingFragmentDirections.actionLandingFragmentToThirdPartySipAccountWarningFragment()
}
findNavController().navigate(action)
}
}
private fun showAcceptConditionsAndPrivacyDialog(
goToAccountCreate: Boolean = false,
goToThirdPartySipAccountLogin: Boolean = false
) {
val model = AcceptConditionsAndPolicyDialogModel()
val dialog = DialogUtils.getAcceptConditionsAndPrivacyDialog(
requireActivity(),
model
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
dialog.dismiss()
}
}
model.conditionsAcceptedEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Conditions & Privacy policy have been accepted")
coreContext.postOnCoreThread {
corePreferences.conditionsAndPrivacyPolicyAccepted = true
}
dialog.dismiss()
if (goToAccountCreate) {
goToRegisterFragment()
} else if (goToThirdPartySipAccountLogin) {
goToLoginThirdPartySipAccountFragment(false)
}
}
}
model.privacyPolicyClickedEvent.observe(viewLifecycleOwner) {
it.consume {
val url = getString(R.string.website_privacy_policy_url)
openUrlInBrowser(url)
}
}
model.generalTermsClickedEvent.observe(viewLifecycleOwner) {
it.consume {
val url = getString(R.string.website_terms_and_conditions_url)
openUrlInBrowser(url)
}
}
dialog.show()
}
private fun openUrlInBrowser(url: String) {
try {
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(browserIntent)
} catch (ise: IllegalStateException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise"
)
} catch (anfe: ActivityNotFoundException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe"
)
} catch (e: Exception) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url]: $e"
)
}
}
}

View file

@ -0,0 +1,201 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.fragment
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.UiThread
import androidx.core.content.ContextCompat
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantPermissionsFragmentBinding
import org.linphone.ui.GenericFragment
import org.linphone.ui.assistant.AssistantActivity
import org.linphone.ui.assistant.viewmodel.PermissionsViewModel
import kotlin.getValue
@UiThread
class PermissionsFragment : GenericFragment() {
companion object {
private const val TAG = "[Permissions Fragment]"
}
private lateinit var binding: AssistantPermissionsFragmentBinding
private val viewModel: PermissionsViewModel by navGraphViewModels(
R.id.assistant_nav_graph
)
private var leaving = false
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
var allGranted = true
permissions.entries.forEach {
val permissionName = it.key
val isGranted = it.value
if (isGranted) {
Log.i("Permission [$permissionName] is now granted")
} else {
Log.i("Permission [$permissionName] has been denied")
allGranted = false
}
}
if (!allGranted) {
Log.w(
"$TAG Not all permissions were granted, leaving anyway, they will be asked again later..."
)
}
leave()
}
private val telecomManagerPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Log.i("$TAG MANAGE_OWN_CALLS permission has been granted")
} else {
Log.w("$TAG MANAGE_OWN_CALLS permission has been denied, leaving this fragment")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantPermissionsFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
binding.setBackClickListener {
findNavController().popBackStack()
}
binding.setSkipClickListener {
Log.i("$TAG User clicked skip...")
leave()
}
binding.setGrantAllClickListener {
Log.i("$TAG Requesting all permissions")
requestPermissionLauncher.launch(
Compatibility.getAllRequiredPermissionsArray()
)
}
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.MANAGE_OWN_CALLS
) != PackageManager.PERMISSION_GRANTED
) {
Log.i("$TAG Request MANAGE_OWN_CALLS permission")
telecomManagerPermissionLauncher.launch(Manifest.permission.MANAGE_OWN_CALLS)
}
if (!Compatibility.hasFullScreenIntentPermission(requireContext())) {
Log.w(
"$TAG Android 14 or newer detected & full screen intent permission hasn't been granted!"
)
Compatibility.requestFullScreenIntentPermission(requireContext())
}
}
override fun onResume() {
super.onResume()
if (!leaving && areAllPermissionsGranted()) {
Log.i("$TAG All permissions have been granted, skipping")
leave()
}
}
private fun leave() {
if (leaving) return
leaving = true
if (requireActivity().intent.getBooleanExtra(AssistantActivity.SKIP_LANDING_EXTRA, false)) {
Log.w(
"$TAG We were asked to leave assistant if at least an account is already configured"
)
coreContext.postOnCoreThread { core ->
if (core.accountList.isNotEmpty()) {
coreContext.postOnMainThread {
Log.w("$TAG At least one account was found, leaving assistant")
try {
requireActivity().finish()
} catch (ise: IllegalStateException) {
Log.e("$TAG Failed to finish activity: $ise")
}
}
} else {
coreContext.postOnMainThread {
Log.w("$TAG No account was found, going to landing fragment")
try {
goToLoginFragment()
} catch (ise: IllegalStateException) {
Log.e("$TAG Failed to navigate to login fragment: $ise")
}
}
}
}
} else {
goToLoginFragment()
}
}
private fun goToLoginFragment() {
if (findNavController().currentDestination?.id == R.id.permissionsFragment) {
val action =
PermissionsFragmentDirections.actionPermissionsFragmentToLandingFragment()
findNavController().navigate(action)
}
}
private fun areAllPermissionsGranted(): Boolean {
for (permission in Compatibility.getAllRequiredPermissionsArray()) {
val granted = ContextCompat.checkSelfPermission(requireContext(), permission) == PackageManager.PERMISSION_GRANTED
viewModel.setPermissionGranted(permission, granted)
if (!granted) {
Log.w("$TAG Permission [$permission] hasn't been granted yet!")
return false
}
}
return Compatibility.hasFullScreenIntentPermission(requireContext())
}
}

View file

@ -0,0 +1,177 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.fragment
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.UiThread
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantQrCodeScannerFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.GenericFragment
import org.linphone.ui.assistant.viewmodel.QrCodeViewModel
import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections
@UiThread
class QrCodeScannerFragment : GenericFragment() {
companion object {
private const val TAG = "[Qr Code Scanner Fragment]"
}
private lateinit var binding: AssistantQrCodeScannerFragmentBinding
private val viewModel: QrCodeViewModel by navGraphViewModels(
R.id.assistant_nav_graph
)
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Log.i("$TAG CAMERA permission has been granted")
enableQrCodeVideoScanner()
} else {
Log.e("$TAG CAMERA permission has been denied, leaving this fragment")
goBack()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantQrCodeScannerFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
goBack()
}
viewModel.remoteProvisioningSuccessfulEvent.observe(viewLifecycleOwner) {
it.consume { atLeastOneAccountFound ->
if (atLeastOneAccountFound) {
requireActivity().finish()
} else {
goBack()
}
}
}
viewModel.onErrorEvent.observe(viewLifecycleOwner) {
it.consume {
// Core has restarted but something went wrong, restart video capture
enableQrCodeVideoScanner()
}
}
coreContext.bearerAuthenticationRequestedEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val serverUrl = pair.first
val username = pair.second
Log.i(
"$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]"
)
if (findNavController().currentDestination?.id == R.id.qrCodeScannerFragment) {
val action = SingleSignOnFragmentDirections.actionGlobalSingleSignOnFragment(
serverUrl,
username
)
findNavController().navigate(action)
}
}
}
if (!isCameraPermissionGranted()) {
if (ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), Manifest.permission.CAMERA)) {
Log.w("$TAG CAMERA permission wasn't granted yet, asking for it now")
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
} else {
Log.i("$TAG Permission request for CAMERA will be automatically denied, go to android app settings instead")
(requireActivity() as GenericActivity).goToAndroidPermissionSettings()
}
}
}
override fun onResume() {
super.onResume()
if (isCameraPermissionGranted()) {
Log.i(
"$TAG Record video permission is granted, starting video preview with back cam if possible"
)
viewModel.setBackCamera()
enableQrCodeVideoScanner()
}
}
override fun onPause() {
coreContext.postOnCoreThread { core ->
core.nativePreviewWindowId = null
core.isVideoPreviewEnabled = false
core.isQrcodeVideoPreviewEnabled = false
coreContext.setFrontCamera()
}
super.onPause()
}
private fun goBack() {
findNavController().popBackStack()
}
private fun isCameraPermissionGranted(): Boolean {
val granted = ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
Log.i("$TAG CAMERA permission is ${if (granted) "granted" else "denied"}")
return granted
}
private fun enableQrCodeVideoScanner() {
coreContext.postOnCoreThread { core ->
core.nativePreviewWindowId = binding.qrCodePreview
core.isQrcodeVideoPreviewEnabled = true
core.isVideoPreviewEnabled = true
}
}
}

View file

@ -0,0 +1,134 @@
/*
* Copyright (c) 2010-2025 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.ui.assistant.fragment
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.core.net.toUri
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.ui.GenericFragment
import org.linphone.databinding.AssistantRecoverAccountFragmentBinding
import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel
import kotlin.getValue
@UiThread
class RecoverAccountFragment : GenericFragment() {
companion object {
private const val TAG = "[Recover Account Fragment]"
}
private lateinit var binding: AssistantRecoverAccountFragmentBinding
private val viewModel: AccountCreationViewModel by navGraphViewModels(
R.id.assistant_nav_graph
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantRecoverAccountFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
viewModel.accountRecoveryTokenReceivedEvent.observe(viewLifecycleOwner) {
it.consume { token ->
Log.i("$TAG Account recovery token received [$token], opening browser")
recoverPhoneNumberAccount(token)
}
}
binding.setBackClickListener {
goBack()
}
binding.setRecoverEmailAccountClickListener {
recoverEmailAccount()
}
binding.setRecoverPhoneNumberAccountClickListener {
viewModel.requestAccountRecoveryToken()
}
}
private fun goBack() {
findNavController().popBackStack()
}
private fun recoverEmailAccount() {
val rootUrl = getString(R.string.web_platform_forgotten_password_url)
val url = "$rootUrl/recovery/email"
try {
Log.i("$TAG Trying to open [$url] URL")
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(browserIntent)
} catch (ise: IllegalStateException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise"
)
} catch (anfe: ActivityNotFoundException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe"
)
} catch (e: Exception) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url]: $e"
)
}
}
private fun recoverPhoneNumberAccount(recoveryToken: String) {
val rootUrl = getString(R.string.web_platform_forgotten_password_url)
val url = "$rootUrl/recovery/phone/$recoveryToken"
try {
Log.i("$TAG Trying to open [$url] URL")
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(browserIntent)
} catch (ise: IllegalStateException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise"
)
} catch (anfe: ActivityNotFoundException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe"
)
} catch (e: Exception) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url]: $e"
)
}
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.fragment
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantRegisterConfirmSmsCodeFragmentBinding
import org.linphone.ui.GenericFragment
import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel
@UiThread
class RegisterCodeConfirmationFragment : GenericFragment() {
companion object {
private const val TAG = "[Register Code Confirmation Fragment]"
}
private lateinit var binding: AssistantRegisterConfirmSmsCodeFragmentBinding
private val viewModel: AccountCreationViewModel by navGraphViewModels(
R.id.assistant_nav_graph
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantRegisterConfirmSmsCodeFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
goBack()
}
viewModel.accountCreatedEvent.observe(viewLifecycleOwner) {
it.consume {
val identity = viewModel.username.value.orEmpty()
Log.i("$TAG Account [$identity] has been created, leaving assistant")
requireActivity().finish()
}
}
// 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(Context.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(
"$TAG Found 4 digits [$clip] as primary clip in clipboard, using it and clear it"
)
viewModel.smsCodeFirstDigit.value = clip[0].toString()
viewModel.smsCodeSecondDigit.value = clip[1].toString()
viewModel.smsCodeThirdDigit.value = clip[2].toString()
viewModel.smsCodeLastDigit.value = clip[3].toString()
clipboard.clearPrimaryClip()
}
}
}
}
private fun goBack() {
findNavController().popBackStack()
}
}

View file

@ -0,0 +1,227 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.fragment
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.telephony.TelephonyManager
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.annotation.UiThread
import androidx.appcompat.widget.AppCompatTextView
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantRegisterFragmentBinding
import org.linphone.ui.GenericFragment
import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel
import org.linphone.utils.ConfirmationDialogModel
import org.linphone.utils.AppUtils
import org.linphone.utils.DialogUtils
import org.linphone.utils.PhoneNumberUtils
import androidx.core.net.toUri
@UiThread
class RegisterFragment : GenericFragment() {
companion object {
private const val TAG = "[Register Fragment]"
}
private lateinit var binding: AssistantRegisterFragmentBinding
private val viewModel: AccountCreationViewModel by navGraphViewModels(
R.id.assistant_nav_graph
)
private val dropdownListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val dialPlan = viewModel.dialPlansList[position]
Log.i(
"$TAG Selected dialplan updated [+${dialPlan.countryCallingCode}] / [${dialPlan.country}]"
)
viewModel.selectedDialPlan.value = dialPlan
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantRegisterFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
goBack()
}
binding.setLoginClickListener {
goBack()
}
binding.setOpenSubscribeWebPageClickListener {
val url = getString(R.string.web_platform_register_email_url)
try {
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(browserIntent)
} catch (ise: IllegalStateException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise"
)
} catch (anfe: ActivityNotFoundException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe"
)
} catch (e: Exception) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url]: $e"
)
}
}
binding.username.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
viewModel.usernameError.value = ""
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
binding.phoneNumber.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
viewModel.phoneNumberError.value = ""
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
viewModel.normalizedPhoneNumberEvent.observe(viewLifecycleOwner) {
it.consume { number ->
showPhoneNumberConfirmationDialog(number)
}
}
viewModel.showPassword.observe(viewLifecycleOwner) {
lifecycleScope.launch {
delay(50)
binding.password.setSelection(binding.password.text?.length ?: 0)
}
}
viewModel.goToSmsCodeConfirmationViewEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Going to SMS code confirmation fragment")
if (findNavController().currentDestination?.id == R.id.registerFragment) {
val action =
RegisterFragmentDirections.actionRegisterFragmentToRegisterCodeConfirmationFragment()
findNavController().navigate(action)
}
}
}
val telephonyManager = requireContext().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val countryIso = telephonyManager.networkCountryIso
coreContext.postOnCoreThread {
val fragmentContext = context ?: return@postOnCoreThread
val adapter = object : ArrayAdapter<String>(
fragmentContext,
R.layout.drop_down_item,
viewModel.dialPlansLabelList
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: super.getView(position, null, parent)
val label = viewModel.dialPlansShortLabelList[position]
(view as? AppCompatTextView)?.text = label
return view
}
}
adapter.setDropDownViewResource(R.layout.assistant_country_picker_dropdown_cell)
val dialPlan = PhoneNumberUtils.getDeviceDialPlan(countryIso)
var default = 0
if (dialPlan != null) {
viewModel.selectedDialPlan.postValue(dialPlan)
default = viewModel.dialPlansList.indexOf(dialPlan)
}
coreContext.postOnMainThread {
binding.prefix.adapter = adapter
binding.prefix.setSelection(default)
binding.prefix.onItemSelectedListener = dropdownListener
}
}
}
private fun goBack() {
findNavController().popBackStack()
}
private fun showPhoneNumberConfirmationDialog(number: String) {
val label = AppUtils.getFormattedString(R.string.assistant_dialog_confirm_phone_number_message, number)
val model = ConfirmationDialogModel(label)
val dialog = DialogUtils.getAccountCreationPhoneNumberConfirmationDialog(
requireActivity(),
model
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
viewModel.startAccountCreation()
dialog.dismiss()
}
}
dialog.show()
}
}

View file

@ -0,0 +1,172 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.fragment
import android.content.Context
import android.os.Bundle
import android.telephony.TelephonyManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.annotation.UiThread
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantThirdPartySipAccountLoginFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.GenericFragment
import org.linphone.ui.assistant.viewmodel.ThirdPartySipAccountLoginViewModel
import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections
import org.linphone.utils.DialogUtils
import org.linphone.utils.PhoneNumberUtils
@UiThread
class ThirdPartySipAccountLoginFragment : GenericFragment() {
companion object {
private const val TAG = "[Third Party SIP Account Login Fragment]"
}
private lateinit var binding: AssistantThirdPartySipAccountLoginFragmentBinding
private val viewModel: ThirdPartySipAccountLoginViewModel by navGraphViewModels(
R.id.assistant_nav_graph
)
private val dropdownListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val transport = viewModel.availableTransports[position]
Log.i("$TAG Selected transport updated [$transport]")
viewModel.transport.value = transport
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
private lateinit var adapter: ArrayAdapter<String>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantThirdPartySipAccountLoginFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
adapter = ArrayAdapter(
requireContext(),
R.layout.drop_down_item,
viewModel.availableTransports
)
adapter.setDropDownViewResource(R.layout.generic_dropdown_cell)
binding.transport.adapter = adapter
binding.transport.onItemSelectedListener = dropdownListener
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
goBack()
}
binding.setOutboundProxyTooltipClickListener {
showOutboundProxyInfoDialog()
}
viewModel.showPassword.observe(viewLifecycleOwner) {
lifecycleScope.launch {
delay(50)
binding.password.setSelection(binding.password.text?.length ?: 0)
}
}
viewModel.accountLoggedInEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Account successfully logged-in, leaving assistant")
requireActivity().finish()
}
}
viewModel.accountLoginErrorEvent.observe(viewLifecycleOwner) {
it.consume { message ->
(requireActivity() as GenericActivity).showRedToast(
message,
R.drawable.warning_circle
)
}
}
viewModel.defaultTransportIndexEvent.observe(viewLifecycleOwner) {
it.consume { index ->
binding.transport.setSelection(index)
}
}
coreContext.bearerAuthenticationRequestedEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val serverUrl = pair.first
val username = pair.second
Log.i(
"$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]"
)
if (findNavController().currentDestination?.id == R.id.thirdPartySipAccountLoginFragment) {
val action = SingleSignOnFragmentDirections.actionGlobalSingleSignOnFragment(
serverUrl,
username
)
findNavController().navigate(action)
}
}
}
val telephonyManager = requireContext().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val countryIso = telephonyManager.networkCountryIso
coreContext.postOnCoreThread {
val dialPlan = PhoneNumberUtils.getDeviceDialPlan(countryIso)
if (dialPlan != null) {
viewModel.internationalPrefix.postValue(dialPlan.countryCallingCode)
viewModel.internationalPrefixIsoCountryCode.postValue(dialPlan.isoCountryCode)
}
}
}
private fun goBack() {
findNavController().popBackStack()
}
private fun showOutboundProxyInfoDialog() {
val dialog = DialogUtils.getAccountOutboundProxyHelpDialog(requireActivity())
dialog.show()
}
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.fragment
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.navigation.fragment.findNavController
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantThirdPartySipAccountWarningFragmentBinding
import org.linphone.ui.GenericFragment
import androidx.core.net.toUri
@UiThread
class ThirdPartySipAccountWarningFragment : GenericFragment() {
companion object {
private const val TAG = "[Third Party SIP Account Warning Fragment]"
}
private lateinit var binding: AssistantThirdPartySipAccountWarningFragmentBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantThirdPartySipAccountWarningFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.setBackClickListener {
goBack()
}
binding.setContactClickListener {
val url = getString(R.string.website_contact_url)
try {
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(browserIntent)
} catch (ise: IllegalStateException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise"
)
} catch (anfe: ActivityNotFoundException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe"
)
} catch (e: Exception) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url]: $e"
)
}
}
binding.setCreateAccountClickListener {
if (findNavController().currentDestination?.id == R.id.thirdPartySipAccountWarningFragment) {
val action =
ThirdPartySipAccountWarningFragmentDirections.actionThirdPartySipAccountWarningFragmentToRegisterFragment()
findNavController().navigate(action)
}
}
binding.setLoginClickListener {
if (findNavController().currentDestination?.id == R.id.thirdPartySipAccountWarningFragment) {
val action =
ThirdPartySipAccountWarningFragmentDirections.actionThirdPartySipAccountWarningFragmentToThirdPartySipAccountLoginFragment()
findNavController().navigate(action)
}
}
}
private fun goBack() {
findNavController().popBackStack()
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.model
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ClickableSpan
import android.view.View
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import java.util.regex.Pattern
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
class AcceptConditionsAndPolicyDialogModel
@UiThread
constructor() {
companion object {
private const val TAG = "[Accept Terms & Policy Dialog Model]"
}
val message = MutableLiveData<SpannableString>()
val dismissEvent = MutableLiveData<Event<Boolean>>()
val conditionsAcceptedEvent = MutableLiveData<Event<Boolean>>()
val generalTermsClickedEvent = MutableLiveData<Event<Boolean>>()
val privacyPolicyClickedEvent = MutableLiveData<Event<Boolean>>()
init {
val generalTerms = AppUtils.getString(R.string.assistant_dialog_general_terms_label)
val privacyPolicy = AppUtils.getString(R.string.assistant_dialog_privacy_policy_label)
val label = coreContext.context.getString(
R.string.assistant_dialog_general_terms_and_privacy_policy_message,
generalTerms,
privacyPolicy
)
val spannable = SpannableString(label)
val termsMatcher = Pattern.compile(generalTerms).matcher(label)
if (termsMatcher.find()) {
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
Log.i("$TAG Clicked on general terms link")
generalTermsClickedEvent.value = Event(true)
}
}
spannable.setSpan(
clickableSpan,
termsMatcher.start(0),
termsMatcher.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
val policyMatcher = Pattern.compile(privacyPolicy).matcher(label)
if (policyMatcher.find()) {
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
Log.i("$TAG Clicked on privacy policy link")
privacyPolicyClickedEvent.value = Event(true)
}
}
spannable.setSpan(
clickableSpan,
policyMatcher.start(0),
policyMatcher.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
message.value = spannable
}
@UiThread
fun dismiss() {
dismissEvent.value = Event(true)
}
@UiThread
fun acceptConditions() {
conditionsAcceptedEvent.value = Event(true)
}
}

View file

@ -0,0 +1,622 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.Account
import org.linphone.core.AccountManagerServices
import org.linphone.core.AccountManagerServicesRequest
import org.linphone.core.AccountManagerServicesRequestListenerStub
import org.linphone.core.AuthInfo
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.DialPlan
import org.linphone.core.Dictionary
import org.linphone.core.Factory
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
class AccountCreationViewModel
@UiThread
constructor() : GenericViewModel() {
companion object {
private const val TAG = "[Account Creation ViewModel]"
private const val TIME_TO_WAIT_FOR_PUSH_NOTIFICATION_WITH_ACCOUNT_CREATION_TOKEN = 5000
private const val HASH_ALGORITHM = "SHA-256"
}
val username = MutableLiveData<String>()
val usernameError = MutableLiveData<String>()
val password = MutableLiveData<String>()
val passwordError = MutableLiveData<String>()
val phoneNumber = MutableLiveData<String>()
val phoneNumberError = MutableLiveData<String>()
val dialPlansLabelList = arrayListOf<String>()
val dialPlansShortLabelList = arrayListOf<String>()
val dialPlansList = arrayListOf<DialPlan>()
val selectedDialPlan = MutableLiveData<DialPlan>()
val showPassword = MutableLiveData<Boolean>()
val lockUsernameAndPassword = MutableLiveData<Boolean>()
val createEnabled = MediatorLiveData<Boolean>()
val pushNotificationsAvailable = MutableLiveData<Boolean>()
val confirmationMessage = MutableLiveData<String>()
val smsCodeFirstDigit = MutableLiveData<String>()
val smsCodeSecondDigit = MutableLiveData<String>()
val smsCodeThirdDigit = MutableLiveData<String>()
val smsCodeLastDigit = MutableLiveData<String>()
val operationInProgress = MutableLiveData<Boolean>()
private var normalizedPhoneNumber: String? = null
val normalizedPhoneNumberEvent = MutableLiveData<Event<String>>()
val goToSmsCodeConfirmationViewEvent = MutableLiveData<Event<Boolean>>()
val accountCreatedEvent = MutableLiveData<Event<Boolean>>()
val accountRecoveryTokenReceivedEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
private var waitingForFlexiApiPushToken = false
private var waitForPushJob: Job? = null
private lateinit var accountManagerServices: AccountManagerServices
private var requestedTokenIsForAccountCreation: Boolean = true
private var accountCreationToken: String? = null
private var accountRecoveryToken: String? = null
private var accountCreatedAuthInfo: AuthInfo? = null
private var accountCreated: Account? = null
private val accountManagerServicesListener = object : AccountManagerServicesRequestListenerStub() {
@WorkerThread
override fun onRequestSuccessful(
request: AccountManagerServicesRequest,
data: String?
) {
Log.i("$TAG Request [${request.type}] was successful, data is [$data]")
operationInProgress.postValue(false)
when (request.type) {
AccountManagerServicesRequest.Type.CreateAccountUsingToken -> {
if (!data.isNullOrEmpty()) {
storeAccountInCore(data)
sendCodeBySms()
} else {
Log.e(
"$TAG No data found for createAccountUsingToken request, can't continue!"
)
}
}
AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush,
AccountManagerServicesRequest.Type.SendAccountRecoveryTokenByPush -> {
Log.i("$TAG Send token by push notification request has been accepted, it should be received soon")
}
AccountManagerServicesRequest.Type.SendPhoneNumberLinkingCodeBySms -> {
goToSmsCodeConfirmationViewEvent.postValue(Event(true))
}
AccountManagerServicesRequest.Type.LinkPhoneNumberUsingCode -> {
enableAccountAndSetItAsDefault()
}
else -> { }
}
}
@WorkerThread
override fun onRequestError(
request: AccountManagerServicesRequest,
statusCode: Int,
errorMessage: String?,
parameterErrors: Dictionary?
) {
Log.e(
"$TAG Request [${request.type}] returned an error with status code [$statusCode] and message [$errorMessage]"
)
operationInProgress.postValue(false)
if (!errorMessage.isNullOrEmpty()) {
showFormattedRedToast(errorMessage, R.drawable.warning_circle)
}
for (parameter in parameterErrors?.keys.orEmpty()) {
val parameterErrorMessage = parameterErrors?.getString(parameter) ?: ""
when (parameter) {
"username" -> usernameError.postValue(parameterErrorMessage)
"password" -> passwordError.postValue(parameterErrorMessage)
"phone" -> phoneNumberError.postValue(parameterErrorMessage)
}
}
when (request.type) {
AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush,
AccountManagerServicesRequest.Type.SendAccountRecoveryTokenByPush -> {
Log.w("$TAG Cancelling job waiting for push notification")
waitingForFlexiApiPushToken = false
waitForPushJob?.cancel()
}
AccountManagerServicesRequest.Type.SendPhoneNumberLinkingCodeBySms -> {
val authInfo = accountCreatedAuthInfo
if (authInfo != null) {
coreContext.core.removeAuthInfo(authInfo)
}
val account = accountCreated
if (account != null) {
coreContext.core.removeAccount(account)
}
}
else -> {
}
}
createEnabled.postValue(true)
}
}
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onPushNotificationReceived(core: Core, payload: String?) {
Log.i("$TAG Push received: [$payload]")
val data = payload.orEmpty()
if (data.isNotEmpty()) {
try {
// This is because JSONObject.toString() done by the SDK will result in payload looking like {"custom-payload":"{\"token\":\"value\"}"}
val cleanPayload = data
.replace("\\\"", "\"")
.replace("\"{", "{")
.replace("}\"", "}")
Log.i("$TAG Cleaned payload is: [$cleanPayload]")
val json = JSONObject(cleanPayload)
val customPayload = json.getJSONObject("custom-payload")
if (customPayload.has("token")) {
waitForPushJob?.cancel()
waitingForFlexiApiPushToken = false
operationInProgress.postValue(false)
val token = customPayload.getString("token")
if (token.isNotEmpty()) {
if (requestedTokenIsForAccountCreation) {
accountCreationToken = token
Log.i(
"$TAG Extracted token [$accountCreationToken] from push payload, creating account"
)
createAccount()
} else {
accountRecoveryToken = token
Log.i(
"$TAG Extracted token [$accountRecoveryToken] from push payload, opening browser"
)
accountRecoveryTokenReceivedEvent.postValue(Event(token))
}
} else {
Log.e("$TAG Push payload JSON object has an empty 'token'!")
onFlexiApiTokenRequestError()
}
} else {
Log.e("$TAG Push payload JSON object has no 'token' key!")
onFlexiApiTokenRequestError()
}
} catch (e: JSONException) {
Log.e("$TAG Exception trying to parse push payload as JSON: [$e]")
onFlexiApiTokenRequestError()
}
} else {
Log.e("$TAG Push payload is null or empty, can't extract auth token!")
onFlexiApiTokenRequestError()
}
}
}
init {
operationInProgress.value = false
lockUsernameAndPassword.value = false
coreContext.postOnCoreThread { core ->
pushNotificationsAvailable.postValue(LinphoneUtils.arePushNotificationsAvailable(core))
val dialPlans = Factory.instance().dialPlans.toList()
for (dialPlan in dialPlans) {
dialPlansList.add(dialPlan)
dialPlansLabelList.add(
"${dialPlan.flag} ${dialPlan.country} | +${dialPlan.countryCallingCode}"
)
dialPlansShortLabelList.add(
"${dialPlan.flag} +${dialPlan.countryCallingCode}"
)
}
accountManagerServices = core.createAccountManagerServices()
accountManagerServices.language = Locale.getDefault().language // Returns en, fr, etc...
core.addListener(coreListener)
}
showPassword.value = false
createEnabled.addSource(username) {
createEnabled.value = isCreateButtonEnabled()
}
createEnabled.addSource(password) {
createEnabled.value = isCreateButtonEnabled()
}
createEnabled.addSource(selectedDialPlan) {
createEnabled.value = isCreateButtonEnabled()
}
createEnabled.addSource(phoneNumber) {
createEnabled.value = isCreateButtonEnabled()
}
}
@UiThread
override fun onCleared() {
coreContext.postOnCoreThread { core ->
core.removeListener(coreListener)
}
waitForPushJob?.cancel()
super.onCleared()
}
@UiThread
fun askUserToConfirmPhoneNumber() {
coreContext.postOnCoreThread {
if (::accountManagerServices.isInitialized) {
val dialPlan = selectedDialPlan.value
if (dialPlan == null) {
Log.e("$TAG No dial plan (country) selected!")
return@postOnCoreThread
}
val number = phoneNumber.value.orEmpty().trim()
val formattedPhoneNumber = dialPlan.formatPhoneNumber(number, false)
Log.i(
"$TAG Formatted phone number [$number] using dial plan [${dialPlan.country}] is [$formattedPhoneNumber]"
)
val message = coreContext.context.getString(
R.string.assistant_account_creation_sms_confirmation_explanation,
formattedPhoneNumber
)
normalizedPhoneNumber = formattedPhoneNumber
confirmationMessage.postValue(message)
normalizedPhoneNumberEvent.postValue(Event(formattedPhoneNumber))
} else {
Log.e("$TAG Account manager services hasn't been initialized!")
showRedToast(R.string.assistant_account_register_unexpected_error, R.drawable.warning_circle)
}
}
}
@UiThread
fun startAccountCreation() {
operationInProgress.value = true
coreContext.postOnCoreThread {
if (accountCreationToken.isNullOrEmpty()) {
Log.i("$TAG We don't have an account creation token yet, let's request one")
requestFlexiApiToken(requestAccountCreationToken = true)
} else {
val authInfo = accountCreatedAuthInfo
if (authInfo != null) {
Log.i("$TAG Account has already been created, requesting SMS to be sent")
sendCodeBySms()
} else {
Log.i("$TAG We've already have a token [$accountCreationToken], continuing")
createAccount()
}
}
}
}
@UiThread
fun requestAccountRecoveryToken() {
coreContext.postOnCoreThread {
val existingToken = accountRecoveryToken
if (existingToken.isNullOrEmpty()) {
Log.i("$TAG We don't have an account recovery token yet, let's request one")
requestFlexiApiToken(requestAccountCreationToken = false)
} else {
Log.i("$TAG We've already have a token [$existingToken], using it")
accountRecoveryTokenReceivedEvent.postValue(Event(existingToken))
}
}
}
@UiThread
fun toggleShowPassword() {
showPassword.value = showPassword.value == false
}
@UiThread
private fun isCreateButtonEnabled(): Boolean {
return username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() && phoneNumber.value.orEmpty().isNotEmpty() && selectedDialPlan.value?.countryCallingCode.orEmpty().isNotEmpty()
}
@UiThread
fun validateCode() {
usernameError.postValue("")
passwordError.postValue("")
phoneNumberError.postValue("")
operationInProgress.value = true
val account = accountCreated
if (::accountManagerServices.isInitialized && account != null) {
val code =
"${smsCodeFirstDigit.value.orEmpty().trim()}${smsCodeSecondDigit.value.orEmpty().trim()}${smsCodeThirdDigit.value.orEmpty().trim()}${smsCodeLastDigit.value.orEmpty().trim()}"
val identity = account.params.identityAddress
if (identity != null) {
Log.i(
"$TAG Activating account using code [$code] for account [${identity.asStringUriOnly()}]"
)
val request = accountManagerServices.createLinkPhoneNumberToAccountUsingCodeRequest(
identity,
code
)
request.addListener(accountManagerServicesListener)
request.submit()
// Reset code
smsCodeFirstDigit.postValue("")
smsCodeSecondDigit.postValue("")
smsCodeThirdDigit.postValue("")
smsCodeLastDigit.postValue("")
}
}
}
@WorkerThread
private fun sendCodeBySms() {
usernameError.postValue("")
passwordError.postValue("")
phoneNumberError.postValue("")
val account = accountCreated
if (::accountManagerServices.isInitialized && account != null) {
val phoneNumberValue = normalizedPhoneNumber
if (phoneNumberValue.isNullOrEmpty()) {
Log.e("$TAG Phone number is null or empty, this shouldn't happen at this step!")
return
}
operationInProgress.postValue(true)
createEnabled.postValue(false)
val identity = account.params.identityAddress
if (identity != null) {
Log.i(
"$TAG Account [${identity.asStringUriOnly()}] should now be created, asking account manager to send a confirmation code by SMS to [$phoneNumberValue]"
)
val request = accountManagerServices.createSendPhoneNumberLinkingCodeBySmsRequest(
identity,
phoneNumberValue
)
request.addListener(accountManagerServicesListener)
request.submit()
}
}
}
@WorkerThread
private fun createAccount() {
usernameError.postValue("")
passwordError.postValue("")
phoneNumberError.postValue("")
if (::accountManagerServices.isInitialized) {
val token = accountCreationToken
if (token.isNullOrEmpty()) {
Log.e("$TAG No account creation token, can't create account!")
return
}
operationInProgress.postValue(true)
createEnabled.postValue(false)
val usernameValue = username.value.orEmpty().trim()
val passwordValue = password.value.orEmpty().trim()
if (usernameValue.isEmpty() || passwordValue.isEmpty()) {
Log.e("$TAG Either username [$usernameValue] or password is null or empty!")
return
}
Log.i(
"$TAG Account creation token is [$token], creating account with username [$usernameValue] and algorithm SHA-256"
)
val request = accountManagerServices.createNewAccountUsingTokenRequest(
usernameValue,
passwordValue,
HASH_ALGORITHM,
token
)
request.addListener(accountManagerServicesListener)
request.submit()
}
}
@WorkerThread
private fun storeAccountInCore(identity: String) {
val core = coreContext.core
core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
val sipIdentity = Factory.instance().createAddress(identity)
if (sipIdentity == null) {
Log.e("$TAG Failed to create address from SIP Identity [$identity]!")
return
}
val passwordValue = password.value
// We need to have an AuthInfo for newly created account to authorize phone number linking request
val authInfo = Factory.instance().createAuthInfo(
sipIdentity.username.orEmpty(),
null,
passwordValue,
null,
null,
sipIdentity.domain
)
core.addAuthInfo(authInfo)
Log.i("$TAG Auth info for SIP identity [${sipIdentity.asStringUriOnly()}] created & added")
val dialPlan = selectedDialPlan.value
val accountParams = core.createAccountParams()
accountParams.identityAddress = sipIdentity
if (dialPlan != null) {
Log.i(
"$TAG Setting international prefix [${dialPlan.internationalCallPrefix}] and country [${dialPlan.isoCountryCode}] to account params"
)
accountParams.internationalPrefix = dialPlan.internationalCallPrefix
accountParams.internationalPrefixIsoCountryCode = dialPlan.isoCountryCode
// Do not enable account just yet, wait for it to be activated using SMS code
accountParams.isRegisterEnabled = false
}
val account = core.createAccount(accountParams)
core.addAccount(account)
Log.i("$TAG Account for SIP identity [${sipIdentity.asStringUriOnly()}] created & added")
accountCreatedAuthInfo = authInfo
accountCreated = account
lockUsernameAndPassword.postValue(true)
}
@WorkerThread
private fun enableAccountAndSetItAsDefault() {
val account = accountCreated ?: return
Log.i(
"$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] has been created & activated, enable it & setting it as default"
)
val newParams = account.params.clone()
newParams.isRegisterEnabled = true
account.params = newParams
coreContext.core.defaultAccount = account
accountCreatedEvent.postValue(Event(true))
}
@WorkerThread
private fun requestFlexiApiToken(requestAccountCreationToken: Boolean) {
requestedTokenIsForAccountCreation = requestAccountCreationToken
if (!coreContext.core.isPushNotificationAvailable) {
Log.e(
"$TAG Core says push notification aren't available, can't request a token from FlexiAPI"
)
onFlexiApiTokenRequestError()
return
}
operationInProgress.postValue(true)
createEnabled.postValue(false)
val pushConfig = coreContext.core.pushNotificationConfig
if (pushConfig != null) {
val provider = pushConfig.provider
val param = pushConfig.param
val prid = pushConfig.prid
if (provider.isNullOrEmpty() || param.isNullOrEmpty() || prid.isNullOrEmpty()) {
Log.e(
"$TAG At least one mandatory push information (provider [$provider], param [$param], prid [$prid]) is missing!"
)
onFlexiApiTokenRequestError()
return
}
// Request an auth token, will be sent by push
val request = if (requestAccountCreationToken) {
Log.i("$TAG Requesting account creation token")
accountManagerServices.createSendAccountCreationTokenByPushRequest(
provider,
param,
prid
)
} else {
Log.i("$TAG Requesting account recovery token")
accountManagerServices.createSendAccountRecoveryTokenByPushRequest(
provider,
param,
prid
)
}
request.addListener(accountManagerServicesListener)
request.submit()
val waitFor = TIME_TO_WAIT_FOR_PUSH_NOTIFICATION_WITH_ACCOUNT_CREATION_TOKEN
waitingForFlexiApiPushToken = true
waitForPushJob?.cancel()
Log.i("$TAG Waiting push with auth token for $waitFor ms")
waitForPushJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
delay(waitFor.toLong())
}
withContext(Dispatchers.Main) {
if (waitingForFlexiApiPushToken) {
waitingForFlexiApiPushToken = false
Log.e("$TAG Auth token wasn't received by push in [$waitFor] ms")
onFlexiApiTokenRequestError()
}
}
}
} else {
Log.e("$TAG No push configuration object in Core, shouldn't happen!")
onFlexiApiTokenRequestError()
}
}
@WorkerThread
private fun onFlexiApiTokenRequestError() {
Log.e("$TAG Flexi API token request by push error!")
operationInProgress.postValue(false)
showRedToast(R.string.assistant_account_register_push_notification_not_received_error, R.drawable.warning_circle)
}
}

View file

@ -0,0 +1,255 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.Account
import org.linphone.core.AuthInfo
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.Factory
import org.linphone.core.Reason
import org.linphone.core.RegistrationState
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
open class AccountLoginViewModel
@UiThread
constructor() : GenericViewModel() {
companion object {
private const val TAG = "[Account Login ViewModel]"
}
val showBackButton = MutableLiveData<Boolean>()
val hideCreateAccount = MutableLiveData<Boolean>()
val hideScanQrCode = MutableLiveData<Boolean>()
val hideThirdPartyAccount = MutableLiveData<Boolean>()
val sipIdentity = MutableLiveData<String>()
val password = MutableLiveData<String>()
val internationalPrefix = MutableLiveData<String>()
val internationalPrefixIsoCountryCode = MutableLiveData<String>()
val showPassword = MutableLiveData<Boolean>()
val loginEnabled = MediatorLiveData<Boolean>()
val registrationInProgress = MutableLiveData<Boolean>()
val accountLoggedInEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val accountLoginErrorEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val skipLandingToThirdPartySipAccountEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
var conditionsAndPrivacyPolicyAccepted = false
private lateinit var newlyCreatedAuthInfo: AuthInfo
private lateinit var newlyCreatedAccount: Account
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onAccountRegistrationStateChanged(
core: Core,
account: Account,
state: RegistrationState?,
message: String
) {
if (account == newlyCreatedAccount) {
Log.i("$TAG Newly created account registration state is [$state] ($message)")
if (state == RegistrationState.Ok) {
registrationInProgress.postValue(false)
core.removeListener(this)
// Set new account as default
core.defaultAccount = newlyCreatedAccount
accountLoggedInEvent.postValue(Event(core.accountList.size == 1))
} else if (state == RegistrationState.Failed) {
registrationInProgress.postValue(false)
core.removeListener(this)
val error = when (account.error) {
Reason.Forbidden -> {
AppUtils.getString(R.string.assistant_account_login_forbidden_error)
}
else -> {
AppUtils.getFormattedString(
R.string.assistant_account_login_error,
account.error.toString()
)
}
}
accountLoginErrorEvent.postValue(Event(error))
Log.e("$TAG Account failed to REGISTER [$message], removing it")
core.removeAuthInfo(newlyCreatedAuthInfo)
core.removeAccount(newlyCreatedAccount)
}
}
}
}
init {
coreContext.postOnCoreThread { core ->
// Prevent user from leaving assistant if no account was configured yet
showBackButton.postValue(core.accountList.isNotEmpty())
hideCreateAccount.postValue(corePreferences.hideAssistantCreateAccount)
hideScanQrCode.postValue(corePreferences.hideAssistantScanQrCode)
hideThirdPartyAccount.postValue(corePreferences.hideAssistantThirdPartySipAccount)
conditionsAndPrivacyPolicyAccepted = corePreferences.conditionsAndPrivacyPolicyAccepted
if (corePreferences.assistantDirectlyGoToThirdPartySipAccountLogin) {
skipLandingToThirdPartySipAccountEvent.postValue(Event(true))
}
}
showPassword.value = false
registrationInProgress.value = false
loginEnabled.addSource(sipIdentity) {
loginEnabled.value = isLoginButtonEnabled()
}
loginEnabled.addSource(password) {
loginEnabled.value = isLoginButtonEnabled()
}
}
@UiThread
fun login() {
coreContext.postOnCoreThread { core ->
core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
val userInput = sipIdentity.value.orEmpty().trim()
val defaultDomain = corePreferences.defaultDomain
val identity = if (userInput.startsWith("sip:")) {
if (userInput.contains("@")) {
userInput
} else {
"$userInput@$defaultDomain"
}
} else {
if (userInput.contains("@")) {
"sip:$userInput"
} else {
"sip:$userInput@$defaultDomain"
}
}
Log.i("$TAG Computed identity is [$identity] from user input [$userInput]")
val identityAddress = Factory.instance().createAddress(identity)
if (identityAddress == null) {
Log.e("$TAG Can't parse [$identity] as Address!")
showRedToast(R.string.assistant_login_cant_parse_address_toast, R.drawable.warning_circle)
return@postOnCoreThread
}
val accounts = core.accountList
val found = accounts.find {
it.params.identityAddress?.weakEqual(identityAddress) == true
}
if (found != null) {
Log.w("$TAG An account with the same identity address [${identityAddress.asStringUriOnly()}] already exists, do not add it again!")
showRedToast(R.string.assistant_account_login_already_connected_error, R.drawable.warning_circle)
return@postOnCoreThread
}
val user = identityAddress.username
if (user == null) {
Log.e(
"$TAG Address [${identityAddress.asStringUriOnly()}] doesn't contains an username!"
)
showRedToast(R.string.assistant_login_address_without_username_toast, R.drawable.warning_circle)
return@postOnCoreThread
}
val domain = identityAddress.domain
newlyCreatedAuthInfo = Factory.instance().createAuthInfo(
user,
null,
password.value.orEmpty().trim(),
null,
null,
domain
)
core.addAuthInfo(newlyCreatedAuthInfo)
val accountParams = core.createAccountParams()
accountParams.identityAddress = identityAddress
val prefix = internationalPrefix.value.orEmpty().trim()
val isoCountryCode = internationalPrefixIsoCountryCode.value.orEmpty()
if (prefix.isNotEmpty()) {
val prefixDigits = if (prefix.startsWith("+")) {
prefix.substring(1)
} else {
prefix
}
if (prefixDigits.isNotEmpty()) {
Log.i(
"$TAG Setting international prefix [$prefixDigits]($isoCountryCode) in account params"
)
accountParams.internationalPrefix = prefixDigits
accountParams.internationalPrefixIsoCountryCode = isoCountryCode
}
}
newlyCreatedAccount = core.createAccount(accountParams)
registrationInProgress.postValue(true)
core.addListener(coreListener)
Log.i(
"$TAG Trying to log in account with SIP identity [${identityAddress.asStringUriOnly()}]"
)
core.addAccount(newlyCreatedAccount)
}
}
@UiThread
fun toggleShowPassword() {
showPassword.value = showPassword.value == false
}
@UiThread
private fun isLoginButtonEnabled(): Boolean {
return sipIdentity.value.orEmpty().trim().isNotEmpty() && password.value.orEmpty().isNotEmpty()
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2010-2025 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.ui.assistant.viewmodel
import android.Manifest
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
class PermissionsViewModel
@UiThread
constructor() : GenericViewModel() {
companion object {
private const val TAG = "[Permissions ViewModel]"
}
val cameraPermissionGranted = MutableLiveData<Boolean>()
val recordAudioPermissionGranted = MutableLiveData<Boolean>()
val readContactsPermissionGranted = MutableLiveData<Boolean>()
val postNotificationsPermissionGranted = MutableLiveData<Boolean>()
fun setPermissionGranted(permission: String, granted: Boolean) {
Log.i("$TAG Permission [$permission] is ${if (granted) "granted" else "not granted yet/denied"}")
when (permission) {
Manifest.permission.READ_CONTACTS -> readContactsPermissionGranted.postValue(granted)
Manifest.permission.RECORD_AUDIO -> recordAudioPermissionGranted.postValue(granted)
Manifest.permission.CAMERA -> cameraPermissionGranted.postValue(granted)
Manifest.permission.POST_NOTIFICATIONS -> postNotificationsPermissionGranted.postValue(granted)
}
}
}

View file

@ -0,0 +1,139 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
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.ui.GenericViewModel
import org.linphone.utils.Event
import org.linphone.R
import org.linphone.core.GlobalState
import org.linphone.utils.LinphoneUtils
class QrCodeViewModel
@UiThread
constructor() : GenericViewModel() {
companion object {
private const val TAG = "[Qr Code Scanner ViewModel]"
}
val remoteProvisioningSuccessfulEvent = MutableLiveData<Event<Boolean>>()
val onErrorEvent = MutableLiveData<Event<Boolean>>()
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onConfiguringStatus(core: Core, status: ConfiguringState, message: String?) {
Log.i("$TAG Configuring state is [$status]")
if (status == ConfiguringState.Failed) {
Log.e("$TAG Failure applying remote provisioning: $message")
showRedToast(R.string.remote_provisioning_config_failed_toast, R.drawable.warning_circle)
onErrorEvent.postValue(Event(true))
}
}
@WorkerThread
override fun onGlobalStateChanged(core: Core, state: GlobalState?, message: String) {
if (state == GlobalState.On) {
if (core.accountList.isEmpty()) {
Log.w("$TAG Provisioning was successful but no account has been configured yet, staying in assistant")
// Remote provisioning didn't contain any account
// and there wasn't at least one configured before either
remoteProvisioningSuccessfulEvent.postValue(Event(false))
} else {
Log.i("$TAG At least an account exists in Core, leaving assistant")
remoteProvisioningSuccessfulEvent.postValue(Event(true))
}
}
}
@WorkerThread
override fun onQrcodeFound(core: Core, result: String?) {
Log.i("$TAG QR Code found: [$result]")
if (result == null) {
showRedToast(R.string.assistant_qr_code_invalid_toast, R.drawable.warning_circle)
} else {
val url = LinphoneUtils.getRemoteProvisioningUrlFromUri(result)
if (url == null) {
Log.e("$TAG The content of the QR Code [$result] doesn't seem to be a valid web URL")
showRedToast(R.string.assistant_qr_code_invalid_toast, R.drawable.warning_circle)
return
}
Log.i(
"$TAG Setting QR code URL [$url], restarting the Core outside of iterate() loop to apply configuration changes"
)
core.nativePreviewWindowId = null
core.isVideoPreviewEnabled = false
core.isQrcodeVideoPreviewEnabled = false
core.provisioningUri = url
coreContext.postOnCoreThread { core ->
Log.i("$TAG Stopping Core")
core.stop()
Log.i("$TAG Core has been stopped, restarting it")
core.start()
Log.i("$TAG Core has been restarted")
}
}
}
}
init {
coreContext.postOnCoreThread { core ->
core.addListener(coreListener)
}
}
@UiThread
override fun onCleared() {
coreContext.postOnCoreThread { core ->
core.removeListener(coreListener)
}
super.onCleared()
}
@UiThread
fun setBackCamera() {
coreContext.postOnCoreThread { core ->
// Just in case, on some devices such as Xiaomi Redmi Note 5
// this is required right after granting the CAMERA permission
core.reloadVideoDevices()
if (!coreContext.setBackCamera()) {
for (camera in core.videoDevicesList) {
if (camera != "StaticImage: Static picture") {
Log.w("$TAG No back facing camera found, using first one available [$camera]")
coreContext.core.videoDevice = camera
return@postOnCoreThread
}
}
Log.e("$TAG No camera device found!")
}
}
}
}

View file

@ -0,0 +1,321 @@
/*
* Copyright (c) 2010-2023 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.ui.assistant.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import java.util.Locale
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.Account
import org.linphone.core.AuthInfo
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.Factory
import org.linphone.core.Reason
import org.linphone.core.RegistrationState
import org.linphone.core.TransportType
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
class ThirdPartySipAccountLoginViewModel
@UiThread
constructor() : GenericViewModel() {
companion object {
private const val TAG = "[Third Party SIP Account Login ViewModel]"
}
val username = MutableLiveData<String>()
val authId = MutableLiveData<String>()
val password = MutableLiveData<String>()
val domain = MutableLiveData<String>()
val displayName = MutableLiveData<String>()
val transport = MutableLiveData<String>()
val internationalPrefix = MutableLiveData<String>()
val internationalPrefixIsoCountryCode = MutableLiveData<String>()
val showPassword = MutableLiveData<Boolean>()
val expandAdvancedSettings = MutableLiveData<Boolean>()
val proxy = MutableLiveData<String>()
val outboundProxy = MutableLiveData<String>()
val loginEnabled = MediatorLiveData<Boolean>()
val registrationInProgress = MutableLiveData<Boolean>()
val accountLoggedInEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val accountLoginErrorEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val defaultTransportIndexEvent: MutableLiveData<Event<Int>> by lazy {
MutableLiveData<Event<Int>>()
}
val availableTransports = arrayListOf<String>()
private lateinit var newlyCreatedAuthInfo: AuthInfo
private lateinit var newlyCreatedAccount: Account
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onAccountRegistrationStateChanged(
core: Core,
account: Account,
state: RegistrationState?,
message: String
) {
if (account == newlyCreatedAccount) {
Log.i("$TAG Newly created account registration state is [$state] ($message)")
if (state == RegistrationState.Ok) {
registrationInProgress.postValue(false)
core.removeListener(this)
// Set new account as default
core.defaultAccount = newlyCreatedAccount
accountLoggedInEvent.postValue(Event(true))
} else if (state == RegistrationState.Failed) {
registrationInProgress.postValue(false)
core.removeListener(this)
val error = when (account.error) {
Reason.Forbidden -> {
AppUtils.getString(R.string.assistant_account_login_forbidden_error)
}
else -> {
AppUtils.getFormattedString(
R.string.assistant_account_login_error,
account.error.toString()
)
}
}
accountLoginErrorEvent.postValue(Event(error))
Log.e("$TAG Account failed to REGISTER [$message], removing it")
core.removeAuthInfo(newlyCreatedAuthInfo)
core.removeAccount(newlyCreatedAccount)
}
}
}
}
init {
showPassword.value = false
expandAdvancedSettings.value = false
registrationInProgress.value = false
loginEnabled.addSource(username) {
loginEnabled.value = isLoginButtonEnabled()
}
loginEnabled.addSource(domain) {
loginEnabled.value = isLoginButtonEnabled()
}
// TODO: handle formatting errors ?
availableTransports.add(TransportType.Udp.name.uppercase(Locale.getDefault()))
availableTransports.add(TransportType.Tcp.name.uppercase(Locale.getDefault()))
availableTransports.add(TransportType.Tls.name.uppercase(Locale.getDefault()))
coreContext.postOnCoreThread {
domain.postValue(corePreferences.thirdPartySipAccountDefaultDomain)
val defaultTransport = corePreferences.thirdPartySipAccountDefaultTransport.uppercase(
Locale.getDefault()
)
val index = if (defaultTransport.isNotEmpty()) {
availableTransports.indexOf(defaultTransport)
} else {
availableTransports.size - 1
}
defaultTransportIndexEvent.postValue(Event(index))
}
}
@UiThread
fun login() {
coreContext.postOnCoreThread { core ->
core.loadConfigFromXml(corePreferences.thirdPartyDefaultValuesPath)
// Remove sip: in front of domain, just in case...
val domainValue = domain.value.orEmpty().trim()
val domainWithoutSip = if (domainValue.startsWith("sip:")) {
domainValue.substring("sip:".length)
} else {
domainValue
}
val domainAddress = Factory.instance().createAddress("sip:$domainWithoutSip")
val port = domainAddress?.port ?: -1
if (port != -1) {
Log.w("$TAG It seems a port [$port] was set in the domain [$domainValue], removing it from SIP identity but setting it to proxy server URI")
}
val domain = domainAddress?.domain ?: domainWithoutSip
// Allow to enter SIP identity instead of simply username
// in case identity domain doesn't match proxy domain
var user = username.value.orEmpty().trim()
if (user.startsWith("sip:")) {
user = user.substring("sip:".length)
} else if (user.startsWith("sips:")) {
user = user.substring("sips:".length)
}
if (user.contains("@")) {
user = user.split("@")[0]
}
val userId = authId.value.orEmpty().trim()
Log.i("$TAG Parsed username is [$user], user ID [$userId] and domain [$domain]")
val identity = "sip:$user@$domain"
val identityAddress = Factory.instance().createAddress(identity)
if (identityAddress == null) {
Log.e("$TAG Can't parse [$identity] as Address!")
showRedToast(R.string.assistant_login_cant_parse_address_toast, R.drawable.warning_circle)
return@postOnCoreThread
}
Log.i("$TAG Computed SIP identity is [${identityAddress.asStringUriOnly()}]")
val accounts = core.accountList
val found = accounts.find {
it.params.identityAddress?.weakEqual(identityAddress) == true
}
if (found != null) {
Log.w("$TAG An account with the same identity address [${found.params.identityAddress?.asStringUriOnly()}] already exists, do not add it again!")
showRedToast(R.string.assistant_account_login_already_connected_error, R.drawable.warning_circle)
return@postOnCoreThread
}
newlyCreatedAuthInfo = Factory.instance().createAuthInfo(
user,
userId,
password.value.orEmpty().trim(),
null,
null,
domainAddress?.domain ?: domainValue
)
core.addAuthInfo(newlyCreatedAuthInfo)
val accountParams = core.createAccountParams()
if (displayName.value.orEmpty().isNotEmpty()) {
identityAddress.displayName = displayName.value.orEmpty().trim()
}
accountParams.identityAddress = identityAddress
val proxyServerValue = proxy.value.orEmpty().trim()
val proxyServerAddress = if (proxyServerValue.isNotEmpty()) {
val server = if (proxyServerValue.startsWith("sip:")) {
proxyServerValue
} else {
"sip:$proxyServerValue"
}
Factory.instance().createAddress(server)
} else {
domainAddress ?: Factory.instance().createAddress("sip:$domainWithoutSip")
}
proxyServerAddress?.transport = when (transport.value.orEmpty().trim()) {
TransportType.Tcp.name.uppercase(Locale.getDefault()) -> TransportType.Tcp
TransportType.Tls.name.uppercase(Locale.getDefault()) -> TransportType.Tls
else -> TransportType.Udp
}
Log.i("$TAG Created proxy server SIP address [${proxyServerAddress?.asStringUriOnly()}]")
accountParams.serverAddress = proxyServerAddress
val outboundProxyValue = outboundProxy.value.orEmpty().trim()
val outboundProxyAddress = if (outboundProxyValue.isNotEmpty()) {
val server = if (outboundProxyValue.startsWith("sip:")) {
outboundProxyValue
} else {
"sip:$outboundProxyValue"
}
Factory.instance().createAddress(server)
} else {
null
}
if (outboundProxyAddress != null) {
outboundProxyAddress.transport = when (transport.value.orEmpty().trim()) {
TransportType.Tcp.name.uppercase(Locale.getDefault()) -> TransportType.Tcp
TransportType.Tls.name.uppercase(Locale.getDefault()) -> TransportType.Tls
else -> TransportType.Udp
}
Log.i("$TAG Created outbound proxy server SIP address [${outboundProxyAddress?.asStringUriOnly()}]")
accountParams.setRoutesAddresses(arrayOf(outboundProxyAddress))
}
val prefix = internationalPrefix.value.orEmpty().trim()
val isoCountryCode = internationalPrefixIsoCountryCode.value.orEmpty()
if (prefix.isNotEmpty()) {
val prefixDigits = if (prefix.startsWith("+")) {
prefix.substring(1)
} else {
prefix
}
if (prefixDigits.isNotEmpty()) {
Log.i(
"$TAG Setting international prefix [$prefixDigits]($isoCountryCode) in account params"
)
accountParams.internationalPrefix = prefixDigits
accountParams.internationalPrefixIsoCountryCode = isoCountryCode
}
}
newlyCreatedAccount = core.createAccount(accountParams)
registrationInProgress.postValue(true)
core.addListener(coreListener)
core.addAccount(newlyCreatedAccount)
}
}
@UiThread
fun toggleShowPassword() {
showPassword.value = showPassword.value == false
}
@UiThread
private fun isLoginButtonEnabled(): Boolean {
// Password isn't mandatory as authentication could be Bearer
return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty()
}
@UiThread
fun toggleAdvancedSettingsExpand() {
expandAdvancedSettings.value = expandAdvancedSettings.value == false
}
}

View file

@ -0,0 +1,584 @@
/*
* Copyright (c) 2010-2023 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.ui.call
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Color
import android.os.Bundle
import android.view.KeyEvent
import android.view.KeyboardShortcutGroup
import android.view.KeyboardShortcutInfo
import android.view.Menu
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.UiThread
import androidx.core.app.ActivityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlin.math.max
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.compatibility.Api28Compatibility
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.databinding.CallActivityBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.call.conference.fragment.ActiveConferenceCallFragmentDirections
import org.linphone.ui.call.conference.fragment.ConferenceLayoutMenuDialogFragment
import org.linphone.ui.call.fragment.ActiveCallFragmentDirections
import org.linphone.ui.call.fragment.AudioDevicesMenuDialogFragment
import org.linphone.ui.call.fragment.CallsListFragmentDirections
import org.linphone.ui.call.fragment.IncomingCallFragmentDirections
import org.linphone.ui.call.fragment.OutgoingCallFragmentDirections
import org.linphone.ui.call.model.AudioDeviceModel
import org.linphone.ui.call.viewmodel.CallsViewModel
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.ui.call.viewmodel.SharedCallViewModel
import org.linphone.ui.main.MainActivity
import org.linphone.utils.AppUtils
@UiThread
class CallActivity : GenericActivity() {
companion object {
private const val TAG = "[Call Activity]"
}
private lateinit var binding: CallActivityBinding
private lateinit var sharedViewModel: SharedCallViewModel
private lateinit var callsViewModel: CallsViewModel
private lateinit var callViewModel: CurrentCallViewModel
private var bottomSheetDialog: BottomSheetDialogFragment? = null
private var isPipSupported = false
private val requestCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Log.i("$TAG CAMERA permission has been granted, enabling video")
callViewModel.toggleVideo()
} else {
Log.e("$TAG CAMERA permission has been denied")
}
}
private val requestRecordAudioPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Log.i("$TAG RECORD_AUDIO permission has been granted, un-muting microphone")
callViewModel.toggleMuteMicrophone()
} else {
Log.e("$TAG RECORD_AUDIO permission has been denied")
}
}
override fun getTheme(): Resources.Theme {
val mainColor = corePreferences.themeMainColor
val theme = super.getTheme()
when (mainColor) {
"yellow" -> theme.applyStyle(R.style.Theme_LinphoneInCallYellow, true)
"green" -> theme.applyStyle(R.style.Theme_LinphoneInCallGreen, true)
"blue" -> theme.applyStyle(R.style.Theme_LinphoneInCallBlue, true)
"red" -> theme.applyStyle(R.style.Theme_LinphoneInCallRed, true)
"pink" -> theme.applyStyle(R.style.Theme_LinphoneInCallPink, true)
"purple" -> theme.applyStyle(R.style.Theme_LinphoneInCallPurple, true)
else -> theme.applyStyle(R.style.Theme_LinphoneInCall, true)
}
return theme
}
override fun onCreate(savedInstanceState: Bundle?) {
val style = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT) {
true // Force dark mode
}
enableEdgeToEdge(style, style)
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.call_activity)
binding.lifecycleOwner = this
setUpToastsArea(binding.toastsArea)
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
ViewCompat.setOnApplyWindowInsetsListener(binding.otherCallsTopBar.root) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
v.updatePadding(0, insets.top, 0, 0)
windowInsets
}
ViewCompat.setOnApplyWindowInsetsListener(binding.callNavContainer) { v, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val keyboard = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
v.updatePadding(insets.left, 0, insets.right, max(insets.bottom, keyboard.bottom))
WindowInsetsCompat.CONSUMED
}
lifecycleScope.launch(Dispatchers.Main) {
WindowInfoTracker
.getOrCreate(this@CallActivity)
.windowLayoutInfo(this@CallActivity)
.collect { newLayoutInfo ->
updateCurrentLayout(newLayoutInfo)
}
}
isPipSupported = packageManager.hasSystemFeature(
PackageManager.FEATURE_PICTURE_IN_PICTURE
)
Log.i("$TAG Is PiP supported [$isPipSupported]")
sharedViewModel = run {
ViewModelProvider(this)[SharedCallViewModel::class.java]
}
callViewModel = run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
binding.callViewModel = callViewModel
callsViewModel = run {
ViewModelProvider(this)[CallsViewModel::class.java]
}
binding.callsViewModel = callsViewModel
callViewModel.showAudioDevicesListEvent.observe(this) {
it.consume { devices ->
showAudioRoutesMenu(devices)
}
}
callViewModel.conferenceModel.showLayoutMenuEvent.observe(this) {
it.consume {
showConferenceLayoutMenu()
}
}
callViewModel.isVideoEnabled.observe(this) { enabled ->
if (isPipSupported) {
// Only enable PiP if video is enabled
Compatibility.enableAutoEnterPiP(this, enabled)
}
}
callViewModel.transferInProgressEvent.observe(this) {
it.consume {
showGreenToast(
getString(R.string.call_transfer_in_progress_toast),
R.drawable.phone_transfer
)
}
}
callViewModel.transferFailedEvent.observe(this) {
it.consume {
showRedToast(
getString(R.string.call_transfer_failed_toast),
R.drawable.warning_circle
)
}
}
callViewModel.goToEndedCallEvent.observe(this) {
it.consume { message ->
if (message.isNotEmpty()) {
showRedToast(message, R.drawable.warning_circle)
}
val action = ActiveCallFragmentDirections.actionGlobalEndedCallFragment()
findNavController(R.id.call_nav_container).navigate(action)
}
}
callViewModel.finishActivityEvent.observe(this) {
it.consume {
Log.i("$TAG Finishing activity")
finish()
}
}
callViewModel.requestRecordAudioPermission.observe(this) {
it.consume {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
Log.w("$TAG Asking for RECORD_AUDIO permission")
requestRecordAudioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
} else {
Log.i("$TAG Permission request for RECORD_AUDIO will be automatically denied, go to android app settings instead")
goToAndroidPermissionSettings()
}
}
}
callViewModel.requestCameraPermission.observe(this) {
it.consume {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
Log.w("$TAG Asking for CAMERA permission")
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
} else {
Log.i("$TAG Permission request for CAMERA will be automatically denied, go to android app settings instead")
goToAndroidPermissionSettings()
}
}
}
callViewModel.proximitySensorEnabled.observe(this) { enabled ->
Log.i("$TAG ${if (enabled) "Enabling" else "Disabling"} proximity sensor")
coreContext.enableProximitySensor(enabled)
}
callsViewModel.showIncomingCallEvent.observe(this) {
it.consume {
val action = IncomingCallFragmentDirections.actionGlobalIncomingCallFragment()
findNavController(R.id.call_nav_container).navigate(action)
}
}
callsViewModel.showOutgoingCallEvent.observe(this) {
it.consume {
val action = OutgoingCallFragmentDirections.actionGlobalOutgoingCallFragment()
findNavController(R.id.call_nav_container).navigate(action)
}
}
callsViewModel.goToActiveCallEvent.observe(this) {
it.consume { singleCall ->
navigateToActiveCall(singleCall)
}
}
callsViewModel.noCallFoundEvent.observe(this) {
it.consume {
finish()
}
}
callsViewModel.goToCallsListEvent.observe(this) {
it.consume {
val navController = findNavController(R.id.call_nav_container)
if (navController.currentDestination?.id == R.id.activeCallFragment) {
val action =
ActiveCallFragmentDirections.actionActiveCallFragmentToCallsListFragment()
navController.navigate(action)
} else if (navController.currentDestination?.id == R.id.activeConferenceCallFragment) {
val action =
ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToCallsListFragment()
navController.navigate(action)
}
}
}
sharedViewModel.toggleFullScreenEvent.observe(this) {
it.consume { hide ->
hideUI(hide)
}
}
coreContext.refreshMicrophoneMuteStateEvent.observe(this) {
it.consume {
Log.i(
"$TAG Refreshing microphone mute state, probably to sync with Android Auto action"
)
callViewModel.refreshMicrophoneState()
}
}
}
override fun onStart() {
super.onStart()
findNavController(R.id.call_nav_container).addOnDestinationChangedListener { _, destination, _ ->
val showTopBar = when (destination.id) {
R.id.inCallConversationFragment, R.id.transferCallFragment, R.id.newCallFragment -> true
else -> false
}
callsViewModel.showTopBar.postValue(showTopBar)
}
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
Log.e("$TAG RECORD_AUDIO permission isn't granted")
val message = R.string.call_audio_record_permission_not_granted_toast
val icon = R.drawable.warning_circle
showRedToast(getString(message), icon)
}
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
Log.e("$TAG CAMERA permission isn't granted")
val message = R.string.call_camera_permission_not_granted_toast
val icon = R.drawable.warning_circle
showRedToast(getString(message), icon)
}
}
override fun onResume() {
super.onResume()
val isInPipMode = isInPictureInPictureMode
Log.i("$TAG onResume: is in PiP mode? [$isInPipMode]")
if (::callViewModel.isInitialized) {
callViewModel.pipMode.value = isInPipMode
}
}
override fun onPause() {
coreContext.enableProximitySensor(false)
super.onPause()
bottomSheetDialog?.dismiss()
bottomSheetDialog = null
}
override fun onDestroy() {
coreContext.enableProximitySensor(false)
super.onDestroy()
coreContext.postOnCoreThread { core ->
Log.i("$TAG Clearing native video window ID")
core.nativeVideoWindowId = null
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent.extras?.getBoolean("ActiveCall", false) == true) {
navigateToActiveCall(
callViewModel.conferenceModel.isCurrentCallInConference.value == false
)
} else if (intent.extras?.getBoolean("IncomingCall", false) == true) {
val action = IncomingCallFragmentDirections.actionGlobalIncomingCallFragment()
findNavController(R.id.call_nav_container).navigate(action)
}
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
if (::callViewModel.isInitialized) {
if (isPipSupported && callViewModel.isVideoEnabled.value == true) {
Log.i("$TAG User leave hint, try entering PiP mode")
val pipMode = Compatibility.enterPipMode(this)
if (!pipMode) {
Log.e("$TAG Failed to enter PiP mode")
callViewModel.pipMode.value = false
}
}
}
}
override fun onProvideKeyboardShortcuts(
data: MutableList<KeyboardShortcutGroup?>?,
menu: Menu?,
deviceId: Int
) {
super.onProvideKeyboardShortcuts(data, menu, deviceId)
val keyboardShortcutGroup = KeyboardShortcutGroup(
"Answer/Decline incoming call",
listOf(
KeyboardShortcutInfo(
AppUtils.getString(R.string.call_action_answer),
KeyEvent.KEYCODE_A,
KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON
),
KeyboardShortcutInfo(
AppUtils.getString(R.string.call_action_decline),
KeyEvent.KEYCODE_D,
KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON
)
)
)
data?.add(keyboardShortcutGroup)
Log.i("$TAG Incoming call answer/decline shortcuts added")
}
override fun onKeyShortcut(keyCode: Int, event: KeyEvent?): Boolean {
if (event?.isCtrlPressed == true && event.isShiftPressed) {
val navController = findNavController(R.id.call_nav_container)
if (navController.currentDestination?.id == R.id.incomingCallFragment) {
when (keyCode) {
KeyEvent.KEYCODE_A -> {
Log.i("$TAG Answer incoming call shortcut triggered")
callViewModel.answer()
}
KeyEvent.KEYCODE_D -> {
Log.i("$TAG Decline incoming call shortcut triggered")
callViewModel.hangUp()
}
}
}
}
return true
}
fun goToMainActivity() {
if (isPipSupported && callViewModel.isVideoEnabled.value == true) {
Log.i("$TAG User is going back to MainActivity, try entering PiP mode")
val pipMode = Api28Compatibility.enterPipMode(this)
if (!pipMode) {
Log.e("$TAG Failed to enter PiP mode, finishing Activity")
callViewModel.pipMode.value = false
finish()
return
}
Log.i("$TAG Launching MainActivity to have PiP above it")
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
startActivity(intent)
} else {
Log.i("$TAG Either PiP isn't supported or video is not enabled, finishing Activity")
finish()
}
}
private fun updateCurrentLayout(newLayoutInfo: WindowLayoutInfo) {
if (newLayoutInfo.displayFeatures.isNotEmpty()) {
for (feature in newLayoutInfo.displayFeatures) {
val foldingFeature = feature as? FoldingFeature
if (foldingFeature != null) {
Log.i(
"$TAG Folding feature state changed: ${foldingFeature.state}, orientation is ${foldingFeature.orientation}"
)
sharedViewModel.foldingState.value = foldingFeature
}
}
}
}
private fun navigateToActiveCall(notInConference: Boolean) {
val navController = findNavController(R.id.call_nav_container)
val action = when (navController.currentDestination?.id) {
R.id.outgoingCallFragment -> {
if (notInConference) {
Log.i("$TAG Going from outgoing call fragment to call fragment")
OutgoingCallFragmentDirections.actionOutgoingCallFragmentToActiveCallFragment()
} else {
Log.i(
"$TAG Going from outgoing call fragment to conference call fragment"
)
OutgoingCallFragmentDirections.actionOutgoingCallFragmentToActiveConferenceCallFragment()
}
}
R.id.incomingCallFragment -> {
if (notInConference) {
Log.i("$TAG Going from incoming call fragment to call fragment")
IncomingCallFragmentDirections.actionIncomingCallFragmentToActiveCallFragment()
} else {
Log.i(
"$TAG Going from incoming call fragment to conference call fragment"
)
IncomingCallFragmentDirections.actionIncomingCallFragmentToActiveConferenceCallFragment()
}
}
R.id.activeCallFragment -> {
if (notInConference) {
Log.i("$TAG Going from call fragment to call fragment")
ActiveCallFragmentDirections.actionGlobalActiveCallFragment()
} else {
Log.i("$TAG Going from call fragment to conference call fragment")
ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment()
}
}
R.id.activeConferenceCallFragment -> {
if (notInConference) {
Log.i("$TAG Going from conference call fragment to call fragment")
ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToActiveCallFragment()
} else {
Log.i(
"$TAG Going from conference call fragment to conference call fragment"
)
ActiveConferenceCallFragmentDirections.actionGlobalActiveConferenceCallFragment()
}
}
R.id.callsListFragment -> {
if (notInConference) {
Log.i("$TAG Going calls list fragment to active call fragment")
CallsListFragmentDirections.actionCallsListFragmentToActiveCallFragment()
} else {
Log.i("$TAG Going calls list fragment to conference fragment")
CallsListFragmentDirections.actionCallsListFragmentToActiveConferenceCallFragment()
}
}
else -> {
if (notInConference) {
Log.i("$TAG Going from call fragment to call fragment")
ActiveCallFragmentDirections.actionGlobalActiveCallFragment()
} else {
Log.i("$TAG Going from call fragment to conference call fragment")
ActiveConferenceCallFragmentDirections.actionGlobalActiveConferenceCallFragment()
}
}
}
navController.navigate(action)
}
private fun hideUI(hide: Boolean) {
Log.i("$TAG Switching full screen mode to [${if (hide) "ON" else "OFF"}]")
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
if (hide) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
} else {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
}
}
private fun showAudioRoutesMenu(devicesList: List<AudioDeviceModel>) {
val modalBottomSheet = AudioDevicesMenuDialogFragment(devicesList)
modalBottomSheet.show(supportFragmentManager, AudioDevicesMenuDialogFragment.TAG)
bottomSheetDialog = modalBottomSheet
}
private fun showConferenceLayoutMenu() {
val modalBottomSheet = ConferenceLayoutMenuDialogFragment(callViewModel.conferenceModel)
modalBottomSheet.show(supportFragmentManager, ConferenceLayoutMenuDialogFragment.TAG)
bottomSheetDialog = modalBottomSheet
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2010-2023 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.ui.call.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.databinding.CallListCellBinding
import org.linphone.ui.call.model.CallModel
import org.linphone.utils.Event
class CallsListAdapter :
ListAdapter<CallModel, RecyclerView.ViewHolder>(CallDiffCallback()) {
var selectedAdapterPosition = -1
val callClickedEvent: MutableLiveData<Event<CallModel>> by lazy {
MutableLiveData<Event<CallModel>>()
}
val callLongClickedEvent: MutableLiveData<Event<CallModel>> by lazy {
MutableLiveData<Event<CallModel>>()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding: CallListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.call_list_cell,
parent,
false
)
val viewHolder = ViewHolder(binding)
binding.apply {
lifecycleOwner = parent.findViewTreeLifecycleOwner()
setOnClickListener {
callClickedEvent.value = Event(model!!)
}
setOnLongClickListener {
selectedAdapterPosition = viewHolder.bindingAdapterPosition
root.isSelected = true
callLongClickedEvent.value = Event(model!!)
true
}
}
return viewHolder
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as ViewHolder).bind(getItem(position))
}
fun resetSelection() {
notifyItemChanged(selectedAdapterPosition)
selectedAdapterPosition = -1
}
inner class ViewHolder(
val binding: CallListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
@UiThread
fun bind(callModel: CallModel) {
with(binding) {
model = callModel
binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
executePendingBindings()
}
}
}
private class CallDiffCallback : DiffUtil.ItemCallback<CallModel>() {
override fun areItemsTheSame(oldItem: CallModel, newItem: CallModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: CallModel, newItem: CallModel): Boolean {
return false
}
}
}

View file

@ -17,34 +17,31 @@
* 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.ui.conversations
package org.linphone.ui.call.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.databinding.ChatRoomListCellBinding
import org.linphone.utils.Event
import org.linphone.databinding.CallConferenceParticipantListCellBinding
import org.linphone.ui.call.conference.model.ConferenceParticipantModel
class ConversationsListAdapter(
private val viewLifecycleOwner: LifecycleOwner
) : ListAdapter<ChatRoomData, RecyclerView.ViewHolder>(ConversationDiffCallback()) {
val chatRoomClickedEvent = MutableLiveData<Event<ChatRoomData>>()
val chatRoomMenuClickedEvent = MutableLiveData<Event<ChatRoomData>>()
class ConferenceParticipantsListAdapter :
ListAdapter<ConferenceParticipantModel, RecyclerView.ViewHolder>(ParticipantDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding: ChatRoomListCellBinding = DataBindingUtil.inflate(
val binding: CallConferenceParticipantListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.chat_room_list_cell,
R.layout.call_conference_participant_list_cell,
parent,
false
)
binding.lifecycleOwner = parent.findViewTreeLifecycleOwner()
return ViewHolder(binding)
}
@ -52,32 +49,32 @@ class ConversationsListAdapter(
(holder as ViewHolder).bind(getItem(position))
}
inner class ViewHolder(
val binding: ChatRoomListCellBinding
class ViewHolder(
val binding: CallConferenceParticipantListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(chatRoomData: ChatRoomData) {
@UiThread
fun bind(participantModel: ConferenceParticipantModel) {
with(binding) {
data = chatRoomData
model = participantModel
lifecycleOwner = viewLifecycleOwner
executePendingBindings()
chatRoomData.chatRoomDataListener = object : ChatRoomDataListener() {
override fun onClicked() {
chatRoomClickedEvent.value = Event(chatRoomData)
}
}
}
}
}
}
private class ConversationDiffCallback : DiffUtil.ItemCallback<ChatRoomData>() {
override fun areItemsTheSame(oldItem: ChatRoomData, newItem: ChatRoomData): Boolean {
return oldItem.id.compareTo(newItem.id) == 0
}
private class ParticipantDiffCallback : DiffUtil.ItemCallback<ConferenceParticipantModel>() {
override fun areItemsTheSame(
oldItem: ConferenceParticipantModel,
newItem: ConferenceParticipantModel
): Boolean {
return oldItem.sipUri == newItem.sipUri
}
override fun areContentsTheSame(oldItem: ChatRoomData, newItem: ChatRoomData): Boolean {
return false
override fun areContentsTheSame(
oldItem: ConferenceParticipantModel,
newItem: ConferenceParticipantModel
): Boolean {
return false
}
}
}

View file

@ -0,0 +1,336 @@
/*
* Copyright (c) 2010-2023 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.ui.call.conference.fragment
import android.os.Bundle
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.doOnLayout
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import androidx.window.layout.FoldingFeature
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.CallActiveConferenceFragmentBinding
import org.linphone.ui.call.CallActivity
import org.linphone.ui.call.fragment.GenericCallFragment
import org.linphone.ui.call.viewmodel.CallsViewModel
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.startAnimatedDrawable
class ActiveConferenceCallFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Active Conference Call Fragment]"
}
private lateinit var binding: CallActiveConferenceFragmentBinding
private lateinit var callViewModel: CurrentCallViewModel
private lateinit var callsViewModel: CallsViewModel
private val actionsBottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
val drawable = AnimatedVectorDrawableCompat.create(
requireContext(),
R.drawable.animated_handle_to_caret
)
binding.bottomBar.mainActions.callActionsHandle.setImageDrawable(drawable)
} else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
val drawable = AnimatedVectorDrawableCompat.create(
requireContext(),
R.drawable.animated_caret_to_handle
)
binding.bottomBar.mainActions.callActionsHandle.setImageDrawable(drawable)
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) { }
}
private val backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val actionsBottomSheetBehavior = BottomSheetBehavior.from(binding.bottomBar.root)
if (actionsBottomSheetBehavior.state != BottomSheetBehavior.STATE_COLLAPSED) {
actionsBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
return
}
val callStatsBottomSheetBehavior = BottomSheetBehavior.from(binding.callStats.root)
if (callStatsBottomSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN) {
callStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
return
}
val callMediaEncryptionStatsBottomSheetBehavior = BottomSheetBehavior.from(
binding.callMediaEncryptionStats.root
)
if (callMediaEncryptionStatsBottomSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN) {
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
return
}
Log.i("$TAG Back gesture/click detected, no bottom sheet is expanded, going back")
isEnabled = false
try {
Log.i("$TAG Back gesture detected, going to MainActivity")
(requireActivity() as CallActivity).goToMainActivity()
} catch (ise: IllegalStateException) {
Log.w("$TAG Can't go back: $ise")
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallActiveConferenceFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
observeToastEvents(callViewModel)
observeToastEvents(callViewModel.conferenceModel)
callsViewModel = requireActivity().run {
ViewModelProvider(this)[CallsViewModel::class.java]
}
observeToastEvents(callsViewModel)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
binding.conferenceViewModel = callViewModel.conferenceModel
binding.callsViewModel = callsViewModel
binding.numpadModel = callViewModel.numpadModel
sharedViewModel.foldingState.observe(viewLifecycleOwner) { feature ->
updateHingeRelatedConstraints(feature)
}
val actionsBottomSheetBehavior = BottomSheetBehavior.from(binding.bottomBar.root)
actionsBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
actionsBottomSheetBehavior.addBottomSheetCallback(actionsBottomSheetCallback)
val callStatsBottomSheetBehavior = BottomSheetBehavior.from(binding.callStats.root)
callStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callStatsBottomSheetBehavior.skipCollapsed = true
val callMediaEncryptionStatsBottomSheetBehavior = BottomSheetBehavior.from(
binding.callMediaEncryptionStats.root
)
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callMediaEncryptionStatsBottomSheetBehavior.skipCollapsed = true
callViewModel.callDuration.observe(viewLifecycleOwner) { duration ->
binding.chronometer.base = SystemClock.elapsedRealtime() - (1000 * duration)
binding.chronometer.start()
}
callViewModel.toggleExtraActionsBottomSheetEvent.observe(viewLifecycleOwner) {
it.consume {
val state = actionsBottomSheetBehavior.state
if (state == BottomSheetBehavior.STATE_COLLAPSED) {
val drawable = AnimatedVectorDrawableCompat.create(
requireContext(),
R.drawable.animated_caret_to_handle
)
binding.bottomBar.mainActions.callActionsHandle.setImageDrawable(drawable)
binding.bottomBar.mainActions.callActionsHandle.startAnimatedDrawable()
actionsBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} else if (state == BottomSheetBehavior.STATE_EXPANDED) {
val drawable = AnimatedVectorDrawableCompat.create(
requireContext(),
R.drawable.animated_handle_to_caret
)
binding.bottomBar.mainActions.callActionsHandle.setImageDrawable(drawable)
binding.bottomBar.mainActions.callActionsHandle.startAnimatedDrawable()
actionsBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
}
callViewModel.conferenceModel.fullScreenMode.observe(viewLifecycleOwner) { hide ->
Log.i("$TAG Switching full screen mode to [${if (hide) "ON" else "OFF"}]")
sharedViewModel.toggleFullScreenEvent.value = Event(hide)
callStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
if (hide != callViewModel.fullScreenMode.value) {
callViewModel.fullScreenMode.value = hide
}
}
callViewModel.conferenceModel.conferenceLayout.observe(viewLifecycleOwner) { layout ->
// Collapse bottom sheet after changing conference layout
actionsBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
callViewModel.conferenceModel.participants.observe(viewLifecycleOwner) { participants ->
coreContext.postOnCoreThread { core ->
if (participants.size == 1) {
Log.i("$TAG We are alone in that conference, using nativePreviewWindowId")
core.nativePreviewWindowId = binding.localPreviewVideoSurface
if (callViewModel.conferenceModel.fullScreenMode.value == true && callViewModel.conferenceModel.isMeParticipantSendingVideo.value == false) {
// Don't forget to leave full screen mode, otherwise we won't be able to leave it by touching video surface...
callViewModel.conferenceModel.fullScreenMode.postValue(false)
}
}
}
}
callViewModel.conferenceModel.firstParticipantOtherThanOurselvesJoinedEvent.observe(
viewLifecycleOwner
) {
it.consume {
if (callViewModel.conferenceModel.fullScreenMode.value == false) {
Log.i("$TAG First participant joined conference, switching to full screen mode")
callViewModel.conferenceModel.toggleFullScreen()
}
}
}
callViewModel.conferenceModel.goToConversationEvent.observe(viewLifecycleOwner) {
it.consume { conversationId ->
if (findNavController().currentDestination?.id == R.id.activeConferenceCallFragment) {
Log.i("$TAG Display conversation with conversation ID [$conversationId]")
val action =
ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToInCallConversationFragment(
conversationId
)
findNavController().navigate(action)
}
}
}
callViewModel.goToCallEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.activeConferenceCallFragment) {
Log.i("$TAG Going to active call fragment")
val action =
ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToActiveCallFragment()
findNavController().navigate(action)
}
}
}
binding.setBackClickListener {
(requireActivity() as CallActivity).goToMainActivity()
}
binding.setCallsListClickListener {
Log.i("$TAG Going to calls list fragment")
if (findNavController().currentDestination?.id == R.id.activeConferenceCallFragment) {
val action =
ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToCallsListFragment()
findNavController().navigate(action)
}
}
binding.setParticipantsListClickListener {
Log.i("$TAG Going to conference participants list fragment")
if (findNavController().currentDestination?.id == R.id.activeConferenceCallFragment) {
val action =
ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToConferenceParticipantsListFragment()
findNavController().navigate(action)
}
}
binding.setCopyConferenceUriToClipboardClickListener {
val sipUri = callViewModel.conferenceModel.sipUri.value.orEmpty()
if (sipUri.isNotEmpty()) {
Log.i("$TAG Copying conference SIP URI [$sipUri] into clipboard")
val label = "Conference SIP address"
AppUtils.copyToClipboard(requireContext(), label, sipUri)
}
}
binding.setCallStatisticsClickListener {
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
binding.setCallMediaEncryptionStatisticsClickListener {
callStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
backPressedCallback
)
}
override fun onResume() {
super.onResume()
(binding.root as? ViewGroup)?.doOnLayout {
setupVideoPreview(binding.localPreviewVideoSurface)
}
coreContext.postOnCoreThread {
// Need to be done manually
callViewModel.updateCallDuration()
}
}
override fun onPause() {
super.onPause()
cleanVideoPreview(binding.localPreviewVideoSurface)
}
private fun updateHingeRelatedConstraints(feature: FoldingFeature) {
Log.i("$TAG Updating constraint layout hinges: $feature")
val constraintLayout = binding.constraintLayout
val set = ConstraintSet()
set.clone(constraintLayout)
if (feature.isSeparating && feature.state == FoldingFeature.State.HALF_OPENED && feature.orientation == FoldingFeature.Orientation.HORIZONTAL) {
set.setGuidelinePercent(R.id.hinge_top, 0.5f)
set.setGuidelinePercent(R.id.hinge_bottom, 0.5f)
callViewModel.halfOpenedFolded.value = true
} else {
set.setGuidelinePercent(R.id.hinge_top, 0f)
set.setGuidelinePercent(R.id.hinge_bottom, 1f)
callViewModel.halfOpenedFolded.value = false
}
set.applyTo(constraintLayout)
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright (c) 2010-2023 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.ui.call.conference.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.CallConferenceActiveSpeakerFragmentBinding
import org.linphone.ui.call.conference.viewmodel.ConferenceViewModel
import org.linphone.ui.call.fragment.GenericCallFragment
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
@UiThread
class ConferenceActiveSpeakerFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Conference Active Speaker Fragment]"
}
private lateinit var binding: CallConferenceActiveSpeakerFragmentBinding
private lateinit var callViewModel: CurrentCallViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallConferenceActiveSpeakerFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
binding.conferenceViewModel = callViewModel.conferenceModel
callViewModel.conferenceModel.conferenceLayout.observe(viewLifecycleOwner) {
when (it) {
ConferenceViewModel.GRID_LAYOUT -> {
Log.i(
"$TAG Conference layout changed to mosaic, navigating to matching fragment"
)
if (findNavController().currentDestination?.id == R.id.conferenceActiveSpeakerFragment) {
findNavController().navigate(
R.id.action_conferenceActiveSpeakerFragment_to_conferenceGridFragment
)
}
}
ConferenceViewModel.AUDIO_ONLY_LAYOUT -> {
Log.i(
"$TAG Conference layout changed to audio only, navigating to matching fragment"
)
if (findNavController().currentDestination?.id == R.id.conferenceActiveSpeakerFragment) {
findNavController().navigate(
R.id.action_conferenceActiveSpeakerFragment_to_conferenceAudioOnlyFragment
)
}
}
else -> {
}
}
}
coreContext.postOnCoreThread { core ->
Log.i("$TAG Setting native video window ID")
core.nativeVideoWindowId = binding.activeSpeakerSurface
}
}
}

View file

@ -0,0 +1,107 @@
/*
* Copyright (c) 2010-2024 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.ui.call.conference.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import org.linphone.core.tools.Log
import org.linphone.databinding.GenericAddParticipantsFragmentBinding
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.ui.main.fragment.GenericAddressPickerFragment
import org.linphone.ui.main.viewmodel.AddParticipantsViewModel
@UiThread
class ConferenceAddParticipantsFragment : GenericAddressPickerFragment() {
companion object {
private const val TAG = "[Conference Add Participants Fragment]"
}
private lateinit var binding: GenericAddParticipantsFragmentBinding
override lateinit var viewModel: AddParticipantsViewModel
private lateinit var callViewModel: CurrentCallViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = GenericAddParticipantsFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun goBack(): Boolean {
try {
return findNavController().popBackStack()
} catch (ise: IllegalStateException) {
Log.e("$TAG Can't go back popping back stack: $ise")
}
return false
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel = ViewModelProvider(this)[AddParticipantsViewModel::class.java]
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
postponeEnterTransition()
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
goBack()
}
setupRecyclerView(binding.contactsList)
viewModel.modelsList.observe(
viewLifecycleOwner
) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
adapter.submitList(it)
attachAdapter()
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
viewModel.selectedSipUrisEvent.observe(viewLifecycleOwner) {
it.consume { list ->
Log.i("$TAG Trying to add participant(s) [${list.size}] to conference")
callViewModel.conferenceModel.inviteSipUrisIntoConference(list)
goBack()
}
}
}
}

View file

@ -0,0 +1,100 @@
/*
* Copyright (c) 2010-2023 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.ui.call.conference.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.CallConferenceAudioOnlyFragmentBinding
import org.linphone.ui.call.conference.viewmodel.ConferenceViewModel
import org.linphone.ui.call.fragment.GenericCallFragment
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
@UiThread
class ConferenceAudioOnlyFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Conference Audio Only Fragment]"
}
private lateinit var binding: CallConferenceAudioOnlyFragmentBinding
private lateinit var callViewModel: CurrentCallViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallConferenceAudioOnlyFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
binding.conferenceViewModel = callViewModel.conferenceModel
callViewModel.conferenceModel.conferenceLayout.observe(viewLifecycleOwner) {
when (it) {
ConferenceViewModel.ACTIVE_SPEAKER_LAYOUT -> {
Log.i(
"$TAG Conference layout changed to active speaker, navigating to matching fragment"
)
if (findNavController().currentDestination?.id == R.id.conferenceAudioOnlyFragment) {
findNavController().navigate(
R.id.action_conferenceAudioOnlyFragment_to_conferenceActiveSpeakerFragment
)
}
}
ConferenceViewModel.GRID_LAYOUT -> {
Log.i(
"$TAG Conference layout changed to mosaic, navigating to matching fragment"
)
if (findNavController().currentDestination?.id == R.id.conferenceAudioOnlyFragment) {
findNavController().navigate(
R.id.action_conferenceAudioOnlyFragment_to_conferenceGridFragment
)
}
}
else -> {
}
}
}
}
override fun onResume() {
super.onResume()
Log.i("$TAG Making sure we are not in full-screen mode")
callViewModel.fullScreenMode.value = false
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2010-2023 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.ui.call.conference.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.CallConferenceGridFragmentBinding
import org.linphone.ui.call.conference.viewmodel.ConferenceViewModel
import org.linphone.ui.call.fragment.GenericCallFragment
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
@UiThread
class ConferenceGridFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Conference Grid Fragment]"
}
private lateinit var binding: CallConferenceGridFragmentBinding
private lateinit var callViewModel: CurrentCallViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallConferenceGridFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
binding.conferenceViewModel = callViewModel.conferenceModel
callViewModel.conferenceModel.conferenceLayout.observe(viewLifecycleOwner) {
when (it) {
ConferenceViewModel.ACTIVE_SPEAKER_LAYOUT -> {
Log.i(
"$TAG Conference layout changed to active speaker, navigating to matching fragment"
)
if (findNavController().currentDestination?.id == R.id.conferenceGridFragment) {
findNavController().navigate(
R.id.action_conferenceGridFragment_to_conferenceActiveSpeakerFragment
)
}
}
ConferenceViewModel.AUDIO_ONLY_LAYOUT -> {
Log.i(
"$TAG Conference layout changed to audio only, navigating to matching fragment"
)
if (findNavController().currentDestination?.id == R.id.conferenceGridFragment) {
findNavController().navigate(
R.id.action_conferenceGridFragment_to_conferenceAudioOnlyFragment
)
}
}
else -> {
}
}
}
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (c) 2010-2023 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.ui.call.conference.fragment
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.databinding.CallConferenceLayoutBottomSheetBinding
import org.linphone.ui.call.conference.viewmodel.ConferenceViewModel
@UiThread
class ConferenceLayoutMenuDialogFragment(
val conferenceModel: ConferenceViewModel,
private val onDismiss: (() -> Unit)? = null
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "ConferenceLayoutMenuDialogFragment"
}
override fun onCancel(dialog: DialogInterface) {
onDismiss?.invoke()
super.onCancel(dialog)
}
override fun onDismiss(dialog: DialogInterface) {
onDismiss?.invoke()
super.onDismiss(dialog)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
// Makes sure all menu entries are visible,
// required for landscape mode (otherwise only first item is visible)
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = CallConferenceLayoutBottomSheetBinding.inflate(layoutInflater)
view.lifecycleOwner = viewLifecycleOwner
view.viewModel = conferenceModel
view.setGridClickListener {
if (conferenceModel.participantDevices.value.orEmpty().size < 7) {
conferenceModel.changeLayout(ConferenceViewModel.GRID_LAYOUT)
dismiss()
}
}
view.setActiveSpeakerClickListener {
conferenceModel.changeLayout(ConferenceViewModel.ACTIVE_SPEAKER_LAYOUT)
dismiss()
}
view.setAudioOnlyClickListener {
conferenceModel.changeLayout(ConferenceViewModel.AUDIO_ONLY_LAYOUT)
dismiss()
}
return view.root
}
}

View file

@ -0,0 +1,219 @@
/*
* Copyright (c) 2010-2023 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.ui.call.conference.fragment
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.PopupWindow
import androidx.core.view.doOnLayout
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Participant
import org.linphone.core.tools.Log
import org.linphone.databinding.CallConferenceParticipantsListFragmentBinding
import org.linphone.databinding.CallConferenceParticipantsListPopupMenuBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.call.adapter.ConferenceParticipantsListAdapter
import org.linphone.ui.call.fragment.GenericCallFragment
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.utils.AppUtils
import org.linphone.utils.ConfirmationDialogModel
import org.linphone.utils.DialogUtils
class ConferenceParticipantsListFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Conference Participants List Fragment]"
}
private lateinit var binding: CallConferenceParticipantsListFragmentBinding
private lateinit var viewModel: CurrentCallViewModel
private lateinit var adapter: ConferenceParticipantsListAdapter
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (findNavController().currentDestination?.id == R.id.conferenceAddParticipantsFragment) {
// Holds fragment in place while new fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
}
return super.onCreateAnimation(transit, enter, nextAnim)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = ConferenceParticipantsListAdapter()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallConferenceParticipantsListFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
observeToastEvents(viewModel.conferenceModel)
binding.participantsList.setHasFixedSize(true)
binding.participantsList.layoutManager = LinearLayoutManager(requireContext())
binding.setBackClickListener {
findNavController().popBackStack()
}
binding.setAddParticipantsClickListener {
if (findNavController().currentDestination?.id == R.id.conferenceParticipantsListFragment) {
val action =
ConferenceParticipantsListFragmentDirections.actionConferenceParticipantsListFragmentToConferenceAddParticipantsFragment()
findNavController().navigate(action)
}
}
binding.setShowMenuClickListener {
showPopupMenu(binding.showMenu)
}
viewModel.conferenceModel.participants.observe(viewLifecycleOwner) {
Log.i("$TAG participants list updated with [${it.size}] items")
adapter.submitList(it)
// Wait for adapter to have items before setting it in the RecyclerView,
// otherwise scroll position isn't retained
if (binding.participantsList.adapter != adapter) {
binding.participantsList.adapter = adapter
}
}
viewModel.conferenceModel.removeParticipantEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val displayName = pair.first
val participant = pair.second
showKickParticipantDialog(displayName, participant)
}
}
viewModel.isSendingVideo.observe(viewLifecycleOwner) { sending ->
coreContext.postOnCoreThread { core ->
core.nativePreviewWindowId = if (sending) {
Log.i("$TAG We are sending video, setting capture preview surface")
binding.localPreviewVideoSurface
} else {
Log.i("$TAG We are not sending video, clearing capture preview surface")
null
}
}
}
}
override fun onResume() {
super.onResume()
(binding.root as? ViewGroup)?.doOnLayout {
setupVideoPreview(binding.localPreviewVideoSurface)
}
}
override fun onPause() {
super.onPause()
cleanVideoPreview(binding.localPreviewVideoSurface)
}
private fun showKickParticipantDialog(displayName: String, participant: Participant) {
val model = ConfirmationDialogModel()
val dialog = DialogUtils.getKickConferenceParticipantConfirmationDialog(
requireActivity(),
model,
displayName
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
coreContext.postOnCoreThread {
viewModel.conferenceModel.kickParticipant(participant)
}
val message = getString(R.string.conference_participant_was_kicked_out_toast)
val icon = R.drawable.check
(requireActivity() as GenericActivity).showGreenToast(message, icon)
dialog.dismiss()
}
}
dialog.show()
}
private fun showPopupMenu(view: View) {
val popupView: CallConferenceParticipantsListPopupMenuBinding = DataBindingUtil.inflate(
LayoutInflater.from(requireContext()),
R.layout.call_conference_participants_list_popup_menu,
null,
false
)
val popupWindow = PopupWindow(
popupView.root,
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
true
)
popupView.setShareInvitationClickListener {
val sipUri = viewModel.conferenceModel.sipUri.value.orEmpty()
if (sipUri.isNotEmpty()) {
Log.i("$TAG Sharing conference SIP URI [$sipUri]")
val label = "Conference SIP address"
AppUtils.copyToClipboard(requireContext(), label, sipUri)
}
popupWindow.dismiss()
}
// Elevation is for showing a shadow around the popup
popupWindow.elevation = 20f
popupWindow.showAsDropDown(view, 0, 0, Gravity.BOTTOM)
}
}

View file

@ -0,0 +1,241 @@
/*
* Copyright (c) 2010-2023 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.ui.call.conference.model
import android.view.TextureView
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.ParticipantDevice
import org.linphone.core.ParticipantDeviceListenerStub
import org.linphone.core.StreamType
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
class ConferenceParticipantDeviceModel
@WorkerThread
constructor(
val device: ParticipantDevice,
val isMe: Boolean = false
) {
companion object {
private const val TAG = "[Conference Participant Device Model]"
}
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(device.address)
val name = avatarModel.contactName ?: device.name.orEmpty().ifEmpty {
LinphoneUtils.getDisplayName(device.address)
}
val isMuted = MutableLiveData<Boolean>()
val isSpeaking = MutableLiveData<Boolean>()
val isActiveSpeaker = MutableLiveData<Boolean>()
val isScreenSharing = MutableLiveData<Boolean>()
val isVideoAvailable = MutableLiveData<Boolean>()
val isThumbnailAvailable = MutableLiveData<Boolean>()
val isJoining = MutableLiveData<Boolean>()
val isInConference = MutableLiveData<Boolean>()
private lateinit var textureView: TextureView
private val deviceListener = object : ParticipantDeviceListenerStub() {
@WorkerThread
override fun onStateChanged(
participantDevice: ParticipantDevice,
state: ParticipantDevice.State?
) {
Log.i(
"$TAG Participant device [${participantDevice.address.asStringUriOnly()}] state changed [$state]"
)
when (state) {
ParticipantDevice.State.Joining, ParticipantDevice.State.Alerting -> {
isJoining.postValue(true)
}
ParticipantDevice.State.OnHold -> {
isInConference.postValue(false)
}
ParticipantDevice.State.Present -> {
isJoining.postValue(false)
isInConference.postValue(true)
}
else -> {}
}
}
@WorkerThread
override fun onIsMuted(participantDevice: ParticipantDevice, muted: Boolean) {
Log.d(
"$TAG Participant device [${participantDevice.address.asStringUriOnly()}] is ${if (participantDevice.isMuted) "muted" else "no longer muted"}"
)
isMuted.postValue(participantDevice.isMuted)
}
@WorkerThread
override fun onIsSpeakingChanged(
participantDevice: ParticipantDevice,
speaking: Boolean
) {
Log.d(
"$TAG Participant device [${participantDevice.address.asStringUriOnly()}] is ${if (participantDevice.isSpeaking) "speaking" else "no longer speaking"}"
)
isSpeaking.postValue(speaking)
}
@WorkerThread
override fun onScreenSharingChanged(
participantDevice: ParticipantDevice,
screenSharing: Boolean
) {
Log.i(
"$TAG Participant device [${participantDevice.address.asStringUriOnly()}] is ${if (screenSharing) "sharing its screen" else "no longer sharing its screen"}"
)
isScreenSharing.postValue(screenSharing)
}
@WorkerThread
override fun onThumbnailStreamAvailabilityChanged(
participantDevice: ParticipantDevice,
available: Boolean
) {
Log.i(
"$TAG Participant device [${participantDevice.address.asStringUriOnly()}] thumbnail availability changed to ${if (available) "available" else "not available"}"
)
isThumbnailAvailable.postValue(available)
}
@WorkerThread
override fun onStreamAvailabilityChanged(
participantDevice: ParticipantDevice,
available: Boolean,
streamType: StreamType?
) {
if (streamType == StreamType.Video) {
Log.i(
"$TAG Participant device [${participantDevice.address.asStringUriOnly()}] video stream availability changed to ${if (available) "available" else "not available"}"
)
isVideoAvailable.postValue(available)
}
}
}
init {
device.addListener(deviceListener)
val state = device.state
val joining = state == ParticipantDevice.State.Joining || state == ParticipantDevice.State.Alerting
isJoining.postValue(joining)
val inConference = device.isInConference
isInConference.postValue(inConference)
if (joining) {
Log.i(
"$TAG Participant [${device.address.asStringUriOnly()}] is joining the conference (state [$state])"
)
} else {
Log.i(
"$TAG Participant [${device.address.asStringUriOnly()}] is [${if (inConference) "inside" else "outside"}] the conference with state [${device.state}]"
)
}
isMuted.postValue(device.isMuted)
isSpeaking.postValue(device.isSpeaking)
Log.i(
"$TAG Participant [${device.address.asStringUriOnly()}] is in state [${device.state}]"
)
isActiveSpeaker.postValue(false)
val screenSharing = device.isScreenSharingEnabled
isScreenSharing.postValue(screenSharing)
if (screenSharing) {
Log.i("$TAG Participant [${device.address.asStringUriOnly()}] is sharing its screen")
}
val videoAvailability = device.getStreamAvailability(StreamType.Video)
isVideoAvailable.postValue(videoAvailability)
Log.i(
"$TAG Participant device [${device.address.asStringUriOnly()}] video stream availability is ${if (videoAvailability) "available" else "not available"}"
)
// In case of joining conference without bundle mode, thumbnail stream availability will be false,
// but we need to display our video preview for video stream to be sent
val thumbnailVideoAvailability = if (isMe) videoAvailability else device.thumbnailStreamAvailability
isThumbnailAvailable.postValue(thumbnailVideoAvailability)
Log.i(
"$TAG Participant device [${device.address.asStringUriOnly()}] thumbnail availability is ${if (thumbnailVideoAvailability) "available" else "not available"}"
)
}
@WorkerThread
fun destroy() {
clearWindowId()
device.removeListener(deviceListener)
}
@WorkerThread
fun clearWindowId() {
Log.i("$TAG Clearing participant [${device.address.asStringUriOnly()}] device window ID")
device.nativeVideoWindowId = null
}
@UiThread
fun setTextureView(view: TextureView) {
Log.i(
"$TAG TextureView for participant [${device.address.asStringUriOnly()}] available from UI [$view]"
)
textureView = view
coreContext.postOnCoreThread {
updateWindowId()
}
}
@WorkerThread
private fun updateWindowId() {
if (::textureView.isInitialized) {
// SDK does it but it's a bit better this way, prevents going to participants map in PlatformHelper for nothing
if (isMe) {
Log.i(
"$TAG Setting our own video preview window ID [$textureView]"
)
coreContext.core.nativePreviewWindowId = textureView
} else {
Log.i(
"$TAG Setting participant [${device.address.asStringUriOnly()}] window ID [$textureView]"
)
device.nativeVideoWindowId = textureView
}
} else {
if (isMe) {
Log.e("$TAG Our TextureView wasn't initialized yet!")
} else {
Log.e(
"$TAG TextureView for participant [${device.address.asStringUriOnly()}] wasn't initialized yet!"
)
}
}
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2010-2023 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.ui.call.conference.model
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Participant
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
class ConferenceParticipantModel
@WorkerThread
constructor(
val participant: Participant,
val avatarModel: ContactAvatarModel,
isMyselfAdmin: Boolean,
val isMyself: Boolean,
private val removeFromConference: ((participant: Participant) -> Unit)?,
private val changeAdminStatus: ((participant: Participant, setAdmin: Boolean) -> Unit)?
) {
companion object {
private const val TAG = "[Conference Participant Model]"
}
val sipUri = participant.address.asStringUriOnly()
val isAdmin = MutableLiveData<Boolean>()
val isMeAdmin = MutableLiveData<Boolean>()
init {
isAdmin.postValue(participant.isAdmin)
isMeAdmin.postValue(isMyselfAdmin)
}
@UiThread
fun removeParticipant() {
Log.w("$TAG Removing participant from conference")
coreContext.postOnCoreThread {
removeFromConference?.invoke(participant)
}
}
@UiThread
fun toggleAdminStatus() {
val newStatus = isAdmin.value == false
Log.w(
"$TAG Changing participant admin status to ${if (newStatus) "admin" else "not admin"}"
)
isAdmin.postValue(newStatus)
coreContext.postOnCoreThread {
changeAdminStatus?.invoke(participant, newStatus)
}
}
}

View file

@ -0,0 +1,126 @@
/*
* 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.ui.call.conference.view
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.widget.GridLayout
import androidx.annotation.UiThread
import androidx.core.view.children
import org.linphone.core.tools.Log
import androidx.core.view.isEmpty
@UiThread
class GridBoxLayout : GridLayout {
companion object {
private const val TAG = "[Grid Box Layout]"
const val MAX_CHILD = 6
private val placementMatrix = arrayOf(
intArrayOf(1, 2, 3, 4, 5, 6),
intArrayOf(1, 1, 2, 2, 3, 3),
intArrayOf(1, 1, 1, 2, 2, 2),
intArrayOf(1, 1, 1, 1, 2, 2),
intArrayOf(1, 1, 1, 1, 1, 2),
intArrayOf(1, 1, 1, 1, 1, 1)
)
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
private var centerContent: Boolean = true
private var previousChildCount = 0
private var previousCellSize = 0
@SuppressLint("DrawAllocation")
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
if (isEmpty() || (!changed && previousChildCount == childCount)) {
super.onLayout(changed, left, top, right, bottom)
// To prevent display issue the first time conference is locally paused
children.forEach { child ->
child.post {
child.layoutParams.width = previousCellSize
child.layoutParams.height = previousCellSize
child.requestLayout()
}
}
return
}
// To prevent java.lang.IllegalArgumentException: columnCount must be greater than or equal
// to the maximum of all grid indices (and spans) defined in the LayoutParams of each child.
children.forEach { child ->
child.layoutParams = LayoutParams()
}
val maxChild = placementMatrix[0].size
if (childCount > maxChild) {
Log.e(
"$TAG $childCount children but placementMatrix only knows how to display $maxChild (max allowed participants for grid layout in settings is 6)"
)
return
}
val availableSize = Pair(right - left, bottom - top)
var cellSize = 0
for (index in 1..childCount) {
val neededColumns = placementMatrix[index - 1][childCount - 1]
val candidateWidth = 1 * availableSize.first / neededColumns
val candidateHeight = 1 * availableSize.second / index
val candidateSize = if (candidateWidth < candidateHeight) candidateWidth else candidateHeight
if (candidateSize > cellSize) {
columnCount = neededColumns
rowCount = index
cellSize = candidateSize
}
}
previousCellSize = cellSize
previousChildCount = childCount
super.onLayout(changed, left, top, right, bottom)
children.forEach { child ->
child.layoutParams.width = cellSize
child.layoutParams.height = cellSize
child.post {
child.requestLayout()
}
}
if (centerContent) {
setPadding(
(availableSize.first - (columnCount * cellSize)) / 2,
(availableSize.second - (rowCount * cellSize)) / 2,
(availableSize.first - (columnCount * cellSize)) / 2,
(availableSize.second - (rowCount * cellSize)) / 2
)
}
Log.d(
"$TAG cellsize=$cellSize columns=$columnCount rows=$rowCount availablesize=$availableSize"
)
}
}

View file

@ -0,0 +1,852 @@
/*
* Copyright (c) 2010-2023 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.ui.call.conference.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Address
import org.linphone.core.Call
import org.linphone.core.Conference
import org.linphone.core.ConferenceListenerStub
import org.linphone.core.MediaDirection
import org.linphone.core.Participant
import org.linphone.core.ParticipantDevice
import org.linphone.core.StreamType
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.ui.call.conference.model.ConferenceParticipantDeviceModel
import org.linphone.ui.call.conference.model.ConferenceParticipantModel
import org.linphone.ui.call.conference.view.GridBoxLayout
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
class ConferenceViewModel
@UiThread
constructor() : GenericViewModel() {
companion object {
private const val TAG = "[Conference ViewModel]"
const val AUDIO_ONLY_LAYOUT = -1
const val GRID_LAYOUT = 0 // Conference.Layout.Grid
const val ACTIVE_SPEAKER_LAYOUT = 1 // Conference.Layout.ActiveSpeaker
}
val subject = MutableLiveData<String>()
val sipUri = MutableLiveData<String>()
val participants = MutableLiveData<ArrayList<ConferenceParticipantModel>>()
val participantDevices = MutableLiveData<ArrayList<ConferenceParticipantDeviceModel>>()
val participantsLabel = MutableLiveData<String>()
val activeSpeaker = MutableLiveData<ConferenceParticipantDeviceModel>()
val isCurrentCallInConference = MutableLiveData<Boolean>()
val conferenceLayout = MutableLiveData<Int>()
val screenSharingParticipantName = MutableLiveData<String>()
val isScreenSharing = MutableLiveData<Boolean>()
val isPaused = MutableLiveData<Boolean>()
val isMeParticipantSendingVideo = MutableLiveData<Boolean>()
val isMeAdmin = MutableLiveData<Boolean>()
val isConversationAvailable = MutableLiveData<Boolean>()
val fullScreenMode = MutableLiveData<Boolean>()
val firstParticipantOtherThanOurselvesJoinedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val showLayoutMenuEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val removeParticipantEvent: MutableLiveData<Event<Pair<String, Participant>>> by lazy {
MutableLiveData<Event<Pair<String, Participant>>>()
}
val goToConversationEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
private lateinit var conference: Conference
private val conferenceListener = object : ConferenceListenerStub() {
@WorkerThread
override fun onParticipantAdded(conference: Conference, participant: Participant) {
Log.i(
"$TAG Participant added: ${participant.address.asStringUriOnly()}"
)
addParticipant(participant)
if (conference.participantList.size == 1) { // we do not count
Log.i("$TAG First participant other than ourselves joined the conference")
firstParticipantOtherThanOurselvesJoinedEvent.postValue(Event(true))
}
}
@WorkerThread
override fun onParticipantRemoved(conference: Conference, participant: Participant) {
Log.i(
"$TAG Participant removed: ${participant.address.asStringUriOnly()}"
)
removeParticipant(participant)
}
@WorkerThread
override fun onParticipantDeviceMediaCapabilityChanged(
conference: Conference,
device: ParticipantDevice
) {
if (device.isMe) {
Log.i("$TAG Our device media capability changed")
val direction = device.getStreamCapability(StreamType.Video)
val sendingVideo = direction == MediaDirection.SendRecv || direction == MediaDirection.SendOnly
localVideoStreamToggled(sendingVideo)
} else {
Log.i("$TAG Participant [${device.address.asStringUriOnly()}] device media capability changed")
}
}
@WorkerThread
override fun onActiveSpeakerParticipantDevice(
conference: Conference,
participantDevice: ParticipantDevice?
) {
activeSpeaker.value?.isActiveSpeaker?.postValue(false)
if (participantDevice != null) {
val found = participantDevices.value.orEmpty().find {
it.device.address.equal(participantDevice.address)
}
if (found != null) {
Log.i("$TAG Newly active speaker participant is [${found.name}]")
found.isActiveSpeaker.postValue(true)
activeSpeaker.postValue(found!!)
} else {
Log.i("$TAG Failed to find actively speaking participant...")
val model = ConferenceParticipantDeviceModel(participantDevice)
model.isActiveSpeaker.postValue(true)
activeSpeaker.postValue(model)
}
} else {
Log.w("$TAG Notified active speaker participant device is null, using first one that's not us")
val firstNotUs = participantDevices.value.orEmpty().find {
!it.isMe
}
if (firstNotUs != null) {
Log.i("$TAG Newly active speaker participant is [${firstNotUs.name}]")
firstNotUs.isActiveSpeaker.postValue(true)
activeSpeaker.postValue(firstNotUs!!)
} else {
Log.i("$TAG No participant device that's not us found, expected if we're alone")
}
}
}
@WorkerThread
override fun onParticipantAdminStatusChanged(
conference: Conference,
participant: Participant
) {
// Only recompute participants list
computeParticipants(true)
}
@WorkerThread
override fun onParticipantDeviceAdded(
conference: Conference,
participantDevice: ParticipantDevice
) {
Log.i(
"$TAG Participant device added: ${participantDevice.address.asStringUriOnly()}"
)
// Since we do not compute our own devices until another participant joins,
// We have to do it when someone else joins
if (participantDevices.value.orEmpty().isEmpty()) {
val list = arrayListOf<ConferenceParticipantDeviceModel>()
val ourDevices = conference.me.devices
Log.i("$TAG We have [${ourDevices.size}] devices, now it's time to add them")
for (device in ourDevices) {
val model = ConferenceParticipantDeviceModel(device, true)
list.add(model)
}
val newModel = ConferenceParticipantDeviceModel(participantDevice)
list.add(newModel)
participantDevices.postValue(sortParticipantDevicesList(list))
} else {
addParticipantDevice(participantDevice)
}
}
@WorkerThread
override fun onParticipantDeviceRemoved(
conference: Conference,
participantDevice: ParticipantDevice
) {
Log.i(
"$TAG Participant device removed: ${participantDevice.address.asStringUriOnly()}"
)
removeParticipantDevice(participantDevice)
}
@WorkerThread
override fun onParticipantDeviceStateChanged(
conference: Conference,
device: ParticipantDevice,
state: ParticipantDevice.State
) {
Log.i(
"$TAG Participant device [${device.address.asStringUriOnly()}] state changed [$state]"
)
}
@WorkerThread
override fun onParticipantDeviceScreenSharingChanged(
conference: Conference,
device: ParticipantDevice,
enabled: Boolean
) {
Log.i(
"$TAG Participant device [${device.address.asStringUriOnly()}] is ${if (enabled) "sharing it's screen" else "no longer sharing it's screen"}"
)
isScreenSharing.postValue(enabled)
if (enabled) {
val deviceModel = participantDevices.value.orEmpty().find {
it.device == device || device.address.weakEqual(it.device.address)
}
if (deviceModel != null) {
screenSharingParticipantName.postValue(deviceModel.name)
} else {
Log.w("$TAG Failed to find screen sharing participant device model!")
}
val call = conference.call
if (call != null) {
val currentLayout = getCurrentLayout(call)
if (currentLayout == GRID_LAYOUT) {
Log.w(
"$TAG Current layout is mosaic but screen sharing was enabled, switching to active speaker layout"
)
setNewLayout(ACTIVE_SPEAKER_LAYOUT)
}
} else {
Log.e("$TAG Screen sharing was enabled but conference's call is null!")
}
} else {
screenSharingParticipantName.postValue("")
}
}
@WorkerThread
override fun onStateChanged(conference: Conference, state: Conference.State) {
Log.i("$TAG State changed [$state]")
if (conference.state == Conference.State.Created) {
val isIn = conference.isIn
isPaused.postValue(!isIn)
Log.i("$TAG We [${if (isIn) "are" else "aren't"}] in the conference")
subject.postValue(conference.subjectUtf8.orEmpty())
computeParticipants(false)
if (conference.participantList.size >= 1) { // we do not count
Log.i("$TAG Joined conference already has at least another participant")
firstParticipantOtherThanOurselvesJoinedEvent.postValue(Event(true))
}
}
}
}
init {
isPaused.value = false
isConversationAvailable.value = false
isMeParticipantSendingVideo.value = false
fullScreenMode.value = false
}
@WorkerThread
fun destroy() {
isCurrentCallInConference.postValue(false)
if (::conference.isInitialized) {
conference.removeListener(conferenceListener)
participantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceModel::destroy)
}
}
@WorkerThread
fun configureFromCall(call: Call) {
val conf = call.conference ?: return
if (::conference.isInitialized) {
conference.removeListener(conferenceListener)
}
isCurrentCallInConference.postValue(true)
conference = conf
conference.addListener(conferenceListener)
val isIn = conference.isIn
val state = conf.state
if (state != Conference.State.CreationPending) {
isPaused.postValue(!isIn)
}
Log.i(
"$TAG We [${if (isIn) "are" else "aren't"}] in the conference right now, current state is [$state]"
)
val screenSharing = conference.screenSharingParticipant != null
isScreenSharing.postValue(screenSharing)
val chatEnabled = conference.currentParams.isChatEnabled
isConversationAvailable.postValue(chatEnabled)
val confSubject = conference.subjectUtf8.orEmpty()
Log.i(
"$TAG Configuring conference with subject [$confSubject] from call [${call.callLog.callId}]"
)
sipUri.postValue(conference.conferenceAddress?.asStringUriOnly())
subject.postValue(confSubject)
if (conference.state == Conference.State.Created) {
computeParticipants(false)
if (conference.participantList.size >= 1) { // we do not count
Log.i("$TAG Joined conference already has at least another participant")
firstParticipantOtherThanOurselvesJoinedEvent.postValue(Event(true))
}
}
val currentLayout = getCurrentLayout(call)
conferenceLayout.postValue(currentLayout)
if (currentLayout == GRID_LAYOUT && screenSharing) {
Log.w(
"$TAG Conference has a participant sharing its screen, changing layout from mosaic to active speaker"
)
setNewLayout(ACTIVE_SPEAKER_LAYOUT)
} else if (currentLayout == AUDIO_ONLY_LAYOUT) {
val defaultLayout = call.core.defaultConferenceLayout.toInt()
if (defaultLayout == Conference.Layout.ActiveSpeaker.toInt()) {
Log.w("$TAG Joined conference in audio only layout, switching to active speaker layout")
setNewLayout(ACTIVE_SPEAKER_LAYOUT)
} else {
Log.w("$TAG Joined conference in audio only layout, switching to grid layout")
setNewLayout(GRID_LAYOUT)
}
}
}
@UiThread
fun toggleFullScreen() {
if (fullScreenMode.value == true) {
// Always allow to switch off full screen mode
fullScreenMode.value = false
return
}
if (conferenceLayout.value == AUDIO_ONLY_LAYOUT) {
// Do not allow turning full screen on for audio only conference
return
}
if (isMeParticipantSendingVideo.value == false && participants.value.orEmpty().size == 1) {
// Do not allow turning full screen on if we're alone and not sending our video
return
}
fullScreenMode.value = true
}
@WorkerThread
fun localVideoStreamToggled(enabled: Boolean) {
isMeParticipantSendingVideo.postValue(enabled)
Log.i("$TAG We [${if (enabled) "are" else "aren't"}] sending video")
}
@UiThread
fun goToConversation() {
if (::conference.isInitialized) {
coreContext.postOnCoreThread { core ->
Log.i("$TAG Navigating to conference's conversation")
val chatRoom = conference.chatRoom
if (chatRoom != null) {
goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom)))
} else {
Log.e(
"$TAG No chat room available for current conference [${conference.conferenceAddress?.asStringUriOnly()}]"
)
}
}
}
}
@UiThread
fun showLayoutMenu() {
showLayoutMenuEvent.value = Event(true)
}
@UiThread
fun changeLayout(newLayout: Int) {
coreContext.postOnCoreThread {
setNewLayout(newLayout)
}
}
@UiThread
fun inviteSipUrisIntoConference(uris: List<String>) {
if (::conference.isInitialized) {
coreContext.postOnCoreThread { core ->
val addresses = arrayListOf<Address>()
for (uri in uris) {
val address = core.interpretUrl(uri, false)
if (address != null) {
addresses.add(address)
Log.i("$TAG Address [${address.asStringUriOnly()}] will be added to conference")
} else {
Log.e(
"$TAG Failed to parse SIP URI [$uri] into address, can't add it to the conference!"
)
showRedToast(
R.string.conference_failed_to_add_participant_invalid_address_toast,
R.drawable.warning_circle
)
}
}
val addressesArray = arrayOfNulls<Address>(addresses.size)
addresses.toArray(addressesArray)
Log.i("$TAG Trying to add [${addressesArray.size}] new participant(s) into conference")
conference.addParticipants(addressesArray)
}
}
}
@WorkerThread
fun kickParticipant(participant: Participant) {
if (::conference.isInitialized) {
coreContext.postOnCoreThread {
Log.i(
"$TAG Kicking participant [${participant.address.asStringUriOnly()}] out of conference"
)
conference.removeParticipant(participant)
}
}
}
@WorkerThread
fun setNewLayout(newLayout: Int) {
if (::conference.isInitialized) {
val call = conference.call
if (call != null) {
val params = call.core.createCallParams(call)
if (params != null) {
val currentLayout = getCurrentLayout(call)
if (currentLayout != newLayout) {
when (newLayout) {
AUDIO_ONLY_LAYOUT -> {
Log.i("$TAG Changing conference layout to [Audio Only]")
params.isVideoEnabled = false
}
ACTIVE_SPEAKER_LAYOUT -> {
Log.i("$TAG Changing conference layout to [Active Speaker]")
params.conferenceVideoLayout = Conference.Layout.ActiveSpeaker
}
GRID_LAYOUT -> {
Log.i("$TAG Changing conference layout to [Grid]")
params.conferenceVideoLayout = Conference.Layout.Grid
}
}
Log.i("$TAG Clearing participant devices window IDs")
participantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceModel::clearWindowId)
if (currentLayout == AUDIO_ONLY_LAYOUT) {
// Previous layout was audio only, make sure video isn't sent without user consent when switching layout
Log.i(
"$TAG Previous layout was [Audio Only], enabling video but in receive only direction"
)
params.isVideoEnabled = true
params.videoDirection = MediaDirection.RecvOnly
}
Log.i("$TAG Updating conference's call params")
call.update(params)
conferenceLayout.postValue(newLayout)
} else {
Log.w(
"$TAG The conference is already using selected layout, aborting layout change"
)
}
} else {
Log.e("$TAG Failed to create call params, aborting layout change")
}
} else {
Log.e("$TAG Failed to get call from conference, aborting layout change")
}
}
}
@WorkerThread
private fun getCurrentLayout(call: Call): Int {
// DO NOT USE call.currentParams, information won't be reliable !
return if (!call.params.isVideoEnabled) {
Log.i("$TAG Current conference layout is [Audio Only]")
AUDIO_ONLY_LAYOUT
} else {
when (val layout = call.params.conferenceVideoLayout) {
Conference.Layout.Grid -> {
Log.i("$TAG Current conference layout is [Grid]")
GRID_LAYOUT
}
Conference.Layout.ActiveSpeaker -> {
Log.i("$TAG Current conference layout is [Active Speaker]")
ACTIVE_SPEAKER_LAYOUT
}
else -> {
Log.e("$TAG Unexpected conference layout value [$layout]")
-2
}
}
}
}
@WorkerThread
private fun computeParticipants(skipDevices: Boolean) {
if (!skipDevices) {
participantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceModel::destroy)
}
val participantsList = arrayListOf<ConferenceParticipantModel>()
val devicesList = arrayListOf<ConferenceParticipantDeviceModel>()
val conferenceParticipants = conference.participantList
Log.i("$TAG [${conferenceParticipants.size}] participant in conference")
val meParticipant = conference.me
val admin = meParticipant.isAdmin
isMeAdmin.postValue(admin)
if (admin) {
Log.i("$TAG We are admin of that conference!")
}
var activeSpeakerParticipantDeviceFound = false
for (participant in conferenceParticipants) {
val devices = participant.devices
val role = participant.role
Log.i(
"$TAG Participant [${participant.address.asStringUriOnly()}] has [${devices.size}] devices and role [${role.name}]"
)
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(
participant.address
)
val participantModel = ConferenceParticipantModel(
participant,
avatarModel,
admin,
false,
{ participant -> // Remove from conference
removeParticipantEvent.postValue(
Event(Pair(avatarModel.name.value.orEmpty(), participant))
)
},
{ participant, setAdmin -> // Change admin status
conference.setParticipantAdminStatus(participant, setAdmin)
}
)
participantsList.add(participantModel)
if (role == Participant.Role.Listener) {
continue
}
if (!skipDevices) {
for (device in devices) {
val model = ConferenceParticipantDeviceModel(device)
devicesList.add(model)
if (device == conference.activeSpeakerParticipantDevice) {
Log.i("$TAG Using participant is [${model.name}] as current active speaker")
model.isActiveSpeaker.postValue(true)
activeSpeaker.postValue(model)
activeSpeakerParticipantDeviceFound = true
}
if (device == conference.screenSharingParticipantDevice) {
Log.i("$TAG Using participant is [${model.name}] as current screen sharing sender")
screenSharingParticipantName.postValue(model.name)
}
}
}
}
if (skipDevices) {
Log.i(
"$TAG [${participantsList.size}] participants will be displayed (not counting ourselves), devices were skipped"
)
} else {
Log.i(
"$TAG [${devicesList.size}] participant devices for [${participantsList.size}] participants will be displayed (not counting ourselves)"
)
}
val meAvatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(
meParticipant.address
)
val meParticipantModel = ConferenceParticipantModel(
meParticipant,
meAvatarModel,
admin,
true,
null,
null
)
participantsList.add(meParticipantModel)
val ourDevices = conference.me.devices
Log.i("$TAG We have [${ourDevices.size}] devices")
for (device in ourDevices) {
if (!skipDevices) {
val model = ConferenceParticipantDeviceModel(device, true)
devicesList.add(model)
if (device == conference.activeSpeakerParticipantDevice) {
Log.i("$TAG Using our device [${model.name}] as current active speaker")
model.isActiveSpeaker.postValue(true)
activeSpeaker.postValue(model)
activeSpeakerParticipantDeviceFound = true
}
}
val direction = device.getStreamCapability(StreamType.Video)
val sendingVideo = direction == MediaDirection.SendRecv || direction == MediaDirection.SendOnly
localVideoStreamToggled(sendingVideo)
}
if (!activeSpeakerParticipantDeviceFound && devicesList.isNotEmpty()) {
val first = devicesList.first()
Log.w(
"$TAG Failed to find current active speaker participant device, using first one [${first.name}]"
)
first.isActiveSpeaker.postValue(true)
activeSpeaker.postValue(first)
}
participants.postValue(sortParticipantList(participantsList))
if (!skipDevices) {
checkIfTooManyParticipantDevicesForGridLayout(devicesList)
if (participantsList.size == 1) {
Log.i("$TAG We are alone in that conference, not posting devices list for now")
participantDevices.postValue(arrayListOf())
} else {
participantDevices.postValue(sortParticipantDevicesList(devicesList))
}
participantsLabel.postValue(
AppUtils.getStringWithPlural(
R.plurals.conference_participants_list_title,
participantsList.size,
"${participantsList.size}"
)
)
}
}
@WorkerThread
private fun sortParticipantList(devices: List<ConferenceParticipantModel>): ArrayList<ConferenceParticipantModel> {
val sortedList = arrayListOf<ConferenceParticipantModel>()
sortedList.addAll(devices)
val meModel = sortedList.find {
it.isMyself
}
if (meModel != null) {
val index = sortedList.indexOf(meModel)
val expectedIndex = 0
if (index != expectedIndex) {
Log.i(
"$TAG Me participant model is at index $index, moving it to index $expectedIndex"
)
sortedList.removeAt(index)
sortedList.add(expectedIndex, meModel)
}
}
return sortedList
}
@WorkerThread
private fun sortParticipantDevicesList(devices: List<ConferenceParticipantDeviceModel>): ArrayList<ConferenceParticipantDeviceModel> {
val sortedList = arrayListOf<ConferenceParticipantDeviceModel>()
sortedList.addAll(devices)
val meDeviceModel = sortedList.find {
it.isMe
}
if (meDeviceModel != null) {
val index = sortedList.indexOf(meDeviceModel)
val expectedIndex = if (conferenceLayout.value == ACTIVE_SPEAKER_LAYOUT) {
Log.i(
"$TAG Current conference layout is [Active Speaker], expecting our device to be at the beginning of the list"
)
0
} else {
Log.i(
"$TAG Current conference layout isn't [Active Speaker], expecting our device to be at the end of the list"
)
sortedList.size - 1
}
if (index != expectedIndex) {
Log.i(
"$TAG Me device model is at index $index, moving it to index $expectedIndex"
)
sortedList.removeAt(index)
sortedList.add(expectedIndex, meDeviceModel)
}
}
return sortedList
}
@WorkerThread
private fun addParticipant(participant: Participant) {
if (::conference.isInitialized) {
val list = arrayListOf<ConferenceParticipantModel>()
list.addAll(participants.value.orEmpty())
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(
participant.address
)
val newModel = ConferenceParticipantModel(
participant,
avatarModel,
isMeAdmin.value == true,
false,
{ participant -> // Remove from conference
removeParticipantEvent.postValue(
Event(Pair(avatarModel.name.value.orEmpty(), participant))
)
},
{ participant, setAdmin -> // Change admin status
conference.setParticipantAdminStatus(participant, setAdmin)
}
)
list.add(newModel)
participants.postValue(sortParticipantList(list))
participantsLabel.postValue(
AppUtils.getStringWithPlural(
R.plurals.conference_participants_list_title,
list.size,
"${list.size}"
)
)
}
}
@WorkerThread
private fun addParticipantDevice(participantDevice: ParticipantDevice) {
val list = arrayListOf<ConferenceParticipantDeviceModel>()
list.addAll(participantDevices.value.orEmpty())
val newModel = ConferenceParticipantDeviceModel(participantDevice)
list.add(newModel)
checkIfTooManyParticipantDevicesForGridLayout(list)
participantDevices.postValue(sortParticipantDevicesList(list))
}
@WorkerThread
private fun removeParticipant(participant: Participant) {
val list = arrayListOf<ConferenceParticipantModel>()
list.addAll(participants.value.orEmpty())
val toRemove = list.find {
participant.address.weakEqual(it.participant.address)
}
if (toRemove != null) {
list.remove(toRemove)
}
participants.postValue(list)
participantsLabel.postValue(
AppUtils.getStringWithPlural(
R.plurals.conference_participants_list_title,
list.size,
"${list.size}"
)
)
}
@WorkerThread
private fun removeParticipantDevice(participantDevice: ParticipantDevice) {
val list = arrayListOf<ConferenceParticipantDeviceModel>()
list.addAll(participantDevices.value.orEmpty())
val toRemove = list.find {
participantDevice.address.weakEqual(it.device.address)
}
if (toRemove != null) {
toRemove.destroy()
list.remove(toRemove)
}
participantDevices.postValue(list)
}
@WorkerThread
fun togglePause() {
if (::conference.isInitialized) {
if (conference.isIn) {
Log.i("$TAG Temporary leaving conference")
conference.leave()
isPaused.postValue(true)
} else {
Log.i("$TAG Entering conference again")
conference.enter()
isPaused.postValue(false)
}
}
}
@WorkerThread
private fun checkIfTooManyParticipantDevicesForGridLayout(
list: ArrayList<ConferenceParticipantDeviceModel>
) {
if (list.size > GridBoxLayout.MAX_CHILD && conferenceLayout.value == GRID_LAYOUT) {
Log.w(
"$TAG Too many participant devices for grid layout, switching to active speaker layout"
)
setNewLayout(ACTIVE_SPEAKER_LAYOUT)
showRedToast(R.string.conference_too_many_participants_for_mosaic_layout_toast, R.drawable.warning_circle)
}
}
}

View file

@ -0,0 +1,504 @@
/*
* Copyright (c) 2010-2023 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.ui.call.fragment
import android.app.Dialog
import android.os.Bundle
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.activity.OnBackPressedCallback
import androidx.annotation.UiThread
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.doOnLayout
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import androidx.window.layout.FoldingFeature
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.CallActiveFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.call.CallActivity
import org.linphone.ui.call.model.ZrtpAlertDialogModel
import org.linphone.ui.call.model.ZrtpSasConfirmationDialogModel
import org.linphone.ui.call.viewmodel.CallsViewModel
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.utils.DialogUtils
import org.linphone.utils.Event
import org.linphone.utils.addCharacterAtPosition
import org.linphone.utils.removeCharacterAtPosition
import org.linphone.utils.startAnimatedDrawable
@UiThread
class ActiveCallFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Active Call Fragment]"
}
private lateinit var binding: CallActiveFragmentBinding
private lateinit var callViewModel: CurrentCallViewModel
private lateinit var callsViewModel: CallsViewModel
private var zrtpSasDialog: Dialog? = null
private val actionsBottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
val drawable = AnimatedVectorDrawableCompat.create(
requireContext(),
R.drawable.animated_handle_to_caret
)
binding.bottomBar.mainActions.callActionsHandle.setImageDrawable(drawable)
} else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
val drawable = AnimatedVectorDrawableCompat.create(
requireContext(),
R.drawable.animated_caret_to_handle
)
binding.bottomBar.mainActions.callActionsHandle.setImageDrawable(drawable)
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) { }
}
private val backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val actionsBottomSheetBehavior = BottomSheetBehavior.from(binding.bottomBar.root)
if (actionsBottomSheetBehavior.state != BottomSheetBehavior.STATE_COLLAPSED) {
actionsBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
return
}
val numpadBottomSheetBehavior = BottomSheetBehavior.from(binding.callNumpad.root)
if (numpadBottomSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN) {
numpadBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
return
}
val callStatsBottomSheetBehavior = BottomSheetBehavior.from(binding.callStats.root)
if (callStatsBottomSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN) {
callStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
return
}
val callMediaEncryptionStatsBottomSheetBehavior = BottomSheetBehavior.from(
binding.callMediaEncryptionStats.root
)
if (callMediaEncryptionStatsBottomSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN) {
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
return
}
Log.i("$TAG Back gesture/click detected, no bottom sheet is expanded, going back")
isEnabled = false
try {
Log.i("$TAG Back gesture detected, going to MainActivity")
(requireActivity() as CallActivity).goToMainActivity()
} catch (ise: IllegalStateException) {
Log.w(
"$TAG Can't go back: $ise"
)
}
}
}
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
return when (findNavController().currentDestination?.id) {
R.id.newCallFragment, R.id.callsListFragment, R.id.transferCallFragment, R.id.inCallConversationFragment -> {
// Holds fragment in place while new fragment slides over it
AnimationUtils.loadAnimation(activity, R.anim.hold)
}
else -> {
super.onCreateAnimation(transit, enter, nextAnim)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallActiveFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
observeToastEvents(callViewModel)
callsViewModel = requireActivity().run {
ViewModelProvider(this)[CallsViewModel::class.java]
}
observeToastEvents(callsViewModel)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
binding.callsViewModel = callsViewModel
binding.numpadModel = callViewModel.numpadModel
val actionsBottomSheetBehavior = BottomSheetBehavior.from(binding.bottomBar.root)
actionsBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
actionsBottomSheetBehavior.addBottomSheetCallback(actionsBottomSheetCallback)
val numpadBottomSheetBehavior = BottomSheetBehavior.from(binding.callNumpad.root)
numpadBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
numpadBottomSheetBehavior.skipCollapsed = true
val callStatsBottomSheetBehavior = BottomSheetBehavior.from(binding.callStats.root)
callStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callStatsBottomSheetBehavior.skipCollapsed = true
val callMediaEncryptionStatsBottomSheetBehavior = BottomSheetBehavior.from(
binding.callMediaEncryptionStats.root
)
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callMediaEncryptionStatsBottomSheetBehavior.skipCollapsed = true
binding.setBackClickListener {
(requireActivity() as CallActivity).goToMainActivity()
}
binding.setTransferCallClickListener {
if (findNavController().currentDestination?.id == R.id.activeCallFragment) {
val action =
ActiveCallFragmentDirections.actionActiveCallFragmentToTransferCallFragment()
findNavController().navigate(action)
}
}
binding.setNewCallClickListener {
if (findNavController().currentDestination?.id == R.id.activeCallFragment) {
val action =
ActiveCallFragmentDirections.actionActiveCallFragmentToNewCallFragment()
findNavController().navigate(action)
}
}
binding.setCallsListClickListener {
if (findNavController().currentDestination?.id == R.id.activeCallFragment) {
val action =
ActiveCallFragmentDirections.actionActiveCallFragmentToCallsListFragment()
findNavController().navigate(action)
}
}
binding.setCallStatisticsClickListener {
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
binding.setCallMediaEncryptionStatisticsClickListener {
callStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
sharedViewModel.foldingState.observe(viewLifecycleOwner) { feature ->
updateHingeRelatedConstraints(feature)
}
callViewModel.fullScreenMode.observe(viewLifecycleOwner) { hide ->
Log.i("$TAG Switching full screen mode to [${if (hide) "ON" else "OFF"}]")
sharedViewModel.toggleFullScreenEvent.value = Event(hide)
numpadBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}
callViewModel.showZrtpSasDialogEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
showZrtpSasValidationDialog(pair.first, pair.second, false)
}
}
callViewModel.showZrtpSasCacheMismatchDialogEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
showZrtpSasValidationDialog(pair.first, pair.second, true)
}
}
callViewModel.zrtpAuthTokenVerifiedEvent.observe(viewLifecycleOwner) {
it.consume { verified ->
if (verified) {
(requireActivity() as GenericActivity).showBlueToast(
getString(R.string.call_can_be_trusted_toast),
R.drawable.trusted,
doNotTint = true
)
} else {
// Only allow "trying again" once
showZrtpAlertDialog()
}
}
}
callViewModel.callDuration.observe(viewLifecycleOwner) { duration ->
binding.chronometer.base = SystemClock.elapsedRealtime() - (1000 * duration)
binding.chronometer.start()
}
callViewModel.toggleExtraActionsBottomSheetEvent.observe(viewLifecycleOwner) {
it.consume {
val state = actionsBottomSheetBehavior.state
if (state == BottomSheetBehavior.STATE_COLLAPSED) {
val drawable = AnimatedVectorDrawableCompat.create(
requireContext(),
R.drawable.animated_caret_to_handle
)
binding.bottomBar.mainActions.callActionsHandle.setImageDrawable(drawable)
binding.bottomBar.mainActions.callActionsHandle.startAnimatedDrawable()
actionsBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} else if (state == BottomSheetBehavior.STATE_EXPANDED) {
val drawable = AnimatedVectorDrawableCompat.create(
requireContext(),
R.drawable.animated_handle_to_caret
)
binding.bottomBar.mainActions.callActionsHandle.setImageDrawable(drawable)
binding.bottomBar.mainActions.callActionsHandle.startAnimatedDrawable()
actionsBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
}
callViewModel.showNumpadBottomSheetEvent.observe(viewLifecycleOwner) {
it.consume {
actionsBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
numpadBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
callViewModel.removedCharacterAtCurrentPositionEvent.observe(viewLifecycleOwner) {
it.consume {
binding.callNumpad.digitsHistory.removeCharacterAtPosition()
}
}
callViewModel.appendDigitToSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { digit ->
binding.callNumpad.digitsHistory.addCharacterAtPosition(digit)
}
}
callViewModel.isRemoteRecordingEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val isRemoteRecording = pair.first
val displayName = pair.second
val toastTag = "REMOTE_RECORDING"
if (isRemoteRecording) {
Log.i("$TAG Showing [$displayName] is recording toast")
val message = getString(R.string.call_remote_is_recording, displayName)
(requireActivity() as GenericActivity).showPersistentRedToast(
message,
R.drawable.record_fill,
toastTag
)
} else {
Log.i("$TAG Removing [$displayName] is recording toast")
(requireActivity() as CallActivity).removePersistentRedToast(toastTag)
}
}
}
callViewModel.goToConferenceEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.activeCallFragment) {
Log.i("$TAG Going to conference fragment")
val action =
ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment()
findNavController().navigate(action)
}
}
}
callViewModel.isReceivingVideo.observe(viewLifecycleOwner) { receiving ->
if (!receiving && callViewModel.fullScreenMode.value == true) {
Log.i("$TAG We are no longer receiving video, leaving full screen mode")
callViewModel.fullScreenMode.value = false
}
}
callViewModel.isSendingVideo.observe(viewLifecycleOwner) { sending ->
coreContext.postOnCoreThread { core ->
core.nativePreviewWindowId = if (sending) {
Log.i("$TAG We are sending video, setting capture preview surface")
binding.localPreviewVideoSurface
} else {
Log.i("$TAG We are not sending video, clearing capture preview surface")
null
}
}
}
callViewModel.goToConversationEvent.observe(viewLifecycleOwner) {
it.consume { conversationId ->
if (findNavController().currentDestination?.id == R.id.activeCallFragment) {
Log.i(
"$TAG Display conversation with conversation ID [$conversationId]"
)
val action =
ActiveCallFragmentDirections.actionActiveCallFragmentToInCallConversationFragment(
conversationId
)
findNavController().navigate(action)
}
}
}
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
backPressedCallback
)
}
override fun onResume() {
super.onResume()
(binding.root as? ViewGroup)?.doOnLayout {
setupVideoPreview(binding.localPreviewVideoSurface)
}
coreContext.postOnCoreThread { core ->
Log.i("$TAG Fragment resuming, setting native video window ID")
core.nativeVideoWindowId = binding.remoteVideoSurface
// Need to be done manually
callViewModel.updateCallDuration()
}
if (callViewModel.isZrtpAlertDialogVisible) {
Log.i("$TAG Fragment resuming, showing ZRTP alert dialog")
showZrtpAlertDialog()
} else if (callViewModel.isZrtpDialogVisible) {
Log.i("$TAG Fragment resuming, showing ZRTP SAS validation dialog")
callViewModel.showZrtpSasDialogIfPossible()
}
}
override fun onPause() {
super.onPause()
zrtpSasDialog?.dismiss()
zrtpSasDialog = null
cleanVideoPreview(binding.localPreviewVideoSurface)
}
private fun updateHingeRelatedConstraints(feature: FoldingFeature) {
Log.i("$TAG Updating constraint layout hinges: $feature")
val constraintLayout = binding.constraintLayout
val set = ConstraintSet()
set.clone(constraintLayout)
if (feature.isSeparating && feature.state == FoldingFeature.State.HALF_OPENED && feature.orientation == FoldingFeature.Orientation.HORIZONTAL) {
set.setGuidelinePercent(R.id.hinge_top, 0.5f)
set.setGuidelinePercent(R.id.hinge_bottom, 0.5f)
callViewModel.halfOpenedFolded.value = true
} else {
set.setGuidelinePercent(R.id.hinge_top, 0f)
set.setGuidelinePercent(R.id.hinge_bottom, 1f)
callViewModel.halfOpenedFolded.value = false
}
set.applyTo(constraintLayout)
}
private fun showZrtpSasValidationDialog(
authTokenToRead: String,
authTokensToListen: List<String>,
cacheMismatch: Boolean
) {
if (zrtpSasDialog != null) {
zrtpSasDialog?.dismiss()
}
val model = ZrtpSasConfirmationDialogModel(
authTokenToRead,
authTokensToListen,
cacheMismatch
)
val dialog = DialogUtils.getZrtpSasConfirmationDialog(requireActivity(), model)
model.skipEvent.observe(viewLifecycleOwner) { event ->
event.consume {
callViewModel.skipZrtpSas()
dialog.dismiss()
callViewModel.isZrtpDialogVisible = false
}
}
model.authTokenClickedEvent.observe(viewLifecycleOwner) { event ->
event.consume { authToken ->
callViewModel.updateZrtpSas(authToken)
dialog.dismiss()
callViewModel.isZrtpDialogVisible = false
}
}
dialog.show()
zrtpSasDialog = dialog
callViewModel.isZrtpDialogVisible = true
}
private fun showZrtpAlertDialog() {
if (zrtpSasDialog != null) {
zrtpSasDialog?.dismiss()
}
val model = ZrtpAlertDialogModel(false)
val dialog = DialogUtils.getZrtpAlertDialog(requireActivity(), model)
model.tryAgainEvent.observe(viewLifecycleOwner) { event ->
event.consume {
callViewModel.showZrtpSasDialogIfPossible()
dialog.dismiss()
callViewModel.isZrtpAlertDialogVisible = false
}
}
model.hangUpEvent.observe(viewLifecycleOwner) { event ->
event.consume {
callViewModel.hangUp()
dialog.dismiss()
callViewModel.isZrtpAlertDialogVisible = false
}
}
dialog.show()
zrtpSasDialog = dialog
callViewModel.isZrtpDialogVisible = false
callViewModel.isZrtpAlertDialogVisible = true
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2010-2023 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.ui.call.fragment
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.databinding.CallAudioDevicesBottomSheetBinding
import org.linphone.ui.call.model.AudioDeviceModel
@UiThread
class AudioDevicesMenuDialogFragment(
private val devicesList: List<AudioDeviceModel>,
private val onDismiss: (() -> Unit)? = null
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "AudioDevicesMenuDialogFragment"
}
override fun onCancel(dialog: DialogInterface) {
onDismiss?.invoke()
super.onCancel(dialog)
}
override fun onDismiss(dialog: DialogInterface) {
onDismiss?.invoke()
super.onDismiss(dialog)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
// Makes sure all menu entries are visible,
// required for landscape mode (otherwise only first item is visible)
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = CallAudioDevicesBottomSheetBinding.inflate(layoutInflater)
view.lifecycleOwner = viewLifecycleOwner
for (device in devicesList) {
device.dismissDialog = {
dismiss()
}
}
view.devices = devicesList
return view.root
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2010-2023 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.ui.call.fragment
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.databinding.CallsListLongPressMenuBinding
import org.linphone.ui.call.model.CallModel
class CallMenuDialogFragment(
private val callModel: CallModel,
private val onDismiss: (() -> Unit)? = null
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "CallMenuDialogFragment"
}
override fun onCancel(dialog: DialogInterface) {
onDismiss?.invoke()
super.onCancel(dialog)
}
override fun onDismiss(dialog: DialogInterface) {
onDismiss?.invoke()
super.onDismiss(dialog)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
// Makes sure all menu entries are visible,
// required for landscape mode (otherwise only first item is visible)
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = CallsListLongPressMenuBinding.inflate(layoutInflater)
view.lifecycleOwner = viewLifecycleOwner
view.setHangUpClickListener {
callModel.hangUp()
dismiss()
}
view.setPauseResumeClickListener {
callModel.togglePauseResume()
dismiss()
}
view.isPaused = callModel.isPaused.value == true
return view.root
}
}

View file

@ -0,0 +1,179 @@
/*
* Copyright (c) 2010-2023 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.ui.call.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.doOnLayout
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.tools.Log
import org.linphone.databinding.CallsListFragmentBinding
import org.linphone.ui.call.adapter.CallsListAdapter
import org.linphone.ui.call.viewmodel.CallsViewModel
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.utils.ConfirmationDialogModel
import org.linphone.utils.DialogUtils
class CallsListFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Calls List Fragment]"
}
private lateinit var binding: CallsListFragmentBinding
private lateinit var viewModel: CallsViewModel
private lateinit var callViewModel: CurrentCallViewModel
private lateinit var adapter: CallsListAdapter
private var bottomSheetDialog: BottomSheetDialogFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = CallsListAdapter()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallsListFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = requireActivity().run {
ViewModelProvider(this)[CallsViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
observeToastEvents(callViewModel)
binding.callsList.setHasFixedSize(true)
binding.callsList.layoutManager = LinearLayoutManager(requireContext())
adapter.callLongClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
val modalBottomSheet = CallMenuDialogFragment(model) {
// onDismiss
adapter.resetSelection()
}
modalBottomSheet.show(parentFragmentManager, CallMenuDialogFragment.TAG)
bottomSheetDialog = modalBottomSheet
}
}
adapter.callClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
coreContext.postOnCoreThread {
model.togglePauseResume()
}
}
}
binding.setBackClickListener {
findNavController().popBackStack()
}
binding.setMergeCallsClickListener {
showMergeCallsIntoConferenceConfirmationDialog()
}
callViewModel.isSendingVideo.observe(viewLifecycleOwner) { sending ->
coreContext.postOnCoreThread { core ->
core.nativePreviewWindowId = if (sending) {
Log.i("$TAG We are sending video, setting capture preview surface")
binding.localPreviewVideoSurface
} else {
Log.i("$TAG We are not sending video, clearing capture preview surface")
null
}
}
}
viewModel.calls.observe(viewLifecycleOwner) {
Log.i("$TAG Calls list updated with [${it.size}] items")
adapter.submitList(it)
// Wait for adapter to have items before setting it in the RecyclerView,
// otherwise scroll position isn't retained
if (binding.callsList.adapter != adapter) {
binding.callsList.adapter = adapter
}
}
}
override fun onResume() {
super.onResume()
(binding.root as? ViewGroup)?.doOnLayout {
setupVideoPreview(binding.localPreviewVideoSurface)
}
}
override fun onPause() {
super.onPause()
bottomSheetDialog?.dismiss()
bottomSheetDialog = null
cleanVideoPreview(binding.localPreviewVideoSurface)
}
private fun showMergeCallsIntoConferenceConfirmationDialog() {
val model = ConfirmationDialogModel()
val dialog = DialogUtils.getConfirmMergeCallsDialog(
requireActivity(),
model
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
viewModel.mergeCallsIntoConference()
dialog.dismiss()
}
}
dialog.show()
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2010-2024 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.ui.call.fragment
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.navigation.fragment.findNavController
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.ui.fileviewer.FileViewerActivity
import org.linphone.ui.fileviewer.MediaViewerActivity
import org.linphone.ui.main.chat.fragment.ConversationFragment
class ConversationFragment : ConversationFragment() {
companion object {
private const val TAG = "[In-call Conversation Fragment]"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.i("$TAG Creating an in-call ConversationFragment")
sendMessageViewModel.isCallConversation.value = true
viewModel.isCallConversation.value = true
binding.setBackClickListener {
findNavController().popBackStack()
}
sharedViewModel.displayFileEvent.observe(viewLifecycleOwner) {
it.consume { bundle ->
if (findNavController().currentDestination?.id == R.id.inCallConversationFragment) {
val path = bundle.getString("path", "")
val isMedia = bundle.getBoolean("isMedia", false)
if (path.isEmpty()) {
Log.e("$TAG Can't navigate to file viewer for empty path!")
return@consume
}
Log.i(
"$TAG Navigating to [${if (isMedia) "media" else "file"}] viewer fragment with path [$path]"
)
if (isMedia) {
val intent = Intent(requireActivity(), MediaViewerActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
} else {
val intent = Intent(requireActivity(), FileViewerActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
}
}
}
}
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2010-2023 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.ui.call.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.annotation.UiThread
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.core.tools.Log
import org.linphone.databinding.CallEndedFragmentBinding
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
@UiThread
class EndedCallFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Ended Call Fragment]"
private const val LOCALLY_TERMINATED_CALL_TIMEOUT: Long = 1000
private const val REMOTELY_TERMINATED_CALL_TIMEOUT: Long = 2000
}
private lateinit var binding: CallEndedFragmentBinding
private lateinit var callViewModel: CurrentCallViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallEndedFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Disable back gesture / button
requireActivity().onBackPressedDispatcher.addCallback { }
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
observeToastEvents(callViewModel)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
Log.i("$TAG Showing ended call fragment")
}
override fun onResume() {
super.onResume()
lifecycleScope.launch {
withContext(Dispatchers.IO) {
if (callViewModel.terminatedByUser) {
Log.i(
"$TAG Call terminated by user, waiting 1 second before finishing activity"
)
delay(LOCALLY_TERMINATED_CALL_TIMEOUT)
} else {
Log.i(
"$TAG Call terminated by remote end, waiting 2 seconds before finishing activity"
)
delay(REMOTELY_TERMINATED_CALL_TIMEOUT)
}
withContext(Dispatchers.Main) {
Log.i("$TAG Finishing activity")
requireActivity().finish()
}
}
}
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2010-2023 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.ui.call.fragment
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import androidx.annotation.UiThread
import androidx.lifecycle.ViewModelProvider
import org.linphone.core.tools.Log
import org.linphone.ui.GenericFragment
import org.linphone.ui.call.view.RoundCornersTextureView
import org.linphone.ui.call.viewmodel.SharedCallViewModel
@UiThread
abstract class GenericCallFragment : GenericFragment() {
companion object {
private const val TAG = "[Generic Call Fragment]"
}
protected lateinit var sharedViewModel: SharedCallViewModel
// For moving video preview purposes
private val videoPreviewTouchListener = View.OnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
sharedViewModel.videoPreviewX = view.x - event.rawX
sharedViewModel.videoPreviewY = view.y - event.rawY
true
}
MotionEvent.ACTION_UP -> {
sharedViewModel.videoPreviewX = view.x
sharedViewModel.videoPreviewY = view.y
true
}
MotionEvent.ACTION_MOVE -> {
view.animate()
.x(event.rawX + sharedViewModel.videoPreviewX)
.y(event.rawY + sharedViewModel.videoPreviewY)
.setDuration(0)
.start()
true
}
else -> {
view.performClick()
false
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel = requireActivity().run {
ViewModelProvider(this)[SharedCallViewModel::class.java]
}
}
@SuppressLint("ClickableViewAccessibility")
protected fun setupVideoPreview(localPreviewVideoSurface: RoundCornersTextureView) {
if (requireActivity().isInPictureInPictureMode) {
Log.i("$TAG Activity is in PiP mode, do not move video preview")
return
}
// To restore video preview position if possible
if (sharedViewModel.videoPreviewX != 0f && sharedViewModel.videoPreviewY != 0f) {
Log.i("$TAG Restoring video preview position with position X [${sharedViewModel.videoPreviewX}] and Y [${sharedViewModel.videoPreviewY}]")
localPreviewVideoSurface.x = sharedViewModel.videoPreviewX
localPreviewVideoSurface.y = sharedViewModel.videoPreviewY
}
localPreviewVideoSurface.setOnTouchListener(videoPreviewTouchListener)
}
@SuppressLint("ClickableViewAccessibility")
protected fun cleanVideoPreview(localPreviewVideoSurface: RoundCornersTextureView) {
localPreviewVideoSurface.setOnTouchListener(null)
}
}

View file

@ -0,0 +1,141 @@
/*
* Copyright (c) 2010-2023 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.ui.call.fragment
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.lifecycle.ViewModelProvider
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.CallIncomingFragmentBinding
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.utils.AppUtils
import kotlin.math.max
import kotlin.math.min
@UiThread
class IncomingCallFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Incoming Call Fragment]"
}
private lateinit var binding: CallIncomingFragmentBinding
private lateinit var callViewModel: CurrentCallViewModel
private val marginSize = AppUtils.getDimension(R.dimen.sliding_accept_decline_call_margin)
private val areaSize = AppUtils.getDimension(R.dimen.call_button_size) + marginSize
private var initialX = 0f
private var slidingButtonX = 0f
private val slidingButtonTouchListener = View.OnTouchListener { view, event ->
val width = binding.bottomBar.lockedScreenBottomBar.root.width.toFloat()
val aboveAnswer = view.x + view.width > width - areaSize
val aboveDecline = view.x < areaSize
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (initialX == 0f) {
initialX = view.x
}
slidingButtonX = view.x - event.rawX
true
}
MotionEvent.ACTION_UP -> {
if (aboveAnswer) {
// Accept
callViewModel.answer()
} else if (aboveDecline) {
// Decline
callViewModel.hangUp()
} else {
// Animate going back to initial position
view.animate()
.x(initialX)
.setDuration(500)
.start()
}
true
}
MotionEvent.ACTION_MOVE -> {
view.animate()
.x(min(max(marginSize, event.rawX + slidingButtonX), width - view.width - marginSize))
.setDuration(0)
.start()
true
}
else -> {
view.performClick()
false
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallIncomingFragmentBinding.inflate(layoutInflater)
return binding.root
}
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
observeToastEvents(callViewModel)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
callViewModel.isIncomingEarlyMedia.observe(viewLifecycleOwner) { earlyMedia ->
if (earlyMedia) {
coreContext.postOnCoreThread { core ->
Log.i("$TAG Incoming early-media call, setting video surface")
core.nativeVideoWindowId = binding.remoteVideoSurface
}
}
}
binding.bottomBar.lockedScreenBottomBar.slidingButton.setOnTouchListener(slidingButtonTouchListener)
}
override fun onResume() {
super.onResume()
callViewModel.refreshKeyguardLockedStatus()
coreContext.notificationsManager.setIncomingCallFragmentCurrentlyDisplayed(true)
}
override fun onPause() {
coreContext.notificationsManager.setIncomingCallFragmentCurrentlyDisplayed(false)
super.onPause()
}
}

View file

@ -0,0 +1,289 @@
/*
* Copyright (c) 2010-2023 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.ui.call.fragment
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.view.doOnPreDraw
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers
import org.linphone.core.Address
import org.linphone.core.tools.Log
import org.linphone.databinding.StartCallFragmentBinding
import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.history.viewmodel.StartCallViewModel
import org.linphone.ui.main.model.ConversationContactOrSuggestionModel
import org.linphone.utils.DialogUtils
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.RecyclerViewHeaderDecoration
import org.linphone.utils.hideKeyboard
import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard
class NewCallFragment : GenericCallFragment() {
companion object {
private const val TAG = "[New Call Fragment]"
}
private lateinit var binding: StartCallFragmentBinding
private val viewModel: StartCallViewModel by navGraphViewModels(
R.id.call_nav_graph
)
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_COLLAPSED || newState == BottomSheetBehavior.STATE_HIDDEN) {
viewModel.isNumpadVisible.value = false
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) { }
}
private lateinit var adapter: ConversationsContactsAndSuggestionsListAdapter
private val listener = object : ContactNumberOrAddressClickListener {
@UiThread
override fun onClicked(model: ContactNumberOrAddressModel) {
val address = model.address
if (address != null) {
coreContext.postOnCoreThread {
action(address)
}
}
}
@UiThread
override fun onLongPress(model: ContactNumberOrAddressModel) {
}
}
private var numberOrAddressPickerDialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = ConversationsContactsAndSuggestionsListAdapter()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = StartCallFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
findNavController().popBackStack()
}
binding.setHideNumpadClickListener {
viewModel.hideNumpad()
}
binding.contactsAndSuggestionsList.setHasFixedSize(true)
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration)
adapter.onClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
startCall(model)
}
}
binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext())
viewModel.modelsList.observe(
viewLifecycleOwner
) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
val count = adapter.itemCount
adapter.submitList(it)
// Wait for adapter to have items before setting it in the RecyclerView,
// otherwise scroll position isn't retained
if (binding.contactsAndSuggestionsList.adapter != adapter) {
binding.contactsAndSuggestionsList.adapter = adapter
}
if (count == 0) {
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
}
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
val trimmed = filter.trim()
viewModel.applyFilter(trimmed)
}
viewModel.removedCharacterAtCurrentPositionEvent.observe(viewLifecycleOwner) {
it.consume {
val selectionStart = binding.searchBar.selectionStart
val selectionEnd = binding.searchBar.selectionEnd
if (selectionStart > 0) {
binding.searchBar.text =
binding.searchBar.text?.delete(
selectionStart - 1,
selectionEnd
)
binding.searchBar.setSelection(selectionStart - 1)
}
}
}
viewModel.appendDigitToSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { digit ->
val newValue = "${binding.searchBar.text}$digit"
binding.searchBar.setText(newValue)
binding.searchBar.setSelection(newValue.length)
}
}
viewModel.requestKeyboardVisibilityChangedEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.searchBar.showKeyboard()
} else {
binding.searchBar.requestFocus()
binding.searchBar.hideKeyboard()
}
}
}
val bottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback)
viewModel.isNumpadVisible.observe(viewLifecycleOwner) { visible ->
if (visible) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} else {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
binding.root.setKeyboardInsetListener { keyboardVisible ->
if (keyboardVisible) {
viewModel.isNumpadVisible.value = false
}
}
}
override fun onResume() {
super.onResume()
coreContext.postOnCoreThread {
if (corePreferences.automaticallyShowDialpad) {
viewModel.isNumpadVisible.postValue(true)
}
}
}
override fun onPause() {
super.onPause()
numberOrAddressPickerDialog?.dismiss()
numberOrAddressPickerDialog = null
}
private fun startCall(model: ConversationContactOrSuggestionModel) {
coreContext.postOnCoreThread {
val friend = model.friend
if (friend == null) {
action(model.address)
return@postOnCoreThread
}
val singleAvailableAddress = LinphoneUtils.getSingleAvailableAddressForFriend(friend)
if (singleAvailableAddress != null) {
Log.i(
"$TAG Only 1 SIP address or phone number found for contact [${friend.name}], starting call directly"
)
action(singleAvailableAddress)
} else {
val list = friend.getListOfSipAddressesAndPhoneNumbers(listener)
Log.i(
"$TAG [${list.size}] numbers or addresses found for contact [${friend.name}], showing selection dialog"
)
coreContext.postOnMainThread {
val numberOrAddressModel = NumberOrAddressPickerDialogModel(list)
val dialog =
DialogUtils.getNumberOrAddressPickerDialog(
requireActivity(),
numberOrAddressModel
)
numberOrAddressPickerDialog = dialog
numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event ->
event.consume {
dialog.dismiss()
}
}
dialog.show()
}
}
}
}
@WorkerThread
private fun action(address: Address) {
Log.i("$TAG Calling [${address.asStringUriOnly()}]")
coreContext.startAudioCall(address)
coreContext.postOnMainThread {
try {
findNavController().popBackStack()
} catch (ise: IllegalStateException) {
Log.e("$TAG Can't go back: $ise")
}
}
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2010-2023 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.ui.call.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.core.view.doOnLayout
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Call
import org.linphone.core.tools.Log
import org.linphone.databinding.CallOutgoingFragmentBinding
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.utils.LinphoneUtils
@UiThread
class OutgoingCallFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Outgoing Call Fragment]"
}
private lateinit var binding: CallOutgoingFragmentBinding
private lateinit var callViewModel: CurrentCallViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallOutgoingFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
observeToastEvents(callViewModel)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
binding.numpadModel = callViewModel.numpadModel
callViewModel.isOutgoingEarlyMedia.observe(viewLifecycleOwner) { earlyMedia ->
if (earlyMedia) {
coreContext.postOnCoreThread { core ->
val call = core.calls.find {
it.state == Call.State.OutgoingEarlyMedia
}
if (call != null && LinphoneUtils.isVideoEnabled(call)) {
Log.i("$TAG Outgoing early-media call with video, setting preview surface")
core.nativePreviewWindowId = binding.localPreviewVideoSurface
}
}
}
}
val numpadBottomSheetBehavior = BottomSheetBehavior.from(binding.callNumpad.root)
numpadBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
numpadBottomSheetBehavior.skipCollapsed = true
callViewModel.showNumpadBottomSheetEvent.observe(viewLifecycleOwner) {
it.consume {
numpadBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
}
override fun onResume() {
super.onResume()
(binding.root as? ViewGroup)?.doOnLayout {
setupVideoPreview(binding.localPreviewVideoSurface)
}
}
override fun onPause() {
super.onPause()
cleanVideoPreview(binding.localPreviewVideoSurface)
}
}

View file

@ -0,0 +1,346 @@
/*
* Copyright (c) 2010-2023 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.ui.call.fragment
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import kotlin.getValue
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.Address
import org.linphone.core.tools.Log
import org.linphone.databinding.CallTransferFragmentBinding
import org.linphone.ui.call.adapter.CallsListAdapter
import org.linphone.ui.call.model.CallModel
import org.linphone.ui.call.viewmodel.CallsViewModel
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter
import org.linphone.ui.main.history.viewmodel.StartCallViewModel
import org.linphone.utils.ConfirmationDialogModel
import org.linphone.utils.AppUtils
import org.linphone.utils.DialogUtils
import org.linphone.utils.RecyclerViewHeaderDecoration
import org.linphone.utils.hideKeyboard
import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard
@UiThread
class TransferCallFragment : GenericCallFragment() {
companion object {
private const val TAG = "[Transfer Call Fragment]"
}
private lateinit var binding: CallTransferFragmentBinding
private val viewModel: StartCallViewModel by navGraphViewModels(
R.id.call_nav_graph
)
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_COLLAPSED || newState == BottomSheetBehavior.STATE_HIDDEN) {
viewModel.isNumpadVisible.value = false
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) { }
}
private lateinit var callViewModel: CurrentCallViewModel
private lateinit var callsViewModel: CallsViewModel
private lateinit var callsAdapter: CallsListAdapter
private lateinit var contactsAdapter: ConversationsContactsAndSuggestionsListAdapter
private var numberOrAddressPickerDialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
callsAdapter = CallsListAdapter()
contactsAdapter = ConversationsContactsAndSuggestionsListAdapter()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CallTransferFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
observeToastEvents(callViewModel)
callsViewModel = requireActivity().run {
ViewModelProvider(this)[CallsViewModel::class.java]
}
observeToastEvents(callsViewModel)
binding.viewModel = viewModel
binding.callsViewModel = callsViewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
findNavController().popBackStack()
}
binding.setHideNumpadClickListener {
viewModel.hideNumpad()
}
binding.callsList.setHasFixedSize(true)
binding.contactsAndSuggestionsList.setHasFixedSize(true)
binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext())
binding.callsList.layoutManager = LinearLayoutManager(requireContext())
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), contactsAdapter)
binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration)
callsAdapter.callClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
showConfirmAttendedTransferDialog(model)
}
}
contactsAdapter.onClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
showConfirmBlindTransferDialog(model.address, model.name)
}
}
callsViewModel.callsExceptCurrentOne.observe(viewLifecycleOwner) {
Log.i("$TAG Calls list updated with [${it.size}] items")
callsAdapter.submitList(it)
// Wait for adapter to have items before setting it in the RecyclerView,
// otherwise scroll position isn't retained
if (binding.callsList.adapter != callsAdapter) {
binding.callsList.adapter = callsAdapter
}
}
viewModel.modelsList.observe(
viewLifecycleOwner
) {
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
val count = contactsAdapter.itemCount
contactsAdapter.submitList(it)
// Wait for adapter to have items before setting it in the RecyclerView,
// otherwise scroll position isn't retained
if (binding.contactsAndSuggestionsList.adapter != contactsAdapter) {
binding.contactsAndSuggestionsList.adapter = contactsAdapter
}
if (count == 0) {
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
}
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
val trimmed = filter.trim()
viewModel.applyFilter(trimmed)
}
viewModel.removedCharacterAtCurrentPositionEvent.observe(viewLifecycleOwner) {
it.consume {
val selectionStart = binding.searchBar.selectionStart
val selectionEnd = binding.searchBar.selectionEnd
if (selectionStart > 0) {
binding.searchBar.text =
binding.searchBar.text?.delete(
selectionStart - 1,
selectionEnd
)
binding.searchBar.setSelection(selectionStart - 1)
}
}
}
viewModel.appendDigitToSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { digit ->
val newValue = "${binding.searchBar.text}$digit"
binding.searchBar.setText(newValue)
binding.searchBar.setSelection(newValue.length)
}
}
viewModel.requestKeyboardVisibilityChangedEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.searchBar.showKeyboard()
} else {
binding.searchBar.requestFocus()
binding.searchBar.hideKeyboard()
}
}
}
val bottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback)
viewModel.isNumpadVisible.observe(viewLifecycleOwner) { visible ->
if (visible) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} else {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
viewModel.initiateBlindTransferEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val address = pair.first
val displayName = pair.second
showConfirmBlindTransferDialog(address, displayName)
}
}
binding.root.setKeyboardInsetListener { keyboardVisible ->
if (keyboardVisible) {
viewModel.isNumpadVisible.value = false
}
}
}
override fun onPause() {
super.onPause()
numberOrAddressPickerDialog?.dismiss()
numberOrAddressPickerDialog = null
}
override fun onResume() {
super.onResume()
viewModel.title.value = getString(
R.string.call_transfer_current_call_title,
callViewModel.displayedName.value ?: callViewModel.displayedAddress.value
)
coreContext.postOnCoreThread {
if (corePreferences.automaticallyShowDialpad) {
viewModel.isNumpadVisible.postValue(true)
}
}
}
private fun showConfirmAttendedTransferDialog(callModel: CallModel) {
val from = callViewModel.displayedName.value.orEmpty()
val to = callModel.displayName.value.orEmpty()
Log.i("$TAG Asking user confirmation before doing attended transfer of call with [$from] to [$to](${callModel.call.remoteAddress.asStringUriOnly()})")
val label = AppUtils.getFormattedString(
R.string.call_transfer_confirm_dialog_message,
from,
to
)
val model = ConfirmationDialogModel(label)
val dialog = DialogUtils.getConfirmCallTransferCallDialog(
requireActivity(),
model
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Attended transfer was cancelled by user")
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
coreContext.postOnCoreThread {
val call = callModel.call
Log.i(
"$TAG Transferring (attended) call to [${call.remoteAddress.asStringUriOnly()}]"
)
callViewModel.attendedTransferCallTo(call)
}
dialog.dismiss()
findNavController().popBackStack()
}
}
dialog.show()
}
private fun showConfirmBlindTransferDialog(toAddress: Address, toDisplayName: String) {
val from = callViewModel.displayedName.value.orEmpty()
Log.i("$TAG Asking user confirmation before doing blind transfer of call with [$from] to [$toDisplayName](${toAddress.asStringUriOnly()})")
val label = AppUtils.getFormattedString(
R.string.call_transfer_confirm_dialog_message,
from,
toDisplayName
)
val model = ConfirmationDialogModel(label)
val dialog = DialogUtils.getConfirmCallTransferCallDialog(
requireActivity(),
model
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Blind transfer was cancelled by user")
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
coreContext.postOnCoreThread {
Log.i("$TAG Transferring (blind) call to [${toAddress.asStringUriOnly()}]")
callViewModel.blindTransferCallTo(toAddress)
}
dialog.dismiss()
findNavController().popBackStack()
}
}
dialog.show()
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2010-2023 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.ui.call.model
import androidx.annotation.WorkerThread
import org.linphone.core.AudioDevice
data class AudioDeviceModel
@WorkerThread
constructor(
val audioDevice: AudioDevice,
val name: String,
val type: AudioDevice.Type,
val isCurrentlySelected: Boolean,
val isEnabled: Boolean,
private val onAudioDeviceSelected: (() -> Unit)? = null
) {
var dismissDialog: (() -> Unit)? = null
fun onClicked() {
onAudioDeviceSelected?.invoke()
dismissDialog?.invoke()
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2010-2023 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.ui.call.model
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.R
import org.linphone.core.Call
import org.linphone.core.MediaEncryption
import org.linphone.core.StreamType
import org.linphone.utils.AppUtils
class CallMediaEncryptionModel
@WorkerThread
constructor(
private val showZrtpSasValidationDialog: () -> Unit
) {
val mediaEncryption = MutableLiveData<String>()
val isMediaEncryptionZrtp = MutableLiveData<Boolean>()
val zrtpCipher = MutableLiveData<String>()
val zrtpKeyAgreement = MutableLiveData<String>()
val zrtpHash = MutableLiveData<String>()
val zrtpAuthTag = MutableLiveData<String>()
val zrtpAuthSas = MutableLiveData<String>()
@WorkerThread
fun update(call: Call) {
isMediaEncryptionZrtp.postValue(false)
val stats = call.getStats(StreamType.Audio)
if (stats != null) {
// ZRTP stats are only available when authentication token isn't null !
if (call.currentParams.mediaEncryption == MediaEncryption.ZRTP && call.authenticationToken != null) {
isMediaEncryptionZrtp.postValue(true)
if (stats.isZrtpKeyAgreementAlgoPostQuantum) {
mediaEncryption.postValue(
AppUtils.getFormattedString(
R.string.call_stats_media_encryption,
AppUtils.getString(
R.string.call_stats_media_encryption_zrtp_post_quantum
)
)
)
} else {
mediaEncryption.postValue(
AppUtils.getFormattedString(
R.string.call_stats_media_encryption,
call.currentParams.mediaEncryption.name
)
)
}
zrtpCipher.postValue(
AppUtils.getFormattedString(
R.string.call_stats_zrtp_cipher_algo,
stats.zrtpCipherAlgo
)
)
zrtpKeyAgreement.postValue(
AppUtils.getFormattedString(
R.string.call_stats_zrtp_key_agreement_algo,
stats.zrtpKeyAgreementAlgo
)
)
zrtpHash.postValue(
AppUtils.getFormattedString(
R.string.call_stats_zrtp_hash_algo,
stats.zrtpHashAlgo
)
)
zrtpAuthTag.postValue(
AppUtils.getFormattedString(
R.string.call_stats_zrtp_auth_tag_algo,
stats.zrtpAuthTagAlgo
)
)
zrtpAuthSas.postValue(
AppUtils.getFormattedString(
R.string.call_stats_zrtp_sas_algo,
stats.zrtpSasAlgo
)
)
} else {
mediaEncryption.postValue(
AppUtils.getFormattedString(
R.string.call_stats_media_encryption,
call.currentParams.mediaEncryption.name
)
)
}
}
}
fun showSasValidationDialog() {
showZrtpSasValidationDialog.invoke()
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2010-2023 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.ui.call.model
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Call
import org.linphone.core.CallListenerStub
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.LinphoneUtils
class CallModel
@WorkerThread
constructor(val call: Call) {
companion object {
private const val TAG = "[Call Model]"
}
val id = call.callLog.callId
val displayName = MutableLiveData<String>()
val state = MutableLiveData<String>()
val isPaused = MutableLiveData<Boolean>()
val friend = coreContext.contactsManager.findContactByAddress(call.callLog.remoteAddress)
val contact = MutableLiveData<ContactAvatarModel>()
private val callListener = object : CallListenerStub() {
@WorkerThread
override fun onStateChanged(call: Call, state: Call.State, message: String) {
this@CallModel.state.postValue(LinphoneUtils.callStateToString(state))
isPaused.postValue(LinphoneUtils.isCallPaused(state))
}
}
init {
call.addListener(callListener)
val conferenceInfo = coreContext.core.findConferenceInformationFromUri(call.remoteAddress)
val remoteAddress = call.callLog.remoteAddress
val avatarModel = if (conferenceInfo != null) {
coreContext.contactsManager.getContactAvatarModelForConferenceInfo(conferenceInfo)
} else {
coreContext.contactsManager.getContactAvatarModelForAddress(
remoteAddress
)
}
contact.postValue(avatarModel)
displayName.postValue(
avatarModel.friend.name ?: LinphoneUtils.getDisplayName(remoteAddress)
)
state.postValue(LinphoneUtils.callStateToString(call.state))
isPaused.postValue(LinphoneUtils.isCallPaused(call.state))
}
@WorkerThread
fun destroy() {
call.removeListener(callListener)
}
@WorkerThread
fun togglePauseResume() {
when (call.state) {
Call.State.Paused -> {
Log.i("$TAG Trying to resume call [${call.remoteAddress.asStringUriOnly()}]")
call.resume()
}
else -> {
Log.i("$TAG Trying to resume call [${call.remoteAddress.asStringUriOnly()}]")
call.pause()
}
}
}
@UiThread
fun hangUp() {
coreContext.postOnCoreThread {
Log.i("$TAG Terminating call [${call.remoteAddress.asStringUriOnly()}]")
coreContext.terminateCall(call)
}
}
}

View file

@ -0,0 +1,168 @@
/*
* Copyright (c) 2010-2023 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.ui.call.model
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import kotlin.math.roundToInt
import org.linphone.R
import org.linphone.core.Call
import org.linphone.core.CallStats
import org.linphone.core.MediaDirection
import org.linphone.core.StreamType
import org.linphone.utils.AppUtils
class CallStatsModel
@WorkerThread
constructor() {
val audioCodec = MutableLiveData<String>()
val audioBandwidth = MutableLiveData<String>()
val lossRate = MutableLiveData<String>()
val jitterBuffer = MutableLiveData<String>()
val isVideoEnabled = MutableLiveData<Boolean>()
val videoCodec = MutableLiveData<String>()
val videoBandwidth = MutableLiveData<String>()
val videoLossRate = MutableLiveData<String>()
val videoResolution = MutableLiveData<String>()
val videoFps = MutableLiveData<String>()
val fecEnabled = MutableLiveData<Boolean>()
val lostPackets = MutableLiveData<String>()
val repairedPackets = MutableLiveData<String>()
val fecBandwidth = MutableLiveData<String>()
@WorkerThread
fun update(call: Call, stats: CallStats?) {
stats ?: return
val videoEnabled = call.currentParams.isVideoEnabled
val remoteParamsVideoDirection = call.remoteParams?.videoDirection
val remoteSendsVideo = remoteParamsVideoDirection == MediaDirection.SendRecv || remoteParamsVideoDirection == MediaDirection.SendOnly
val localParamsVideoDirection = call.params.videoDirection
val localSendsVideo = localParamsVideoDirection == MediaDirection.SendRecv || localParamsVideoDirection == MediaDirection.SendOnly
val showVideoStats = videoEnabled && (remoteSendsVideo || localSendsVideo)
isVideoEnabled.postValue(showVideoStats)
val isFecEnabled = call.currentParams.isFecEnabled
fecEnabled.postValue(showVideoStats && isFecEnabled)
when (stats.type) {
StreamType.Audio -> {
val payloadType = call.currentParams.usedAudioPayloadType
val clockRate = (payloadType?.clockRate ?: 0) / 1000
val codecLabel = AppUtils.getFormattedString(
R.string.call_stats_codec_label,
"${payloadType?.mimeType}/$clockRate kHz"
)
audioCodec.postValue(codecLabel)
val uploadBandwidth = stats.uploadBandwidth.roundToInt()
val downloadBandwidth = stats.downloadBandwidth.roundToInt()
val bandwidthLabel = AppUtils.getFormattedString(
R.string.call_stats_bandwidth_label,
"$uploadBandwidth kbits/s ↓ $downloadBandwidth kbits/s"
)
audioBandwidth.postValue(bandwidthLabel)
val uploadLoss = stats.senderLossRate.roundToInt()
val downloadLoss = stats.receiverLossRate.roundToInt()
val lossRateLabel = AppUtils.getFormattedString(
R.string.call_stats_loss_rate_label,
"$uploadLoss% ↓ $downloadLoss%"
)
lossRate.postValue(lossRateLabel)
val jitterBufferSize = stats.jitterBufferSizeMs.roundToInt()
val jitterBufferLabel = AppUtils.getFormattedString(
R.string.call_stats_jitter_buffer_label,
"$jitterBufferSize ms"
)
jitterBuffer.postValue(jitterBufferLabel)
}
StreamType.Video -> {
val payloadType = call.currentParams.usedVideoPayloadType
val clockRate = (payloadType?.clockRate ?: 0) / 1000
val codecLabel = AppUtils.getFormattedString(
R.string.call_stats_codec_label,
"${payloadType?.mimeType}/$clockRate kHz"
)
videoCodec.postValue(codecLabel)
val uploadBandwidth = stats.uploadBandwidth.roundToInt()
val downloadBandwidth = stats.downloadBandwidth.roundToInt()
val bandwidthLabel = AppUtils.getFormattedString(
R.string.call_stats_bandwidth_label,
"$uploadBandwidth kbits/s ↓ $downloadBandwidth kbits/s"
)
videoBandwidth.postValue(bandwidthLabel)
val uploadLoss = stats.senderLossRate.roundToInt()
val downloadLoss = stats.receiverLossRate.roundToInt()
val lossRateLabel = AppUtils.getFormattedString(
R.string.call_stats_loss_rate_label,
"$uploadLoss% ↓ $downloadLoss%"
)
videoLossRate.postValue(lossRateLabel)
val sentResolution = call.currentParams.sentVideoDefinition?.name
val receivedResolution = call.currentParams.receivedVideoDefinition?.name
val resolutionLabel = AppUtils.getFormattedString(
R.string.call_stats_resolution_label,
"$sentResolution$receivedResolution"
)
videoResolution.postValue(resolutionLabel)
val sentFps = call.currentParams.sentFramerate.roundToInt()
val receivedFps = call.currentParams.receivedFramerate.roundToInt()
val fpsLabel = AppUtils.getFormattedString(
R.string.call_stats_fps_label,
"$sentFps$receivedFps"
)
videoFps.postValue(fpsLabel)
if (isFecEnabled) {
val lostPacketsValue = stats.fecCumulativeLostPacketsNumber
val lostPacketsLabel = AppUtils.getFormattedString(
R.string.call_stats_fec_lost_packets_label,
lostPacketsValue
)
lostPackets.postValue(lostPacketsLabel)
val repairedPacketsValue = stats.fecRepairedPacketsNumber
val repairedPacketsLabel = AppUtils.getFormattedString(
R.string.call_stats_fec_repaired_packets_label,
repairedPacketsValue
)
repairedPackets.postValue(repairedPacketsLabel)
val fecUploadBandwidth = stats.fecUploadBandwidth.roundToInt()
val fecDownloadBandwidth = stats.fecDownloadBandwidth.roundToInt()
val fecBandwidthLabel = AppUtils.getFormattedString(
R.string.call_stats_fec_lost_bandwidth_label,
"$fecUploadBandwidth kbits/s ↓ $fecDownloadBandwidth kbits/s"
)
fecBandwidth.postValue(fecBandwidthLabel)
}
}
else -> {}
}
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2010-2024 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.ui.call.model
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import org.linphone.ui.GenericViewModel
import org.linphone.utils.Event
class ZrtpAlertDialogModel
@UiThread
constructor(val allowTryAgain: Boolean) : GenericViewModel() {
val tryAgainEvent = MutableLiveData<Event<Boolean>>()
val hangUpEvent = MutableLiveData<Event<Boolean>>()
@UiThread
fun tryAgain() {
tryAgainEvent.value = Event(true)
}
@UiThread
fun hangUp() {
hangUpEvent.value = Event(true)
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2010-2023 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.ui.call.model
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.utils.Event
class ZrtpSasConfirmationDialogModel
@UiThread
constructor(
authTokenToRead: String,
authTokensToListen: List<String>,
val cacheMismatch: Boolean
) : GenericViewModel() {
companion object {
private const val TAG = "[ZRTP SAS Confirmation Dialog]"
}
val localToken = MutableLiveData<String>()
val letters1 = MutableLiveData<String>()
val letters2 = MutableLiveData<String>()
val letters3 = MutableLiveData<String>()
val letters4 = MutableLiveData<String>()
val authTokenClickedEvent = MutableLiveData<Event<String>>()
val skipEvent = MutableLiveData<Event<Boolean>>()
init {
localToken.value = authTokenToRead
letters1.value = authTokensToListen[0]
letters2.value = authTokensToListen[1]
letters3.value = authTokensToListen[2]
letters4.value = authTokensToListen[3]
}
@UiThread
fun skip() {
skipEvent.value = Event(true)
}
@UiThread
fun notFound() {
Log.e("$TAG User clicked on 'Not Found' button!")
authTokenClickedEvent.value = Event("")
}
@UiThread
fun lettersClicked(letters: MutableLiveData<String>) {
val token = letters.value.orEmpty()
Log.i("$TAG User clicked on [$token] letters")
authTokenClickedEvent.value = Event(token)
}
}

Some files were not shown because too many files have changed in this diff Show more