Compare commits

...

190 commits
master ... c3

Author SHA1 Message Date
Daniel Gultsch 69866e591c
include mediaType and size in message content 2023-04-19 08:11:39 +02:00
Daniel Gultsch 506e4e1d0c
add index on disco.feature coloum 2023-04-02 09:53:35 +02:00
Daniel Gultsch c858b5346f
clean up references in ChatFragment 2023-04-01 13:59:48 +02:00
Daniel Gultsch e6bf595388
do not restrain embedded message size to outer message size 2023-04-01 11:39:30 +02:00
Daniel Gultsch 9127d68197
render reactions 2023-04-01 11:01:10 +02:00
Daniel Gultsch 340bf45095
do not show fallback in embedded message 2023-03-31 15:36:57 +02:00
Daniel Gultsch acfcde8416
flash background after scrolling to message 2023-03-31 14:14:20 +02:00
Daniel Gultsch 4f654044b4
remove fallback body when rendering 2023-03-30 18:49:04 +02:00
Daniel Gultsch 1b3c7b6a42
render inReplyTo message 2023-03-30 17:47:00 +02:00
Daniel Gultsch a4fe60dece
add image preview to message bubbles 2023-03-30 16:36:16 +02:00
Daniel Gultsch 03cf48f4c1
set avatar invisible when message is null 2023-03-28 23:14:05 +02:00
Daniel Gultsch 4d5445d123
jump to message id 2023-03-28 17:15:35 +02:00
Daniel Gultsch 4bfcf209d7
add date separators 2023-03-27 16:48:35 +02:00
Daniel Gultsch 5b777ef657
display outgoing messages 2023-03-27 14:41:23 +02:00
Daniel Gultsch d52cbb8e8c
fix message comparator 2023-03-27 11:18:29 +02:00
Daniel Gultsch cc07f86bf4
use occupantResource for sender name 2023-03-27 10:41:46 +02:00
Daniel Gultsch f13f15cc91
include occupant resource 2023-03-24 17:51:29 +01:00
Daniel Gultsch 405eeadd95
show sender and display correct encryption icon 2023-03-24 16:11:55 +01:00
Daniel Gultsch 75a4008aee
use resource for consistent color gen (as per modernxmpp) 2023-03-24 15:07:24 +01:00
Daniel Gultsch 4fae8d4e11
show avatars in chat 2023-03-24 12:21:19 +01:00
Daniel Gultsch 805d0db486
show time underneath bubble 2023-03-23 19:08:09 +01:00
Daniel Gultsch 779e6fa61e
rudimentary MessageAdapter 2023-03-23 12:36:59 +01:00
Daniel Gultsch da528776db
include membersOnlyNonAnonymous in chat info 2023-03-23 12:36:33 +01:00
Daniel Gultsch 4fd96e740f
do not start inner transaction for transformation
throwing in an inner transaction will fail the entire transaction even if the
exception is caught in the outer transaction
2023-03-21 22:01:39 +01:00
Daniel Gultsch 4139c11771
add test for multi page result 2023-03-21 18:21:56 +01:00
Daniel Gultsch 1e884ec435
display title in chat fragment 2023-03-21 16:08:05 +01:00
Daniel Gultsch 86d9264ee5
create stub chat fragment 2023-03-21 11:03:50 +01:00
Daniel Gultsch 0f6f9b0001
do not reload chat filter when correct one is already set 2023-03-12 21:18:13 +01:00
Daniel Gultsch e22fcab844
set up back press action for search view 2023-03-12 21:15:13 +01:00
Daniel Gultsch e3f5f6404b
add unit test for archive dao 2023-03-11 23:16:55 +01:00
Daniel Gultsch 7c820f7b32
create stub message contents for encryption failures 2023-03-11 15:56:17 +01:00
Daniel Gultsch ee1c938f2a
look up sender in group chats 2023-03-11 12:10:26 +01:00
Daniel Gultsch d9e8918727
add TODO on how to use RangeAfter 2023-03-11 09:57:51 +01:00
Daniel Gultsch 97f54b6673
bump annotation processors to java 17 2023-03-10 20:03:32 +01:00
Daniel Gultsch 58c5bd0f1b
fetch MAM messages 2023-03-10 20:03:02 +01:00
Daniel Gultsch bb2d077b7c
warn user when lacking internet connnection 2023-03-09 08:14:30 +01:00
Daniel Gultsch b2c348a1df
delete unused disco info on bind 2023-03-08 16:04:18 +01:00
Daniel Gultsch 9a0c2226c1
fix hashCode and equals in account 2023-03-08 12:57:24 +01:00
Daniel Gultsch e971b77539
fix sentAt not always being properly end aligned 2023-03-08 12:17:25 +01:00
Daniel Gultsch c1ef2ac628
submit empty page when changing filter 2023-03-08 09:52:57 +01:00
Daniel Gultsch eb15dc1260
make avatar shape configurable (in code) 2023-03-08 09:52:01 +01:00
Daniel Gultsch 26d856e91f
support chat filters 2023-03-07 20:43:16 +01:00
Daniel Gultsch 9819ef7d05
fetch vcard avatars 2023-03-07 20:05:20 +01:00
Daniel Gultsch 417e801811
unarchive chat when receiving message 2023-03-07 16:13:25 +01:00
Daniel Gultsch 0d134a919e
add avatar image to chat overview item 2023-03-07 16:04:32 +01:00
Daniel Gultsch 260654f171
rudimentary ChatOverviewAdapter 2023-03-06 18:55:02 +01:00
Daniel Gultsch cfaf6162e6
use Java 17 2023-03-05 15:23:46 +01:00
Daniel Gultsch e4fb793769
use transaction for complex model 2023-03-05 15:15:57 +01:00
Daniel Gultsch f1fbf15fea
add HttpUploadManager slot request 2023-03-05 12:09:56 +01:00
Daniel Gultsch f9b3d42a8a
bump agp 2023-03-05 08:38:49 +01:00
Daniel Gultsch a67979adf8
join MultiUserChats on bind 2023-03-05 08:38:12 +01:00
Daniel Gultsch 8be8d7df8f
parse more presence metadata 2023-03-03 12:05:20 +01:00
Daniel Gultsch 2e5e2ff6fe
cache last used service record in DB 2023-03-03 10:14:02 +01:00
Daniel Gultsch 807078b24f
remove unused 'service' from resolver 2023-03-02 19:45:45 +01:00
Daniel Gultsch 4addeaa356
use futures in DNS resolver 2023-03-02 18:44:27 +01:00
Daniel Gultsch 100c735636
use guavas utiltiy equals+hash in resolver result 2023-03-02 16:12:07 +01:00
Daniel Gultsch b2414434dc
fix connection pool not handling removes 2023-03-02 15:43:56 +01:00
Daniel Gultsch 0c4771e2a8
persist certificate trust to disk 2023-03-02 13:44:29 +01:00
Daniel Gultsch 177320d8fe
use scopes for trust 2023-03-02 10:10:12 +01:00
Daniel Gultsch 9c64f9c24c
add UI for certificate trust 2023-03-01 22:05:46 +01:00
Daniel Gultsch 786a6c4c2a
put trust manager framework in place 2023-03-01 22:05:46 +01:00
Daniel Gultsch be6f4300da
include sender id in tests 2023-03-01 22:05:46 +01:00
Daniel Gultsch c2bf9d0413
store senderIdentity in message 2023-03-01 22:05:46 +01:00
Daniel Gultsch 303f14200f
take btbv setting into account when deciding default trust 2023-03-01 22:05:46 +01:00
Daniel Gultsch 1a924d3efd
introduce AppSettings for easier access to preferences 2023-03-01 22:05:45 +01:00
Daniel Gultsch 86ef179c42
use empty message (not key transport) to finish sessions 2023-03-01 22:05:45 +01:00
Daniel Gultsch 5e79dd8b68
left join trust into MessageWithContentReactions 2023-03-01 22:05:45 +01:00
Daniel Gultsch 3c207c28b4
fix drawer layout reacting to back press after rotation 2023-03-01 22:05:45 +01:00
Daniel Gultsch 9c95554782
add trust to identity table 2023-03-01 22:05:45 +01:00
Daniel Gultsch ac2866a682
add automatic session completion 2023-03-01 22:05:45 +01:00
Daniel Gultsch cf17a2ac6d
request device list when encountering unknown device 2023-03-01 22:05:45 +01:00
Daniel Gultsch c3f5273813
close drawer on back press 2023-03-01 22:05:45 +01:00
Daniel Gultsch 6ef2997b5e
add some menu items to setup screen 2023-03-01 22:05:45 +01:00
Daniel Gultsch b8f3472af0
remember chat filter selection across rotations 2023-03-01 22:05:45 +01:00
Daniel Gultsch d54978f593
store connection settings after pressing submit in hostname fragment 2023-03-01 22:05:45 +01:00
Daniel Gultsch 99c11fba17
add stub hostname fragment 2023-03-01 22:05:45 +01:00
Daniel Gultsch cf5910e96e
add 'encryption' and 'identityKey' to message version entity 2023-03-01 22:05:44 +01:00
Daniel Gultsch 677cfcd34c
generate prekeys on cpu executor 2023-03-01 22:05:44 +01:00
Daniel Gultsch 2abcb1b4e4
decrypt omemo messages 2023-03-01 22:05:44 +01:00
Daniel Gultsch 49b4f54285
run RtpSessionService during phone calls 2023-03-01 22:05:44 +01:00
Daniel Gultsch 1be1334794
fix memory leak in local video track 2023-03-01 22:05:44 +01:00
Daniel Gultsch 63df518c19
include PartType in CallLog 2023-03-01 22:05:44 +01:00
Daniel Gultsch 63bfbfb40a
create transformation for call log 2023-03-01 22:05:44 +01:00
Daniel Gultsch 44ac7190a9
add notifications and attachments settings screens 2023-03-01 22:05:44 +01:00
Daniel Gultsch bfafad6c65
use icons in security preferences 2023-03-01 22:05:44 +01:00
Daniel Gultsch f5203b082b
calculate switch to video cap on jingle connection startup 2023-03-01 22:05:44 +01:00
Daniel Gultsch eafa93d132
port jingle rtp connection 2023-03-01 22:05:44 +01:00
Daniel Gultsch d7ab5e1a4b
add http upload manager 2023-03-01 22:05:44 +01:00
Daniel Gultsch d136928322
redirect from main to setup on zero accounts 2023-03-01 22:05:43 +01:00
Daniel Gultsch 0727b0aba6
add 'security' settings 2023-03-01 22:05:43 +01:00
Daniel Gultsch 1f22c5f534
show dynamic colors setting only if available 2023-03-01 22:05:43 +01:00
Daniel Gultsch 7d42da8c34
Android 7+: do not repeat app name in notification 2023-03-01 22:05:43 +01:00
Daniel Gultsch 09b28358ab
add more sample settings 2023-03-01 22:05:43 +01:00
Daniel Gultsch 7567dcff5e
add settings for dynamic colors and dark theme 2023-03-01 22:05:43 +01:00
Daniel Gultsch b80fe9802a
try to fix SearchBar disappearing 2023-03-01 22:05:43 +01:00
Daniel Gultsch fe9b3b8ed9
introduce settings activity 2023-03-01 22:05:43 +01:00
Daniel Gultsch cdcd323c36
stick intent into menu items to know which one was clicked 2023-03-01 22:05:43 +01:00
Daniel Gultsch 867db9d54c
toggle between 'chats' and 'all chats' 2023-03-01 22:05:43 +01:00
Daniel Gultsch 87e33a779f
add stub MainActivity 2023-03-01 22:05:43 +01:00
Daniel Gultsch c105c3420e
store roster groups and bookmark groups in one table 2023-03-01 22:05:43 +01:00
Daniel Gultsch 2212c63810
add basic foreground service and event receiver 2023-03-01 22:05:42 +01:00
Daniel Gultsch d6edea8ddf
avoid accounts being connected multiple times
the pool should not be asked to connect a specific account
it should only be called to do a full reconfiguration
2023-03-01 22:05:42 +01:00
Daniel Gultsch bca253faa4
navigate from start to password to done in Setup 2023-03-01 22:05:42 +01:00
Daniel Gultsch 68e9f25da2
add leak canary 2023-03-01 22:05:42 +01:00
Daniel Gultsch a1e97461f9
do not parse presences from account 2023-03-01 22:05:42 +01:00
Daniel Gultsch bf9b0b18f9
restructure build setup 2023-03-01 22:05:42 +01:00
Daniel Gultsch a09cc126ea
use logging framework in more places 2023-03-01 22:05:42 +01:00
Daniel Gultsch b0010307c0
move Domain Verifier to im.conversations 2023-03-01 22:05:42 +01:00
Daniel Gultsch b5a47000c9
get rid of payment required account state 2023-03-01 22:05:42 +01:00
Daniel Gultsch 7d34c894d0
move SSLSockets helper library into im.conversations package 2023-03-01 22:05:41 +01:00
Daniel Gultsch 5866974eff
wire up SetupViewModel with account repo 2023-03-01 22:05:41 +01:00
Daniel Gultsch 3c42066a7c
get rid of legacy Jid wrapper around jxmpp 2023-03-01 22:05:41 +01:00
Daniel Gultsch 6845380be5
move Element, Tag etc into im.conversations package 2023-03-01 22:05:41 +01:00
Daniel Gultsch eeac779e25
introduce SetupActiviy 2023-03-01 22:05:41 +01:00
Daniel Gultsch 35360fde91
modify XmppConnection to change status to online for unbound cons 2023-03-01 22:05:41 +01:00
Daniel Gultsch a204bf9ec1
add support to retrieve registration 2023-03-01 22:05:41 +01:00
Daniel Gultsch 79eebe68e2
add registration manager (change password + delete account) 2023-03-01 22:05:41 +01:00
Daniel Gultsch 268bef4433
verify we set occupantId on modifcations in group chat 2023-03-01 22:05:41 +01:00
Daniel Gultsch 69d212141b
parse message retractions 2023-03-01 22:05:41 +01:00
Daniel Gultsch 94c8b9ed04
add models for retraction 2023-03-01 22:05:41 +01:00
Daniel Gultsch 2d10a561e4
rename EmbeddedMessage to MessageEmbedded 2023-03-01 22:05:41 +01:00
Daniel Gultsch acb297ac96
store roster groups in DB 2023-03-01 22:05:41 +01:00
Daniel Gultsch 405445afbe
store reference to inReplyTo in database 2023-03-01 22:05:40 +01:00
Daniel Gultsch 56a462833e
in group chats corrections and reactions use different ids. we need to merge stubs 2023-03-01 22:05:40 +01:00
Daniel Gultsch 2728a96ab9
add helper method to count reactions 2023-03-01 22:05:40 +01:00
Daniel Gultsch 7e2bff9d03
test message correction 2023-03-01 22:05:40 +01:00
Daniel Gultsch 4c09b20aa4
support reaction arriving before message 2023-03-01 22:05:40 +01:00
Daniel Gultsch fbb900d4ad
make transformer testable
note that the test will currently fail because the implemtation isnt complete
2023-03-01 22:05:40 +01:00
Daniel Gultsch 6c24cb12dd
store reactions in database 2023-03-01 22:05:40 +01:00
Daniel Gultsch a69b4b14a5
apply message corrections 2023-03-01 22:05:40 +01:00
Daniel Gultsch be3a8dc5e1
insert message states (displayed, received, error) into DB 2023-03-01 22:05:40 +01:00
Daniel Gultsch 9b62861a64
store messages in database 2023-03-01 22:05:40 +01:00
Daniel Gultsch dc371d7017
add models for Displayed and replace 2023-03-01 22:05:40 +01:00
Daniel Gultsch a43160b13d
setup stub transformer 2023-03-01 22:05:40 +01:00
Daniel Gultsch 458f0ef280
parse and validate stanza-id 2023-03-01 22:05:40 +01:00
Daniel Gultsch 3f59dd2688
add model for MAM result + MAM manager 2023-03-01 22:05:39 +01:00
Daniel Gultsch ca0a0c07fc
add models for Chat States + manager 2023-03-01 22:05:39 +01:00
Daniel Gultsch bed6b07bdd
add receipt manager and process receipt requests 2023-03-01 22:05:39 +01:00
Daniel Gultsch 870393df8e
introduce 'PepManager' to unify what pubsub service we talk to 2023-03-01 22:05:39 +01:00
Daniel Gultsch e2ea1f9437
fix device list publication. boomarks add + retract 2023-03-01 22:05:39 +01:00
Daniel Gultsch 3be56b6775
reconfigure node when precondition is not met 2023-03-01 22:05:39 +01:00
Daniel Gultsch 58b1e26367
include publish-options. prepare code for reconfiguration 2023-03-01 22:05:39 +01:00
Daniel Gultsch c077e4e8da
add PubSubManager, AvatarManager and AxolotlManager 2023-03-01 22:05:39 +01:00
Daniel Gultsch f1e1cf9653
respond to software version requests 2023-03-01 22:05:39 +01:00
Daniel Gultsch e073f22ec0
respond to disco#info queries 2023-03-01 22:05:39 +01:00
Daniel Gultsch 57d264d72e
include caps in outgoing presence 2023-03-01 22:05:39 +01:00
Daniel Gultsch 9a855a57ac
add models for Error conditions 2023-03-01 22:05:39 +01:00
Daniel Gultsch ddcab5fb58
add message carbon processing 2023-03-01 22:05:38 +01:00
Daniel Gultsch fe32526de8
parse blocking command pushes 2023-03-01 22:05:38 +01:00
Daniel Gultsch 164ac450d4
introduce CarbonsManager to enable and maintain carbon state 2023-03-01 22:05:38 +01:00
Daniel Gultsch d2794ccf32
create new models for IQ, Message & Presence 2023-03-01 22:05:38 +01:00
Daniel Gultsch f16603742f
make authentications work with null password 2023-03-01 22:05:38 +01:00
Daniel Gultsch f982885d2e
fix regression in ping strategy 2023-03-01 22:05:38 +01:00
Daniel Gultsch 8df97067bb
discover commands on domain 2023-03-01 22:05:38 +01:00
Daniel Gultsch bd343eafa0
add async variant for createAccount 2023-03-01 22:05:38 +01:00
Daniel Gultsch c31fa7ed2b
include parentNode in disco items table 2023-03-01 22:05:38 +01:00
Daniel Gultsch d25cc059c5
add AccountRepository 2023-03-01 22:05:38 +01:00
Daniel Gultsch 359ef330df
get rid of upsert in favor of update and insert
upsert seems to only work with primary keys and not other
unique constraints.
2023-03-01 22:05:38 +01:00
Daniel Gultsch de06bfb8f0
retrieve Extensions not Elements from extension map 2023-03-01 22:05:38 +01:00
Daniel Gultsch 1e6aed759b
check caps hash after retrieving them 2023-03-01 22:05:37 +01:00
Daniel Gultsch 1a09b3ed05
use empty string instead of null for 'no node' and 'no resource' 2023-03-01 22:05:37 +01:00
Daniel Gultsch 90e613f94e
fix parsing error in Legacy caps 2023-03-01 22:05:37 +01:00
Daniel Gultsch 09db9e574b
do not return NodeHash if no valid hash mech is found 2023-03-01 22:05:37 +01:00
Daniel Gultsch f5faa8fc4d
try with resources in CredentialStore 2023-03-01 22:05:37 +01:00
Daniel Gultsch bfa61d56af
use annotations processor to create extensions 2023-03-01 22:05:37 +01:00
Daniel Gultsch da65960fd1
reconnect account after adding to ConnectionPool 2023-03-01 22:05:37 +01:00
Daniel Gultsch 6983aedddc
add IDs.seed() method for random account seed 2023-03-01 22:05:37 +01:00
Daniel Gultsch 27952c00ed
flush credential store file 2023-03-01 22:05:37 +01:00
Daniel Gultsch 944c48e00b
store presence in DB 2023-03-01 22:05:37 +01:00
Daniel Gultsch 26bff8028a
check disco feature on entity 2023-03-01 22:05:37 +01:00
Daniel Gultsch 873644f528
remove XmppConnection.Features helper class in favor of DiscoManager 2023-03-01 22:05:37 +01:00
Daniel Gultsch 199a1cdc64
add code to fetch items and their infos in one go 2023-03-01 22:05:37 +01:00
Daniel Gultsch 43a82e504b
parse caps from presence 2023-03-01 22:05:37 +01:00
Daniel Gultsch a2b21d97eb
use dedicated hash object instead of byte[] for caps
this way we can store the algo alongside the object
2023-03-01 22:05:36 +01:00
Daniel Gultsch 6458c6e9f9
store disco features after fetching them 2023-03-01 22:05:36 +01:00
Daniel Gultsch 1b438117a3
add Entity Caps 2 hash calculation 2023-03-01 22:05:36 +01:00
Daniel Gultsch 78af8cbd87
migrate entity caps 1 calculation to new code 2023-03-01 22:05:36 +01:00
Daniel Gultsch 482dc8cfe9
insert disco items into db 2023-03-01 22:05:36 +01:00
Daniel Gultsch 3e9029dc8f
bump targetSdk; bump room version 2023-03-01 22:05:36 +01:00
Daniel Gultsch 38c612d35d
make name+namespace assignment in xmpp less error prone 2023-03-01 22:05:36 +01:00
Daniel Gultsch 07c1669813
introduce Manager concept to bundle functionality like roster, blocking, … 2023-03-01 22:05:36 +01:00
Daniel Gultsch 20962554a4
retrieve blocklist on bind 2023-03-01 22:05:36 +01:00
Daniel Gultsch 6b232f7a5a
fetch roster. process result 2023-03-01 22:05:36 +01:00
Daniel Gultsch 9e7bbcc272
offer alternative access to elements and children
instead of Element.findChild(name, namespace) we can now use
Element.getExtension(Extension.class) for registered extensions
2023-03-01 22:05:36 +01:00
Daniel Gultsch 49bf92f7ca
wire up MessageAckProcessor with DB 2023-03-01 22:05:35 +01:00
Daniel Gultsch 2c32f9738c
homogenize ID generation 2023-03-01 22:05:35 +01:00
Daniel Gultsch 7ee3e07946
Introduce XmppConnection v3
The various layers of the app are too intertwined to refactor them in place.

The C3 refactor is going to create a parallel architecture for all classes that
have too strong of a connection to other parts of the app.

This commit introduces XmppConnection v3 that keeps a lot of the logic of the
privous XmppConnection but cuts ties to XmppConnectionService and the very
stateful `entites.Account`. The latter is replaced by a lightweight immutable
account model.

The reconnection logic has been kept but was moved from XmppConnectionService
to a singleton ConnectionPool.
2023-03-01 22:05:35 +01:00
Daniel Gultsch 94dde9f433
initial set of Room entities 2023-03-01 22:05:35 +01:00
Daniel Gultsch 5d79cfbf0d
add spotless plugin 2023-03-01 22:05:35 +01:00
Daniel Gultsch 80d97c3fcc
bump version to 3.0.0-alpha. modify appId 2023-03-01 22:05:35 +01:00
2376 changed files with 34194 additions and 84282 deletions

View file

@ -0,0 +1,15 @@
apply plugin: "java-library"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
dependencies {
implementation project(':annotation')
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'
api 'com.google.auto.service:auto-service-annotations:1.0.1'
implementation 'com.google.guava:guava:31.1-jre'
}

View file

@ -0,0 +1,160 @@
package im.conversations.android.annotation.processor;
import com.google.auto.service.AutoService;
import com.google.common.base.CaseFormat;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.annotation.XmlPackage;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedAnnotationTypes("im.conversations.android.annotation.XmlElement")
public class XmlElementProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
final Set<? extends Element> elements =
roundEnvironment.getElementsAnnotatedWith(XmlElement.class);
final ImmutableMap.Builder<Id, String> builder = ImmutableMap.builder();
for (final Element element : elements) {
if (element instanceof final TypeElement typeElement) {
final Id id = of(typeElement);
builder.put(id, typeElement.getQualifiedName().toString());
}
}
final ImmutableMap<Id, String> maps = builder.build();
if (maps.isEmpty()) {
return false;
}
final JavaFileObject extensionFile;
try {
extensionFile =
processingEnv
.getFiler()
.createSourceFile("im.conversations.android.xmpp.Extensions");
} catch (final IOException e) {
throw new RuntimeException(e);
}
try (final PrintWriter out = new PrintWriter(extensionFile.openWriter())) {
out.println("package im.conversations.android.xmpp;");
out.println("import com.google.common.collect.BiMap;");
out.println("import com.google.common.collect.ImmutableBiMap;");
out.println("import im.conversations.android.xmpp.ExtensionFactory;");
out.println("import im.conversations.android.xmpp.model.Extension;");
out.print("\n");
out.println("public final class Extensions {");
out.println(
"public static final BiMap<ExtensionFactory.Id, Class<? extends Extension>>"
+ " EXTENSION_CLASS_MAP;");
out.println("static {");
out.println(
"final var builder = new ImmutableBiMap.Builder<ExtensionFactory.Id, Class<?"
+ " extends Extension>>();");
for (final Map.Entry<Id, String> entry : maps.entrySet()) {
Id id = entry.getKey();
String clazz = entry.getValue();
out.format(
"builder.put(new ExtensionFactory.Id(\"%s\",\"%s\"),%s.class);",
id.name, id.namespace, clazz);
out.print("\n");
}
out.println("EXTENSION_CLASS_MAP = builder.build();");
out.println("}");
out.println(" private Extensions() {}");
out.println("}");
// writing generated file to out
} catch (IOException e) {
throw new RuntimeException(e);
}
return true;
}
private static Id of(final TypeElement typeElement) {
final XmlElement xmlElement = typeElement.getAnnotation(XmlElement.class);
PackageElement packageElement = getPackageElement(typeElement);
XmlPackage xmlPackage =
packageElement == null ? null : packageElement.getAnnotation(XmlPackage.class);
if (xmlElement == null) {
throw new IllegalStateException(
String.format(
"%s is not annotated as @XmlElement",
typeElement.getQualifiedName().toString()));
}
final String packageNamespace = xmlPackage == null ? null : xmlPackage.namespace();
final String elementName = xmlElement.name();
final String elementNamespace = xmlElement.namespace();
final String namespace;
if (!Strings.isNullOrEmpty(elementNamespace)) {
namespace = elementNamespace;
} else if (!Strings.isNullOrEmpty(packageNamespace)) {
namespace = packageNamespace;
} else {
throw new IllegalStateException(
String.format(
"%s does not declare a namespace",
typeElement.getQualifiedName().toString()));
}
final String name;
if (Strings.isNullOrEmpty(elementName)) {
name =
CaseFormat.UPPER_CAMEL.to(
CaseFormat.LOWER_HYPHEN, typeElement.getSimpleName().toString());
} else {
name = elementName;
}
return new Id(name, namespace);
}
private static PackageElement getPackageElement(final TypeElement typeElement) {
final Element parent = typeElement.getEnclosingElement();
if (parent instanceof PackageElement) {
return (PackageElement) parent;
} else {
final Element nextParent = parent.getEnclosingElement();
if (nextParent instanceof PackageElement) {
return (PackageElement) nextParent;
} else {
return null;
}
}
}
public static class Id {
public final String name;
public final String namespace;
public Id(String name, String namespace) {
this.name = name;
this.namespace = namespace;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Id id = (Id) o;
return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace);
}
@Override
public int hashCode() {
return Objects.hashCode(name, namespace);
}
}
}

6
annotation/build.gradle Normal file
View file

@ -0,0 +1,6 @@
apply plugin: "java-library"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View file

@ -0,0 +1,15 @@
package im.conversations.android.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
public @interface XmlElement {
String name() default "";
String namespace() default "";
}

View file

@ -0,0 +1,12 @@
package im.conversations.android.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.PACKAGE)
public @interface XmlPackage {
String namespace();
}

147
app/build.gradle Normal file
View file

@ -0,0 +1,147 @@
apply plugin: "com.android.application"
apply plugin: "androidx.navigation.safeargs"
apply plugin: "com.diffplug.spotless"
android {
namespace 'im.conversations.android'
compileSdk 33
defaultConfig {
minSdk 23
targetSdk 33
versionCode 1
versionName "3.0.0-alpha"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
buildFeatures {
dataBinding true
}
flavorDimensions "product"
productFlavors {
quicksy {
dimension "product"
applicationId = "im.quicksy.client"
def appName = "Quicksy"
resValue "string", "applicationId", applicationId
resValue "string", "app_name", appName
buildConfigField "String", "APP_NAME", "\"$appName\""
}
conversations {
dimension "product"
applicationId "im.conversations.android"
def appName = "Conversations"
resValue "string", "applicationId", applicationId
resValue "string", "app_name", appName
buildConfigField "String", "APP_NAME", "\"$appName\""
}
}
}
spotless {
java {
target '**/*.java'
googleJavaFormat().aosp().reflowLongStrings()
}
}
dependencies {
implementation project(':annotation')
annotationProcessor project(':annotation-processor')
// make Java 8 API available
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
// Jetpack / AndroidX libraries
implementation "androidx.appcompat:appcompat:$rootProject.ext.appcompatVersion"
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.ext.lifecycleVersion"
implementation "androidx.navigation:navigation-fragment:$rootProject.ext.navVersion"
implementation "androidx.navigation:navigation-ui:$rootProject.ext.navVersion"
implementation "androidx.room:room-runtime:$rootProject.ext.roomVersion"
implementation "androidx.room:room-guava:$rootProject.ext.roomVersion"
implementation "androidx.room:room-paging:$rootProject.ext.roomVersion"
annotationProcessor "androidx.room:room-compiler:$rootProject.ext.roomVersion"
implementation "androidx.paging:paging-runtime:$rootProject.ext.pagingVersion"
implementation "androidx.preference:preference:$rootProject.ext.preferenceVersion"
implementation "androidx.security:security-crypto:1.0.0"
// Google material design libraries
implementation "com.google.android.material:material:$rootProject.ext.material"
// LeakCanary to detect memory leaks in debug builds
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
// crypto libraries
implementation 'org.whispersystems:signal-protocol-java:2.6.2'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'org.bouncycastle:bcmail-jdk15on:1.64'
// XMPP Address library
implementation 'org.jxmpp:jxmpp-jid:1.0.3'
// WebRTC
implementation 'im.conversations.webrtc:webrtc-android:104.0.0'
// Consistent Color Generation
implementation 'org.hsluv:hsluv:0.2'
// DNS library (XMPP needs to resolve SRV records)
implementation 'de.measite.minidns:minidns-hla:0.2.4'
// Guava
implementation 'com.google.guava:guava:31.1-android'
// HTTP library
implementation "com.squareup.okhttp3:okhttp:4.10.0"
// JSON parser
implementation 'com.google.code.gson:gson:2.10.1'
// logging framework + logging api
implementation 'org.slf4j:slf4j-api:1.7.36'
implementation 'com.github.tony19:logback-android:2.0.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.9.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation "androidx.test.espresso:espresso-core:$rootProject.ext.espressoVersion"
}

21
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,214 @@
package im.conversations.android.xmpp;
import static org.hamcrest.Matchers.*;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import im.conversations.android.database.model.Account;
import im.conversations.android.database.model.StanzaId;
import im.conversations.android.transformer.MessageTransformation;
import im.conversations.android.transformer.Transformer;
import im.conversations.android.xmpp.manager.ArchiveManager;
import im.conversations.android.xmpp.model.jabber.Body;
import im.conversations.android.xmpp.model.stanza.Message;
import java.time.Instant;
import java.util.Collections;
import java.util.concurrent.ExecutionException;
import org.hamcrest.MatcherAssert;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class ArchivePagingTest extends BaseTransformationTest {
@Test
public void initialQuery() throws ExecutionException, InterruptedException {
final var ranges = database.archiveDao().resetLivePage(account(), ACCOUNT);
final Range range = Iterables.getOnlyElement(ranges);
Assert.assertNull(range.id);
Assert.assertEquals(Range.Order.REVERSE, range.order);
}
@Test
public void queryAfterSingleLiveMessage() throws ExecutionException, InterruptedException {
final var stub = new StubMessage(2);
transformer.transform(stub.messageTransformation(), stub.stanzaId());
final var ranges = database.archiveDao().resetLivePage(account(), ACCOUNT);
Assert.assertEquals(2, ranges.size());
MatcherAssert.assertThat(
ranges,
contains(new Range(Range.Order.REVERSE, "2"), new Range(Range.Order.NORMAL, "2")));
}
@Test
public void twoLiveMessageQueryNoSubmitAndQuery()
throws ExecutionException, InterruptedException {
final var stub2 = new StubMessage(2);
transformer.transform(stub2.messageTransformation(), stub2.stanzaId());
final var stub3 = new StubMessage(3);
transformer.transform(stub3.messageTransformation(), stub3.stanzaId());
final var ranges = database.archiveDao().resetLivePage(account(), ACCOUNT);
Assert.assertEquals(2, ranges.size());
MatcherAssert.assertThat(
ranges,
contains(new Range(Range.Order.REVERSE, "2"), new Range(Range.Order.NORMAL, "3")));
final var stub4 = new StubMessage(4);
transformer.transform(stub4.messageTransformation(), stub4.stanzaId());
final var rangesSecondAttempt = database.archiveDao().resetLivePage(account(), ACCOUNT);
Assert.assertEquals(2, rangesSecondAttempt.size());
MatcherAssert.assertThat(
rangesSecondAttempt,
contains(new Range(Range.Order.REVERSE, "2"), new Range(Range.Order.NORMAL, "3")));
}
@Test
public void liveMessageQuerySubmitAndQuery() throws ExecutionException, InterruptedException {
final var stub2 = new StubMessage(2);
transformer.transform(stub2.messageTransformation(), stub2.stanzaId());
final var stub3 = new StubMessage(3);
transformer.transform(stub3.messageTransformation(), stub3.stanzaId());
final var ranges = database.archiveDao().resetLivePage(account(), ACCOUNT);
Assert.assertEquals(2, ranges.size());
MatcherAssert.assertThat(
ranges,
contains(new Range(Range.Order.REVERSE, "2"), new Range(Range.Order.NORMAL, "3")));
final var stub4 = new StubMessage(4);
transformer.transform(stub4.messageTransformation(), stub4.stanzaId());
for (final Range range : ranges) {
database.archiveDao()
.submitPage(
account(),
ACCOUNT,
range,
new ArchiveManager.QueryResult(
true, Page.emptyWithCount(range.id, null)),
false);
}
final var rangesSecondAttempt = database.archiveDao().resetLivePage(account(), ACCOUNT);
// we mark the reversing range as complete in the submit above; hence it is not included in
// the second ranges
Assert.assertEquals(1, rangesSecondAttempt.size());
MatcherAssert.assertThat(rangesSecondAttempt, contains(new Range(Range.Order.NORMAL, "4")));
}
@Test
public void liveMessageQuerySubmitTwice() throws ExecutionException, InterruptedException {
final var stub2 = new StubMessage(2);
transformer.transform(stub2.messageTransformation(), stub2.stanzaId());
final var ranges = database.archiveDao().resetLivePage(account(), ACCOUNT);
Assert.assertEquals(2, ranges.size());
MatcherAssert.assertThat(
ranges,
contains(new Range(Range.Order.REVERSE, "2"), new Range(Range.Order.NORMAL, "2")));
final var account = account();
final var transformer =
new Transformer(account, ApplicationProvider.getApplicationContext(), database);
transformer.transform(
Collections.emptyList(),
ACCOUNT,
new Range(Range.Order.REVERSE, "2"),
new ArchiveManager.QueryResult(true, Page.emptyWithCount("2", null)),
true);
transformer.transform(
Collections.emptyList(),
ACCOUNT,
new Range(Range.Order.NORMAL, "2"),
new ArchiveManager.QueryResult(false, new Page("3", "4", 2)),
false);
transformer.transform(
Collections.emptyList(),
ACCOUNT,
new Range(Range.Order.NORMAL, "4"),
new ArchiveManager.QueryResult(true, new Page("5", "6", 2)),
false);
final var rangesSecondAttempt = database.archiveDao().resetLivePage(account(), ACCOUNT);
// we mark the reversing range as complete in the submit above; hence it is not included in
// the second ranges
Assert.assertEquals(1, rangesSecondAttempt.size());
MatcherAssert.assertThat(rangesSecondAttempt, contains(new Range(Range.Order.NORMAL, "6")));
}
@Test
public void liveMessageQuerySubmitTwiceWithDuplicates()
throws ExecutionException, InterruptedException {
final var stub2 = new StubMessage(2);
transformer.transform(stub2.messageTransformation(), stub2.stanzaId());
final var ranges = database.archiveDao().resetLivePage(account(), ACCOUNT);
Assert.assertEquals(2, ranges.size());
MatcherAssert.assertThat(
ranges,
contains(new Range(Range.Order.REVERSE, "2"), new Range(Range.Order.NORMAL, "2")));
final var account = account();
final var transformer =
new Transformer(account, ApplicationProvider.getApplicationContext(), database);
transformer.transform(
Collections.emptyList(),
ACCOUNT,
new Range(Range.Order.REVERSE, "2"),
new ArchiveManager.QueryResult(true, Page.emptyWithCount("2", null)),
true);
transformer.transform(
ImmutableList.of(stub2.messageTransformation()),
ACCOUNT,
new Range(Range.Order.NORMAL, "2"),
new ArchiveManager.QueryResult(false, new Page("3", "4", 2)),
false);
transformer.transform(
Collections.emptyList(),
ACCOUNT,
new Range(Range.Order.NORMAL, "4"),
new ArchiveManager.QueryResult(true, new Page("5", "6", 2)),
false);
}
private Account account() throws ExecutionException, InterruptedException {
return this.database.accountDao().getEnabledAccount(ACCOUNT).get();
}
private static class StubMessage {
public final int id;
private StubMessage(int id) {
this.id = id;
}
public StanzaId stanzaId() {
return new StanzaId(String.valueOf(id), ACCOUNT);
}
public MessageTransformation messageTransformation() {
final var message = new Message();
message.setTo(ACCOUNT);
message.setFrom(REMOTE);
message.addExtension(new Body()).setContent(String.format("%s (%d)", GREETING, id));
return MessageTransformation.of(
message,
Instant.ofEpochSecond(id * 2000L),
REMOTE,
String.valueOf(id),
message.getFrom().asBareJid(),
null);
}
}
}

View file

@ -0,0 +1,42 @@
package im.conversations.android.xmpp;
import android.content.Context;
import androidx.room.Room;
import androidx.test.core.app.ApplicationProvider;
import im.conversations.android.IDs;
import im.conversations.android.database.ConversationsDatabase;
import im.conversations.android.database.entity.AccountEntity;
import im.conversations.android.transformer.Transformer;
import java.util.concurrent.ExecutionException;
import org.junit.Before;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.impl.JidCreate;
public abstract class BaseTransformationTest {
protected static final BareJid ACCOUNT = JidCreate.bareFromOrThrowUnchecked("user@example.com");
protected static final BareJid REMOTE =
JidCreate.bareFromOrThrowUnchecked("juliet@example.com");
protected static final BareJid REMOTE_2 =
JidCreate.bareFromOrThrowUnchecked("romeo@example.com");
protected static final String GREETING = "Hi Juliet. How are you?";
protected ConversationsDatabase database;
protected Transformer transformer;
@Before
public void setupTransformer() throws ExecutionException, InterruptedException {
final Context context = ApplicationProvider.getApplicationContext();
this.database = Room.inMemoryDatabaseBuilder(context, ConversationsDatabase.class).build();
final var account = new AccountEntity();
account.address = ACCOUNT;
account.enabled = true;
account.randomSeed = IDs.seed();
final long id = database.accountDao().insert(account);
this.transformer =
new Transformer(
database.accountDao().getEnabledAccount(id).get(), context, database);
}
}

View file

@ -0,0 +1,555 @@
package im.conversations.android.xmpp;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.Iterables;
import im.conversations.android.database.model.Encryption;
import im.conversations.android.database.model.MessageEmbedded;
import im.conversations.android.database.model.Modification;
import im.conversations.android.database.model.PartType;
import im.conversations.android.transformer.MessageTransformation;
import im.conversations.android.xmpp.model.correction.Replace;
import im.conversations.android.xmpp.model.jabber.Body;
import im.conversations.android.xmpp.model.reactions.Reaction;
import im.conversations.android.xmpp.model.reactions.Reactions;
import im.conversations.android.xmpp.model.receipts.Received;
import im.conversations.android.xmpp.model.reply.Reply;
import im.conversations.android.xmpp.model.retract.Retract;
import im.conversations.android.xmpp.model.stanza.Message;
import java.time.Instant;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Resourcepart;
import org.jxmpp.stringprep.XmppStringprepException;
@RunWith(AndroidJUnit4.class)
public class MessageTransformationTest extends BaseTransformationTest {
@Test
public void reactionBeforeOriginal() throws XmppStringprepException {
final var reactionMessage = new Message();
reactionMessage.setId("2");
reactionMessage.setTo(ACCOUNT);
reactionMessage.setFrom(JidCreate.fullFrom(REMOTE, Resourcepart.from("junit")));
final var reactions = reactionMessage.addExtension(new Reactions());
reactions.setId("1");
final var reaction = reactions.addExtension(new Reaction());
reaction.setContent("Y");
this.transformer.transform(
MessageTransformation.of(
reactionMessage,
Instant.now(),
REMOTE,
"stanza-b",
reactionMessage.getFrom().asBareJid(),
null));
final var originalMessage = new Message();
originalMessage.setId("1");
originalMessage.setTo(REMOTE);
originalMessage.setFrom(JidCreate.fullFrom(ACCOUNT, Resourcepart.from(("junit"))));
final var body = originalMessage.addExtension(new Body());
body.setContent(GREETING);
this.transformer.transform(
MessageTransformation.of(
originalMessage,
Instant.now(),
REMOTE,
"stanza-a",
originalMessage.getFrom().asBareJid(),
null));
final var messages = database.messageDao().getMessagesForTesting(1L);
Assert.assertEquals(1, messages.size());
final var message = Iterables.getOnlyElement(messages);
final var onlyContent = Iterables.getOnlyElement(message.contents);
Assert.assertEquals(GREETING, onlyContent.body);
Assert.assertEquals(Encryption.CLEARTEXT, message.encryption);
final var onlyReaction = Iterables.getOnlyElement(message.reactions);
Assert.assertEquals("Y", onlyReaction.reaction);
Assert.assertEquals(REMOTE, onlyReaction.reactionBy);
}
@Test
public void multipleReactions() throws XmppStringprepException {
final var group = JidCreate.bareFrom("a@group.example.com");
final var message = new Message(Message.Type.GROUPCHAT);
message.addExtension(new Body("Please give me a thumbs up"));
message.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-a")));
this.transformer.transform(
MessageTransformation.of(
message, Instant.now(), REMOTE, "stanza-a", null, "id-user-a"));
final var reactionA = new Message(Message.Type.GROUPCHAT);
reactionA.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-b")));
reactionA.addExtension(Reactions.to("stanza-a")).addExtension(new Reaction("Y"));
this.transformer.transform(
MessageTransformation.of(
reactionA, Instant.now(), REMOTE, "stanza-b", null, "id-user-b"));
final var reactionB = new Message(Message.Type.GROUPCHAT);
reactionB.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-c")));
reactionB.addExtension(Reactions.to("stanza-a")).addExtension(new Reaction("Y"));
this.transformer.transform(
MessageTransformation.of(
reactionB, Instant.now(), REMOTE, "stanza-c", null, "id-user-c"));
final var reactionC = new Message(Message.Type.GROUPCHAT);
reactionC.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-d")));
final var reactions = reactionC.addExtension(Reactions.to("stanza-a"));
reactions.addExtension(new Reaction("Y"));
reactions.addExtension(new Reaction("Z"));
this.transformer.transform(
MessageTransformation.of(
reactionC, Instant.now(), REMOTE, "stanza-d", null, "id-user-d"));
final var messages = database.messageDao().getMessagesForTesting(1L);
Assert.assertEquals(1, messages.size());
final var dbMessage = Iterables.getOnlyElement(messages);
Assert.assertEquals(4, dbMessage.reactions.size());
final var aggregated = dbMessage.getAggregatedReactions();
final var mostFrequentReaction = Iterables.get(aggregated, 0);
Assert.assertEquals("Y", mostFrequentReaction.getKey());
Assert.assertEquals(3L, (long) mostFrequentReaction.getValue());
final var secondReaction = Iterables.get(aggregated, 1);
Assert.assertEquals("Z", secondReaction.getKey());
Assert.assertEquals(1L, (long) secondReaction.getValue());
}
@Test
public void correctionBeforeOriginal() throws XmppStringprepException {
final var messageCorrection = new Message();
messageCorrection.setId("2");
messageCorrection.setTo(ACCOUNT);
messageCorrection.setFrom(JidCreate.fullFrom(REMOTE, Resourcepart.from("junit")));
messageCorrection.addExtension(new Body()).setContent("Hi example!");
messageCorrection.addExtension(new Replace()).setId("1");
this.transformer.transform(
MessageTransformation.of(
messageCorrection,
Instant.now(),
REMOTE,
"stanza-a",
messageCorrection.getFrom().asBareJid(),
null));
// the correction should not show up as a message
Assert.assertEquals(0, database.messageDao().getMessagesForTesting(1L).size());
final var messageWithTypo = new Message();
messageWithTypo.setId("1");
messageWithTypo.setTo(ACCOUNT);
messageWithTypo.setFrom(JidCreate.fullFrom(REMOTE, Resourcepart.from("junit")));
messageWithTypo.addExtension(new Body()).setContent("Hii example!");
this.transformer.transform(
MessageTransformation.of(
messageWithTypo,
Instant.now(),
REMOTE,
"stanza-b",
messageWithTypo.getFrom().asBareJid(),
null));
final var messages = database.messageDao().getMessagesForTesting(1L);
Assert.assertEquals(1, messages.size());
final var message = Iterables.getOnlyElement(messages);
final var onlyContent = Iterables.getOnlyElement(message.contents);
Assert.assertEquals(Modification.CORRECTION, message.modification);
Assert.assertEquals("Hi example!", onlyContent.body);
}
@Test
public void correctionAfterOriginal() throws XmppStringprepException {
final var messageWithTypo = new Message();
messageWithTypo.setId("1");
messageWithTypo.setTo(ACCOUNT);
messageWithTypo.setFrom(JidCreate.fullFrom(REMOTE, Resourcepart.from("junit")));
messageWithTypo.addExtension(new Body()).setContent("Hii example!");
this.transformer.transform(
MessageTransformation.of(
messageWithTypo,
Instant.now(),
REMOTE,
"stanza-a",
messageWithTypo.getFrom().asBareJid(),
null));
Assert.assertEquals(1, database.messageDao().getMessagesForTesting(1L).size());
final var messageCorrection = new Message();
messageCorrection.setId("2");
messageCorrection.setTo(ACCOUNT);
messageCorrection.setFrom(JidCreate.fullFrom(REMOTE, Resourcepart.from("junit")));
messageCorrection.addExtension(new Body()).setContent("Hi example!");
messageCorrection.addExtension(new Replace()).setId("1");
this.transformer.transform(
MessageTransformation.of(
messageCorrection,
Instant.now(),
REMOTE,
"stanza-b",
messageCorrection.getFrom().asBareJid(),
null));
final var messages = database.messageDao().getMessagesForTesting(1L);
Assert.assertEquals(1, messages.size());
final var message = Iterables.getOnlyElement(messages);
final var onlyContent = Iterables.getOnlyElement(message.contents);
Assert.assertEquals(Modification.CORRECTION, message.modification);
Assert.assertEquals("Hi example!", onlyContent.body);
}
@Test
public void replacingReactions() throws XmppStringprepException {
final var group = JidCreate.bareFrom("a@group.example.com");
final var message = new Message(Message.Type.GROUPCHAT);
message.addExtension(new Body("Please give me a thumbs up"));
message.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-a")));
this.transformer.transform(
MessageTransformation.of(
message, Instant.now(), REMOTE, "stanza-a", null, "id-user-a"));
final var reactionA = new Message(Message.Type.GROUPCHAT);
reactionA.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-b")));
reactionA.addExtension(Reactions.to("stanza-a")).addExtension(new Reaction("N"));
this.transformer.transform(
MessageTransformation.of(
reactionA, Instant.now(), REMOTE, "stanza-b", null, "id-user-b"));
final var reactionB = new Message(Message.Type.GROUPCHAT);
reactionB.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-b")));
reactionB.addExtension(Reactions.to("stanza-a")).addExtension(new Reaction("Y"));
this.transformer.transform(
MessageTransformation.of(
reactionB, Instant.now(), REMOTE, "stanza-c", null, "id-user-b"));
final var messages = database.messageDao().getMessagesForTesting(1L);
Assert.assertEquals(1, messages.size());
final var dbMessage = Iterables.getOnlyElement(messages);
Assert.assertEquals(1, dbMessage.reactions.size());
}
@Test
public void twoCorrectionsOneReactionBeforeOriginalInGroupChat()
throws XmppStringprepException {
final var group = JidCreate.bareFrom("a@group.example.com");
final var ogStanzaId = "og-stanza-id";
final var ogMessageId = "og-message-id";
// first correction
final var m1 = new Message(Message.Type.GROUPCHAT);
// m1.setId(ogMessageId);
m1.addExtension(new Body("Please give me an thumbs up"));
m1.addExtension(new Replace()).setId(ogMessageId);
m1.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-a")));
this.transformer.transform(
MessageTransformation.of(
m1,
Instant.ofEpochMilli(2000),
REMOTE,
"irrelevant-stanza-id1",
null,
"id-user-a"));
// second correction
final var m2 = new Message(Message.Type.GROUPCHAT);
// m2.setId(ogMessageId);
m2.addExtension(new Body("Please give me a thumbs up"));
m2.addExtension(new Replace()).setId(ogMessageId);
m2.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-a")));
this.transformer.transform(
MessageTransformation.of(
m2,
Instant.ofEpochMilli(3000),
REMOTE,
"irrelevant-stanza-id2",
null,
"id-user-a"));
// a reaction
final var reactionB = new Message(Message.Type.GROUPCHAT);
reactionB.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-b")));
reactionB.addExtension(Reactions.to(ogStanzaId)).addExtension(new Reaction("Y"));
this.transformer.transform(
MessageTransformation.of(
reactionB,
Instant.now(),
REMOTE,
"irrelevant-stanza-id3",
null,
"id-user-b"));
// the original message
final var m4 = new Message(Message.Type.GROUPCHAT);
m4.setId(ogMessageId);
m4.addExtension(new Body("Please give me thumbs up"));
m4.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-a")));
this.transformer.transform(
MessageTransformation.of(
m4, Instant.ofEpochMilli(1000), REMOTE, ogStanzaId, null, "id-user-a"));
final var messages = database.messageDao().getMessagesForTesting(1L);
Assert.assertEquals(1, messages.size());
final var dbMessage = Iterables.getOnlyElement(messages);
Assert.assertEquals(1, dbMessage.reactions.size());
Assert.assertEquals(Modification.CORRECTION, dbMessage.modification);
Assert.assertEquals(
"Please give me a thumbs up", Iterables.getOnlyElement(dbMessage.contents).body);
}
@Test
public void twoReactionsOneCorrectionBeforeOriginalInGroupChat()
throws XmppStringprepException {
final var group = JidCreate.bareFrom("a@group.example.com");
final var ogStanzaId = "og-stanza-id";
final var ogMessageId = "og-message-id";
// first reaction
final var reactionA = new Message(Message.Type.GROUPCHAT);
reactionA.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-b")));
reactionA.addExtension(Reactions.to(ogStanzaId)).addExtension(new Reaction("Y"));
this.transformer.transform(
MessageTransformation.of(
reactionA,
Instant.now(),
REMOTE,
"irrelevant-stanza-id1",
null,
"id-user-b"));
// second reaction
final var reactionB = new Message(Message.Type.GROUPCHAT);
reactionB.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-c")));
reactionB.addExtension(Reactions.to(ogStanzaId)).addExtension(new Reaction("Y"));
this.transformer.transform(
MessageTransformation.of(
reactionB,
Instant.now(),
REMOTE,
"irrelevant-stanza-id2",
null,
"id-user-c"));
// a correction
final var m1 = new Message(Message.Type.GROUPCHAT);
m1.addExtension(new Body("Please give me a thumbs up"));
m1.addExtension(new Replace()).setId(ogMessageId);
m1.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-a")));
this.transformer.transform(
MessageTransformation.of(
m1,
Instant.ofEpochMilli(2000),
REMOTE,
"irrelevant-stanza-id3",
null,
"id-user-a"));
// the original message
final var m4 = new Message(Message.Type.GROUPCHAT);
m4.setId(ogMessageId);
m4.addExtension(new Body("Please give me thumbs up (Typo)"));
m4.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-a")));
this.transformer.transform(
MessageTransformation.of(
m4, Instant.ofEpochMilli(1000), REMOTE, ogStanzaId, null, "id-user-a"));
final var messages = database.messageDao().getMessagesForTesting(1L);
Assert.assertEquals(1, messages.size());
final var dbMessage = Iterables.getOnlyElement(messages);
Assert.assertEquals(2, dbMessage.reactions.size());
final var onlyReaction = Iterables.getOnlyElement(dbMessage.getAggregatedReactions());
Assert.assertEquals(2L, (long) onlyReaction.getValue());
Assert.assertEquals(Modification.CORRECTION, dbMessage.modification);
Assert.assertEquals(
"Please give me a thumbs up", Iterables.getOnlyElement(dbMessage.contents).body);
}
@Test
public void twoReactionsInGroupChat() throws XmppStringprepException {
final var group = JidCreate.bareFrom("a@group.example.com");
final var ogStanzaId = "og-stanza-id";
final var ogMessageId = "og-message-id";
// the original message
final var m4 = new Message(Message.Type.GROUPCHAT);
m4.setId(ogMessageId);
m4.addExtension(new Body("Please give me a thumbs up"));
m4.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-a")));
this.transformer.transform(
MessageTransformation.of(
m4, Instant.ofEpochMilli(1000), REMOTE, ogStanzaId, null, "id-user-a"));
// first reaction
final var reactionA = new Message(Message.Type.GROUPCHAT);
reactionA.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-b")));
reactionA.addExtension(Reactions.to(ogStanzaId)).addExtension(new Reaction("Y"));
this.transformer.transform(
MessageTransformation.of(
reactionA,
Instant.now(),
REMOTE,
"irrelevant-stanza-id1",
null,
"id-user-b"));
// second reaction
final var reactionB = new Message(Message.Type.GROUPCHAT);
reactionB.setFrom(JidCreate.fullFrom(group, Resourcepart.from("user-c")));
reactionB.addExtension(Reactions.to(ogStanzaId)).addExtension(new Reaction("Y"));
this.transformer.transform(
MessageTransformation.of(
reactionB,
Instant.now(),
REMOTE,
"irrelevant-stanza-id2",
null,
"id-user-c"));
final var messages = database.messageDao().getMessagesForTesting(1L);
Assert.assertEquals(1, messages.size());
final var dbMessage = Iterables.getOnlyElement(messages);
Assert.assertEquals(2, dbMessage.reactions.size());
final var onlyReaction = Iterables.getOnlyElement(dbMessage.getAggregatedReactions());
Assert.assertEquals(2L, (long) onlyReaction.getValue());
Assert.assertEquals(Modification.ORIGINAL, dbMessage.modification);
Assert.assertEquals(
"Please give me a thumbs up", Iterables.getOnlyElement(dbMessage.contents).body);
}
@Test
public void inReplyTo() throws XmppStringprepException {
final var m1 = new Message();
m1.setId("1");
m1.setTo(ACCOUNT);
m1.setFrom(JidCreate.fullFrom(REMOTE, Resourcepart.from("junit")));
m1.addExtension(new Body("Hi. How are you?"));
this.transformer.transform(
MessageTransformation.of(
m1, Instant.now(), REMOTE, "stanza-a", m1.getFrom().asBareJid(), null));
final var m2 = new Message();
m2.setId("2");
m2.setTo(REMOTE);
m2.setFrom(ACCOUNT);
m2.addExtension(new Body("I am fine."));
final var reply = m2.addExtension(new Reply());
reply.setId("1");
reply.setTo(REMOTE);
this.transformer.transform(
MessageTransformation.of(
m2, Instant.now(), REMOTE, "stanza-b", m2.getFrom().asBareJid(), null));
final var messages = database.messageDao().getMessagesForTesting(1L);
Assert.assertEquals(2, messages.size());
final var response = Iterables.get(messages, 1);
Assert.assertNotNull(response.inReplyToMessageEntityId);
final MessageEmbedded embeddedMessage = response.inReplyTo;
Assert.assertNotNull(embeddedMessage);
Assert.assertEquals(REMOTE, embeddedMessage.fromBare);
Assert.assertEquals(1L, embeddedMessage.contents.size());
Assert.assertEquals(
"Hi. How are you?", Iterables.getOnlyElement(embeddedMessage.contents).body);
Assert.assertNull(response.identityKey);
Assert.assertNull(response.trust);
}
@Test
public void messageWithReceipt() throws XmppStringprepException {
final var m1 = new Message();
m1.setId("1");
m1.setTo(REMOTE);
m1.setFrom(JidCreate.fullFrom(ACCOUNT, Resourcepart.from("junit")));
m1.addExtension(new Body("Hi. How are you?"));
this.transformer.transform(
MessageTransformation.of(
m1, Instant.now(), REMOTE, null, m1.getFrom().asBareJid(), null));
final var m2 = new Message();
m2.setTo(JidCreate.fullFrom(ACCOUNT, Resourcepart.from("junit")));
m2.setFrom(JidCreate.fullFrom(REMOTE, Resourcepart.from("junit")));
m2.addExtension(new Received()).setId("1");
this.transformer.transform(
MessageTransformation.of(
m2, Instant.now(), REMOTE, null, m2.getFrom().asBareJid(), null));
final var messages = database.messageDao().getMessagesForTesting(1L);
final var message = Iterables.getOnlyElement(messages);
Assert.assertEquals(1L, message.states.size());
}
@Test
public void messageAndRetraction() throws XmppStringprepException {
final var m1 = new Message();
m1.setTo(ACCOUNT);
m1.setFrom(JidCreate.fullFrom(REMOTE, Resourcepart.from("junit")));
m1.setId("m1");
m1.addExtension(new Body("It is raining outside"));
this.transformer.transform(
MessageTransformation.of(
m1, Instant.now(), REMOTE, null, m1.getFrom().asBareJid(), null));
final var m2 = new Message();
m2.setTo(ACCOUNT);
m2.setFrom(JidCreate.fullFrom(REMOTE, Resourcepart.from("junit")));
m2.addExtension(new Retract()).setId("m1");
this.transformer.transform(
MessageTransformation.of(
m2, Instant.now(), REMOTE, null, m2.getFrom().asBareJid(), null));
final var messages = database.messageDao().getMessagesForTesting(1L);
final var message = Iterables.getOnlyElement(messages);
Assert.assertEquals(Modification.RETRACTION, message.modification);
Assert.assertEquals(
PartType.RETRACTION, Iterables.getOnlyElement(message.contents).partType);
}
@Test
public void twoChatThreeMessages() throws XmppStringprepException {
final var m1 = new Message();
m1.setId("1");
m1.setTo(REMOTE);
m1.setFrom(JidCreate.fullFrom(ACCOUNT, Resourcepart.from("junit")));
m1.addExtension(new Body("Hi. How are you?"));
this.transformer.transform(
MessageTransformation.of(
m1, Instant.now(), REMOTE, null, m1.getFrom().asBareJid(), null));
final var m2 = new Message();
m2.setId("2");
m2.setTo(REMOTE);
m2.setFrom(JidCreate.fullFrom(ACCOUNT, Resourcepart.from("junit")));
m2.addExtension(new Body("Please answer"));
this.transformer.transform(
MessageTransformation.of(
m2, Instant.now(), REMOTE, null, m2.getFrom().asBareJid(), null));
final var m3 = new Message();
m3.setId("3");
m3.setTo(REMOTE_2);
m3.setFrom(JidCreate.fullFrom(ACCOUNT, Resourcepart.from("junit")));
m3.addExtension(new Body("Another message"));
this.transformer.transform(
MessageTransformation.of(
m3, Instant.now(), REMOTE, null, m3.getFrom().asBareJid(), null));
}
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="2dp"
android:end="4dp"
android:start="4dp"
android:top="2dp">
<shape>
<solid android:color="?colorSurfaceVariant" />
<corners android:radius="12dp" />
</shape>
</item>
</layer-list>

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-feature
android:name="android.hardware.location"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<uses-feature
android:name="android.hardware.location.network"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />
<queries>
<package android:name="org.torproject.android" />
<intent>
<action android:name="eu.siacs.conversations.location.request" />
</intent>
<intent>
<action android:name="eu.siacs.conversations.location.show" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="resource/folder" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
</intent>
<intent>
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
</intent>
</queries>
<application
android:name="im.conversations.android.Conversations"
android:allowBackup="true"
android:appCategory="social"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/new_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/new_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Conversations3"
tools:targetApi="31">
<service
android:name=".service.ForegroundService"
android:exported="false"/>
<service
android:name=".service.RtpSessionService"
android:exported="false"
android:foregroundServiceType="phoneCall" />
<receiver
android:name=".receiver.EventReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
<action android:name="android.media.RINGER_MODE_CHANGED" />
</intent-filter>
</receiver>
<activity
android:name="im.conversations.android.ui.activity.MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="im.conversations.android.ui.activity.SettingsActivity" />
<activity
android:name="im.conversations.android.ui.activity.SetupActivity"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.activity.RtpSessionActivity"
android:autoRemoveFromRecents="true"
android:launchMode="singleInstance"
android:supportsPictureInPicture="true" />
</application>
</manifest>

View file

@ -0,0 +1,16 @@
<configuration xmlns="https://tony19.github.io/logback-android/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://tony19.github.io/logback-android/xml https://cdn.jsdelivr.net/gh/tony19/logback-android/logback.xsd">
<appender name="logcat" class="ch.qos.logback.classic.android.LogcatAppender">
<tagEncoder>
<pattern>conversations</pattern>
</tagEncoder>
<encoder>
<pattern>%logger{12}: %msg</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="logcat" />
</root>
</configuration>

View file

@ -0,0 +1,10 @@
package eu.siacs.conversations;
import android.net.Uri;
public class Config {
public static final String LOGTAG = "conversations";
public static final Uri HELP = Uri.parse("https://help.conversations.im");
public static final boolean REQUIRE_RTP_VERIFICATION =
false; // require a/v calls to be verified with OMEMO
}

View file

@ -0,0 +1,54 @@
package eu.siacs.conversations.generator;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import im.conversations.android.xml.Element;
import im.conversations.android.xml.Namespace;
import im.conversations.android.xmpp.manager.JingleConnectionManager;
import im.conversations.android.xmpp.model.stanza.Message;
import org.jxmpp.jid.Jid;
public final class MessageGenerator {
private MessageGenerator() {
throw new IllegalStateException("Do not instantiate me");
}
public static Message sessionProposal(
final JingleConnectionManager.RtpSessionProposal proposal) {
final Message packet = new Message(Message.Type.CHAT); // we want to carbon copy those
packet.setTo(proposal.with);
packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId);
final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", proposal.sessionId);
for (final Media media : proposal.media) {
propose.addChild("description", Namespace.JINGLE_APPS_RTP)
.setAttribute("media", media.toString());
}
packet.addChild("request", "urn:xmpp:receipts");
packet.addChild("store", "urn:xmpp:hints");
return packet;
}
public static Message sessionRetract(
final JingleConnectionManager.RtpSessionProposal proposal) {
final Message packet = new Message(Message.Type.CHAT); // we want to carbon copy those
packet.setTo(proposal.with);
final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", proposal.sessionId);
propose.addChild("description", Namespace.JINGLE_APPS_RTP);
packet.addChild("store", "urn:xmpp:hints");
return packet;
}
public static Message sessionReject(final Jid with, final String sessionId) {
final Message packet = new Message(Message.Type.CHAT); // we want to carbon copy those
packet.setTo(with);
final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", sessionId);
propose.addChild("description", Namespace.JINGLE_APPS_RTP);
packet.addChild("store", "urn:xmpp:hints");
return packet;
}
}

View file

@ -21,37 +21,28 @@ import android.media.AudioRecord;
import android.media.MediaRecorder; import android.media.MediaRecorder;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import eu.siacs.conversations.Config;
import org.webrtc.ThreadUtils; import eu.siacs.conversations.utils.AppRTCUtils;
import eu.siacs.conversations.xmpp.jingle.Media;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import org.webrtc.ThreadUtils;
import eu.siacs.conversations.Config; /** AppRTCAudioManager manages all audio related parts of the AppRTC demo. */
import eu.siacs.conversations.utils.AppRTCUtils;
import eu.siacs.conversations.xmpp.jingle.Media;
/**
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
*/
public class AppRTCAudioManager { public class AppRTCAudioManager {
private static CountDownLatch microphoneLatch; private static CountDownLatch microphoneLatch;
private final Context apprtcContext; private final Context apprtcContext;
// Contains speakerphone setting: auto, true or false // Contains speakerphone setting: auto, true or false
@Nullable @Nullable private SpeakerPhonePreference speakerPhonePreference;
private SpeakerPhonePreference speakerPhonePreference;
// Handles all tasks related to Bluetooth headset devices. // Handles all tasks related to Bluetooth headset devices.
private final AppRTCBluetoothManager bluetoothManager; private final AppRTCBluetoothManager bluetoothManager;
@Nullable @Nullable private final AudioManager audioManager;
private final AudioManager audioManager; @Nullable private AudioManagerEvents audioManagerEvents;
@Nullable
private AudioManagerEvents audioManagerEvents;
private AudioManagerState amState; private AudioManagerState amState;
private boolean savedIsSpeakerPhoneOn; private boolean savedIsSpeakerPhoneOn;
private boolean savedIsMicrophoneMute; private boolean savedIsMicrophoneMute;
@ -74,18 +65,17 @@ public class AppRTCAudioManager {
// relative to the view screen of a device and can therefore be used to // relative to the view screen of a device and can therefore be used to
// assist device switching (close to ear <=> use headset earpiece if // assist device switching (close to ear <=> use headset earpiece if
// available, far from ear <=> use speaker phone). // available, far from ear <=> use speaker phone).
@Nullable @Nullable private AppRTCProximitySensor proximitySensor;
private AppRTCProximitySensor proximitySensor;
// Contains a list of available audio devices. A Set collection is used to // Contains a list of available audio devices. A Set collection is used to
// avoid duplicate elements. // avoid duplicate elements.
private Set<AudioDevice> audioDevices = new HashSet<>(); private Set<AudioDevice> audioDevices = new HashSet<>();
// Broadcast receiver for wired headset intent broadcasts. // Broadcast receiver for wired headset intent broadcasts.
private final BroadcastReceiver wiredHeadsetReceiver; private final BroadcastReceiver wiredHeadsetReceiver;
// Callback method for changes in audio focus. // Callback method for changes in audio focus.
@Nullable @Nullable private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) { private AppRTCAudioManager(
Context context, final SpeakerPhonePreference speakerPhonePreference) {
Log.d(Config.LOGTAG, "ctor"); Log.d(Config.LOGTAG, "ctor");
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
apprtcContext = context; apprtcContext = context;
@ -102,7 +92,9 @@ public class AppRTCAudioManager {
// Create and initialize the proximity sensor. // Create and initialize the proximity sensor.
// Tablet devices (e.g. Nexus 7) does not support proximity sensors. // Tablet devices (e.g. Nexus 7) does not support proximity sensors.
// Note that, the sensor will not be active until start() has been called. // Note that, the sensor will not be active until start() has been called.
proximitySensor = AppRTCProximitySensor.create(context, proximitySensor =
AppRTCProximitySensor.create(
context,
// This method will be called each time a state change is detected. // This method will be called each time a state change is detected.
// Example: user holds his hand over the device (closer than ~5 cm), // Example: user holds his hand over the device (closer than ~5 cm),
// or removes his hand from the device. // or removes his hand from the device.
@ -121,10 +113,9 @@ public class AppRTCAudioManager {
updateAudioDeviceState(); updateAudioDeviceState();
} }
/** /** Construction. */
* Construction. public static AppRTCAudioManager create(
*/ Context context, SpeakerPhonePreference speakerPhonePreference) {
public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) {
return new AppRTCAudioManager(context, speakerPhonePreference); return new AppRTCAudioManager(context, speakerPhonePreference);
} }
@ -137,17 +128,18 @@ public class AppRTCAudioManager {
final int channel = AudioFormat.CHANNEL_IN_MONO; final int channel = AudioFormat.CHANNEL_IN_MONO;
final int format = AudioFormat.ENCODING_PCM_16BIT; final int format = AudioFormat.ENCODING_PCM_16BIT;
final int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format); final int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channel, format, bufferSize); audioRecord =
new AudioRecord(
MediaRecorder.AudioSource.MIC, sampleRate, channel, format, bufferSize);
audioRecord.startRecording(); audioRecord.startRecording();
final short[] buffer = new short[bufferSize]; final short[] buffer = new short[bufferSize];
final int audioStatus = audioRecord.read(buffer, 0, bufferSize); final int audioStatus = audioRecord.read(buffer, 0, bufferSize);
if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION || audioStatus == AudioRecord.STATE_UNINITIALIZED) if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION
available = false; || audioStatus == AudioRecord.STATE_UNINITIALIZED) available = false;
} catch (Exception e) { } catch (Exception e) {
available = false; available = false;
} finally { } finally {
release(audioRecord); release(audioRecord);
} }
microphoneLatch.countDown(); microphoneLatch.countDown();
return available; return available;
@ -165,8 +157,8 @@ public class AppRTCAudioManager {
} }
/** /**
* This method is called when the proximity sensor reports a state change, * This method is called when the proximity sensor reports a state change, e.g. from "NEAR to
* e.g. from "NEAR to FAR" or from "FAR to NEAR". * FAR" or from "FAR to NEAR".
*/ */
private void onProximitySensorChangedState() { private void onProximitySensorChangedState() {
if (speakerPhonePreference != SpeakerPhonePreference.AUTO) { if (speakerPhonePreference != SpeakerPhonePreference.AUTO) {
@ -174,7 +166,8 @@ public class AppRTCAudioManager {
} }
// The proximity sensor should only be activated when there are exactly two // The proximity sensor should only be activated when there are exactly two
// available audio devices. // available audio devices.
if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE) if (audioDevices.size() == 2
&& audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE)
&& audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) { && audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
if (proximitySensor.sensorReportsNearState()) { if (proximitySensor.sensorReportsNearState()) {
// Sensor reports that a "handset is being held up to a person's ear", // Sensor reports that a "handset is being held up to a person's ear",
@ -204,12 +197,17 @@ public class AppRTCAudioManager {
savedIsMicrophoneMute = audioManager.isMicrophoneMute(); savedIsMicrophoneMute = audioManager.isMicrophoneMute();
hasWiredHeadset = hasWiredHeadset(); hasWiredHeadset = hasWiredHeadset();
// Create an AudioManager.OnAudioFocusChangeListener instance. // Create an AudioManager.OnAudioFocusChangeListener instance.
audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { audioFocusChangeListener =
// Called on the listener to notify if the audio focus for this listener has been changed. new AudioManager.OnAudioFocusChangeListener() {
// The |focusChange| value indicates whether the focus was gained, whether the focus was lost, // Called on the listener to notify if the audio focus for this listener has
// and whether that loss is transient, or whether the new focus holder will hold it for an // been changed.
// The |focusChange| value indicates whether the focus was gained, whether the
// focus was lost,
// and whether that loss is transient, or whether the new focus holder will hold
// it for an
// unknown amount of time. // unknown amount of time.
// TODO(henrika): possibly extend support of handling audio-focus changes. Only contains // TODO(henrika): possibly extend support of handling audio-focus changes. Only
// contains
// logging for now. // logging for now.
@Override @Override
public void onAudioFocusChange(int focusChange) { public void onAudioFocusChange(int focusChange) {
@ -244,8 +242,11 @@ public class AppRTCAudioManager {
} }
}; };
// Request audio playout focus (without ducking) and install listener for changes in focus. // Request audio playout focus (without ducking) and install listener for changes in focus.
int result = audioManager.requestAudioFocus(audioFocusChangeListener, int result =
AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); audioManager.requestAudioFocus(
audioFocusChangeListener,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams"); Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams");
} else { } else {
@ -312,9 +313,7 @@ public class AppRTCAudioManager {
audioManagerEvents = null; audioManagerEvents = null;
} }
/** /** Changes selection of the currently active audio device. */
* Changes selection of the currently active audio device.
*/
private void setAudioDeviceInternal(AudioDevice device) { private void setAudioDeviceInternal(AudioDevice device) {
Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")"); Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
AppRTCUtils.assertIsTrue(audioDevices.contains(device)); AppRTCUtils.assertIsTrue(audioDevices.contains(device));
@ -335,8 +334,8 @@ public class AppRTCAudioManager {
} }
/** /**
* Changes default audio device. * Changes default audio device. TODO(henrika): add usage of this method in the AppRTCMobile
* TODO(henrika): add usage of this method in the AppRTCMobile client. * client.
*/ */
public void setDefaultAudioDevice(AudioDevice defaultDevice) { public void setDefaultAudioDevice(AudioDevice defaultDevice) {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
@ -359,9 +358,7 @@ public class AppRTCAudioManager {
updateAudioDeviceState(); updateAudioDeviceState();
} }
/** /** Changes selection of the currently active audio device. */
* Changes selection of the currently active audio device.
*/
public void selectAudioDevice(AudioDevice device) { public void selectAudioDevice(AudioDevice device) {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
if (!audioDevices.contains(device)) { if (!audioDevices.contains(device)) {
@ -371,39 +368,29 @@ public class AppRTCAudioManager {
updateAudioDeviceState(); updateAudioDeviceState();
} }
/** /** Returns current set of available/selectable audio devices. */
* Returns current set of available/selectable audio devices.
*/
public Set<AudioDevice> getAudioDevices() { public Set<AudioDevice> getAudioDevices() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
return Collections.unmodifiableSet(new HashSet<>(audioDevices)); return Collections.unmodifiableSet(new HashSet<>(audioDevices));
} }
/** /** Returns the currently selected audio device. */
* Returns the currently selected audio device.
*/
public AudioDevice getSelectedAudioDevice() { public AudioDevice getSelectedAudioDevice() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
return selectedAudioDevice; return selectedAudioDevice;
} }
/** /** Helper method for receiver registration. */
* Helper method for receiver registration.
*/
private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
apprtcContext.registerReceiver(receiver, filter); apprtcContext.registerReceiver(receiver, filter);
} }
/** /** Helper method for unregistration of an existing receiver. */
* Helper method for unregistration of an existing receiver.
*/
private void unregisterReceiver(BroadcastReceiver receiver) { private void unregisterReceiver(BroadcastReceiver receiver) {
apprtcContext.unregisterReceiver(receiver); apprtcContext.unregisterReceiver(receiver);
} }
/** /** Sets the speaker phone mode. */
* Sets the speaker phone mode.
*/
private void setSpeakerphoneOn(boolean on) { private void setSpeakerphoneOn(boolean on) {
boolean wasOn = audioManager.isSpeakerphoneOn(); boolean wasOn = audioManager.isSpeakerphoneOn();
if (wasOn == on) { if (wasOn == on) {
@ -412,9 +399,7 @@ public class AppRTCAudioManager {
audioManager.setSpeakerphoneOn(on); audioManager.setSpeakerphoneOn(on);
} }
/** /** Sets the microphone mute state. */
* Sets the microphone mute state.
*/
private void setMicrophoneMute(boolean on) { private void setMicrophoneMute(boolean on) {
boolean wasMuted = audioManager.isMicrophoneMute(); boolean wasMuted = audioManager.isMicrophoneMute();
if (wasMuted == on) { if (wasMuted == on) {
@ -423,19 +408,15 @@ public class AppRTCAudioManager {
audioManager.setMicrophoneMute(on); audioManager.setMicrophoneMute(on);
} }
/** /** Gets the current earpiece state. */
* Gets the current earpiece state.
*/
private boolean hasEarpiece() { private boolean hasEarpiece() {
return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
} }
/** /**
* Checks whether a wired headset is connected or not. * Checks whether a wired headset is connected or not. This is not a valid indication that audio
* This is not a valid indication that audio playback is actually over * playback is actually over the wired headset as audio routing depends on other conditions. We
* the wired headset as audio routing depends on other conditions. We * only use it as an early indicator (during initialization) of an attached wired headset.
* only use it as an early indicator (during initialization) of an attached
* wired headset.
*/ */
@Deprecated @Deprecated
private boolean hasWiredHeadset() { private boolean hasWiredHeadset() {
@ -458,18 +439,30 @@ public class AppRTCAudioManager {
} }
/** /**
* Updates list of possible audio devices and make new device selection. * Updates list of possible audio devices and make new device selection. TODO(henrika): add unit
* TODO(henrika): add unit test to verify all state transitions. * test to verify all state transitions.
*/ */
public void updateAudioDeviceState() { public void updateAudioDeviceState() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "--- updateAudioDeviceState: " Log.d(
+ "wired headset=" + hasWiredHeadset + ", " Config.LOGTAG,
+ "BT state=" + bluetoothManager.getState()); "--- updateAudioDeviceState: "
Log.d(Config.LOGTAG, "Device status: " + "wired headset="
+ "available=" + audioDevices + ", " + hasWiredHeadset
+ "selected=" + selectedAudioDevice + ", " + ", "
+ "user selected=" + userSelectedAudioDevice); + "BT state="
+ bluetoothManager.getState());
Log.d(
Config.LOGTAG,
"Device status: "
+ "available="
+ audioDevices
+ ", "
+ "selected="
+ selectedAudioDevice
+ ", "
+ "user selected="
+ userSelectedAudioDevice);
// Check if any Bluetooth headset is connected. The internal BT state will // Check if any Bluetooth headset is connected. The internal BT state will
// change accordingly. // change accordingly.
// TODO(henrika): perhaps wrap required state into BT manager. // TODO(henrika): perhaps wrap required state into BT manager.
@ -526,15 +519,23 @@ public class AppRTCAudioManager {
// Bluetooth SCO connection is established or in the process. // Bluetooth SCO connection is established or in the process.
boolean needBluetoothAudioStop = boolean needBluetoothAudioStop =
(bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING) || bluetoothManager.getState()
== AppRTCBluetoothManager.State.SCO_CONNECTING)
&& (userSelectedAudioDevice != AudioDevice.NONE && (userSelectedAudioDevice != AudioDevice.NONE
&& userSelectedAudioDevice != AudioDevice.BLUETOOTH); && userSelectedAudioDevice != AudioDevice.BLUETOOTH);
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) { || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
Log.d(Config.LOGTAG, "Need BT audio: start=" + needBluetoothAudioStart + ", " Log.d(
+ "stop=" + needBluetoothAudioStop + ", " Config.LOGTAG,
+ "BT state=" + bluetoothManager.getState()); "Need BT audio: start="
+ needBluetoothAudioStart
+ ", "
+ "stop="
+ needBluetoothAudioStop
+ ", "
+ "BT state="
+ bluetoothManager.getState());
} }
// Start or stop Bluetooth SCO connection given states set earlier. // Start or stop Bluetooth SCO connection given states set earlier.
if (needBluetoothAudioStop) { if (needBluetoothAudioStop) {
@ -563,7 +564,8 @@ public class AppRTCAudioManager {
} else { } else {
// No wired headset and no Bluetooth, hence the audio-device list can contain speaker // No wired headset and no Bluetooth, hence the audio-device list can contain speaker
// phone (on a tablet), or speaker phone and earpiece (on mobile phone). // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
// |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or
// AudioDevice.EARPIECE
// depending on the user's selection. // depending on the user's selection.
newAudioDevice = defaultAudioDevice; newAudioDevice = defaultAudioDevice;
} }
@ -571,9 +573,14 @@ public class AppRTCAudioManager {
if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) { if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
// Do the required device switch. // Do the required device switch.
setAudioDeviceInternal(newAudioDevice); setAudioDeviceInternal(newAudioDevice);
Log.d(Config.LOGTAG, "New device status: " Log.d(
+ "available=" + audioDevices + ", " Config.LOGTAG,
+ "selected=" + newAudioDevice); "New device status: "
+ "available="
+ audioDevices
+ ", "
+ "selected="
+ newAudioDevice);
if (audioManagerEvents != null) { if (audioManagerEvents != null) {
// Notify a listening client that audio device has been changed. // Notify a listening client that audio device has been changed.
audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices); audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
@ -582,15 +589,16 @@ public class AppRTCAudioManager {
Log.d(Config.LOGTAG, "--- updateAudioDeviceState done"); Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
} }
/** /** AudioDevice is the names of possible audio devices that we currently support. */
* AudioDevice is the names of possible audio devices that we currently public enum AudioDevice {
* support. SPEAKER_PHONE,
*/ WIRED_HEADSET,
public enum AudioDevice {SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE} EARPIECE,
BLUETOOTH,
NONE
}
/** /** AudioManager state. */
* AudioManager state.
*/
public enum AudioManagerState { public enum AudioManagerState {
UNINITIALIZED, UNINITIALIZED,
PREINITIALIZED, PREINITIALIZED,
@ -598,7 +606,9 @@ public class AppRTCAudioManager {
} }
public enum SpeakerPhonePreference { public enum SpeakerPhonePreference {
AUTO, EARPIECE, SPEAKER; AUTO,
EARPIECE,
SPEAKER;
public static SpeakerPhonePreference of(final Set<Media> media) { public static SpeakerPhonePreference of(final Set<Media> media) {
if (media.contains(Media.VIDEO)) { if (media.contains(Media.VIDEO)) {
@ -609,9 +619,7 @@ public class AppRTCAudioManager {
} }
} }
/** /** Selected audio device change event. */
* Selected audio device change event.
*/
public interface AudioManagerEvents { public interface AudioManagerEvents {
// Callback fired once audio device is changed or list of available audio devices changed. // Callback fired once audio device is changed or list of available audio devices changed.
void onAudioDeviceChanged( void onAudioDeviceChanged(
@ -630,10 +638,20 @@ public class AppRTCAudioManager {
int state = intent.getIntExtra("state", STATE_UNPLUGGED); int state = intent.getIntExtra("state", STATE_UNPLUGGED);
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
String name = intent.getStringExtra("name"); String name = intent.getStringExtra("name");
Log.d(Config.LOGTAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": " Log.d(
+ "a=" + intent.getAction() + ", s=" Config.LOGTAG,
+ (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m=" "WiredHeadsetReceiver.onReceive"
+ (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb=" + AppRTCUtils.getThreadInfo()
+ ": "
+ "a="
+ intent.getAction()
+ ", s="
+ (state == STATE_UNPLUGGED ? "unplugged" : "plugged")
+ ", m="
+ (microphone == HAS_MIC ? "mic" : "no mic")
+ ", n="
+ name
+ ", sb="
+ isInitialStickyBroadcast()); + isInitialStickyBroadcast());
hasWiredHeadset = (state == STATE_PLUGGED); hasWiredHeadset = (state == STATE_PLUGGED);
updateAudioDeviceState(); updateAudioDeviceState();

View file

@ -25,19 +25,14 @@ import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import org.webrtc.ThreadUtils;
import java.util.Collections;
import java.util.List;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.AppRTCUtils; import eu.siacs.conversations.utils.AppRTCUtils;
import java.util.Collections;
import java.util.List;
import org.webrtc.ThreadUtils;
/** AppRTCProximitySensor manages functions related to Bluetoth devices in the AppRTC demo. */ /** AppRTCProximitySensor manages functions related to Bluetoth devices in the AppRTC demo. */
public class AppRTCBluetoothManager { public class AppRTCBluetoothManager {

View file

@ -16,22 +16,17 @@ import android.hardware.SensorEventListener;
import android.hardware.SensorManager; import android.hardware.SensorManager;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.webrtc.ThreadUtils;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.AppRTCUtils; import eu.siacs.conversations.utils.AppRTCUtils;
import org.webrtc.ThreadUtils;
/** /**
* AppRTCProximitySensor manages functions related to the proximity sensor in * AppRTCProximitySensor manages functions related to the proximity sensor in the AppRTC demo. On
* the AppRTC demo. * most device, the proximity sensor is implemented as a boolean-sensor. It returns just two values
* On most device, the proximity sensor is implemented as a boolean-sensor. * "NEAR" or "FAR". Thresholding is done on the LUX value i.e. the LUX value of the light sensor is
* It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX * compared with a threshold. A LUX-value more than the threshold means the proximity sensor returns
* value i.e. the LUX value of the light sensor is compared with a threshold. * "FAR". Anything less than the threshold value and the sensor returns "NEAR".
* A LUX-value more than the threshold means the proximity sensor returns "FAR".
* Anything less than the threshold value and the sensor returns "NEAR".
*/ */
public class AppRTCProximitySensor implements SensorEventListener { public class AppRTCProximitySensor implements SensorEventListener {
// This class should be created, started and stopped on one thread // This class should be created, started and stopped on one thread
@ -40,8 +35,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker();
private final Runnable onSensorStateListener; private final Runnable onSensorStateListener;
private final SensorManager sensorManager; private final SensorManager sensorManager;
@Nullable @Nullable private Sensor proximitySensor;
private Sensor proximitySensor;
private boolean lastStateReportIsNear; private boolean lastStateReportIsNear;
private AppRTCProximitySensor(Context context, Runnable sensorStateListener) { private AppRTCProximitySensor(Context context, Runnable sensorStateListener) {
@ -50,17 +44,12 @@ public class AppRTCProximitySensor implements SensorEventListener {
sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)); sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE));
} }
/** /** Construction */
* Construction
*/
static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) { static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) {
return new AppRTCProximitySensor(context, sensorStateListener); return new AppRTCProximitySensor(context, sensorStateListener);
} }
/** /** Activate the proximity sensor. Also do initialization if called for the first time. */
* Activate the proximity sensor. Also do initialization if called for the
* first time.
*/
public boolean start() { public boolean start() {
threadChecker.checkIsOnValidThread(); threadChecker.checkIsOnValidThread();
Log.d(Config.LOGTAG, "start" + AppRTCUtils.getThreadInfo()); Log.d(Config.LOGTAG, "start" + AppRTCUtils.getThreadInfo());
@ -72,9 +61,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
return true; return true;
} }
/** /** Deactivate the proximity sensor. */
* Deactivate the proximity sensor.
*/
public void stop() { public void stop() {
threadChecker.checkIsOnValidThread(); threadChecker.checkIsOnValidThread();
Log.d(Config.LOGTAG, "stop" + AppRTCUtils.getThreadInfo()); Log.d(Config.LOGTAG, "stop" + AppRTCUtils.getThreadInfo());
@ -84,9 +71,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
sensorManager.unregisterListener(this, proximitySensor); sensorManager.unregisterListener(this, proximitySensor);
} }
/** /** Getter for last reported state. Set to true if "near" is reported. */
* Getter for last reported state. Set to true if "near" is reported.
*/
public boolean sensorReportsNearState() { public boolean sensorReportsNearState() {
threadChecker.checkIsOnValidThread(); threadChecker.checkIsOnValidThread();
return lastStateReportIsNear; return lastStateReportIsNear;
@ -120,15 +105,22 @@ public class AppRTCProximitySensor implements SensorEventListener {
if (onSensorStateListener != null) { if (onSensorStateListener != null) {
onSensorStateListener.run(); onSensorStateListener.run();
} }
Log.d(Config.LOGTAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": " Log.d(
+ "accuracy=" + event.accuracy + ", timestamp=" + event.timestamp + ", distance=" Config.LOGTAG,
"onSensorChanged"
+ AppRTCUtils.getThreadInfo()
+ ": "
+ "accuracy="
+ event.accuracy
+ ", timestamp="
+ event.timestamp
+ ", distance="
+ event.values[0]); + event.values[0]);
} }
/** /**
* Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) does not support
* does not support this type of sensor and false will be returned in such * this type of sensor and false will be returned in such cases.
* cases.
*/ */
private boolean initDefaultSensor() { private boolean initDefaultSensor() {
if (proximitySensor != null) { if (proximitySensor != null) {
@ -142,9 +134,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
return true; return true;
} }
/** /** Helper method for logging information about the proximity sensor. */
* Helper method for logging information about the proximity sensor.
*/
private void logProximitySensorInfo() { private void logProximitySensorInfo() {
if (proximitySensor == null) { if (proximitySensor == null) {
return; return;

View file

@ -0,0 +1,66 @@
/*
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package eu.siacs.conversations.utils;
import android.os.Build;
import android.util.Log;
/** AppRTCUtils provides helper functions for managing thread safety. */
public final class AppRTCUtils {
private AppRTCUtils() {}
/** Helper method which throws an exception when an assertion has failed. */
public static void assertIsTrue(boolean condition) {
if (!condition) {
throw new AssertionError("Expected condition to be true");
}
}
/** Helper method for building a string of thread information. */
public static String getThreadInfo() {
return "@[name="
+ Thread.currentThread().getName()
+ ", id="
+ Thread.currentThread().getId()
+ "]";
}
/** Information about the current build, taken from system properties. */
public static void logDeviceInfo(String tag) {
Log.d(
tag,
"Android SDK: "
+ Build.VERSION.SDK_INT
+ ", "
+ "Release: "
+ Build.VERSION.RELEASE
+ ", "
+ "Brand: "
+ Build.BRAND
+ ", "
+ "Device: "
+ Build.DEVICE
+ ", "
+ "Id: "
+ Build.ID
+ ", "
+ "Hardware: "
+ Build.HARDWARE
+ ", "
+ "Manufacturer: "
+ Build.MANUFACTURER
+ ", "
+ "Model: "
+ Build.MODEL
+ ", "
+ "Product: "
+ Build.PRODUCT);
}
}

View file

@ -0,0 +1,119 @@
package eu.siacs.conversations.xmpp.jingle;
import android.content.Context;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import im.conversations.android.IDs;
import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.model.stanza.Iq;
import org.jxmpp.jid.Jid;
public abstract class AbstractJingleConnection extends XmppConnection.Delegate {
public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-";
protected final Id id;
private final Jid initiator;
AbstractJingleConnection(
final Context context,
final XmppConnection connection,
final Id id,
final Jid initiator) {
super(context, connection);
this.id = id;
this.initiator = initiator;
}
boolean isInitiator() {
return initiator.equals(connection.getBoundAddress());
}
public abstract void deliverPacket(Iq jinglePacket);
public Id getId() {
return id;
}
public abstract void notifyRebound();
public static class Id implements OngoingRtpSession {
public final Jid with;
public final String sessionId;
private Id(final Jid with, final String sessionId) {
Preconditions.checkNotNull(with);
Preconditions.checkNotNull(sessionId);
this.with = with;
this.sessionId = sessionId;
}
public static Id of(final JinglePacket jinglePacket) {
return new Id(jinglePacket.getFrom(), jinglePacket.getSessionId());
}
public static Id of(Jid with, final String sessionId) {
return new Id(with, sessionId);
}
public static Id of(Jid with) {
return new Id(with, IDs.medium());
}
@Override
public Jid getWith() {
return with;
}
@Override
public String getSessionId() {
return sessionId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Id id = (Id) o;
return Objects.equal(with, id.with) && Objects.equal(sessionId, id.sessionId);
}
@Override
public int hashCode() {
return Objects.hashCode(with, sessionId);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("with", with)
.add("sessionId", sessionId)
.toString();
}
}
public enum State {
NULL, // default value; nothing has been sent or received yet
PROPOSED,
ACCEPTED,
PROCEED,
REJECTED,
REJECTED_RACED, // used when we want to reject but havent received session init yet
RETRACTED,
RETRACTED_RACED, // used when receiving a retract after we already asked to proceed
SESSION_INITIALIZED, // equal to 'PENDING'
SESSION_INITIALIZED_PRE_APPROVED,
SESSION_ACCEPTED, // equal to 'ACTIVE'
TERMINATED_SUCCESS, // equal to 'ENDED' (after successful call) ui will just close
TERMINATED_DECLINED_OR_BUSY, // equal to 'ENDED' (after other party declined the call)
TERMINATED_CONNECTIVITY_ERROR, // equal to 'ENDED' (but after network failures; ui will
// display retry button)
TERMINATED_CANCEL_OR_TIMEOUT, // more or less the same as retracted; caller pressed end call
// before session was accepted
TERMINATED_APPLICATION_FAILURE,
TERMINATED_SECURITY_ERROR
}
}

View file

@ -4,10 +4,8 @@ import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import java.util.Set;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import java.util.Set;
public final class ContentAddition { public final class ContentAddition {

View file

@ -10,8 +10,7 @@ import java.util.ArrayList;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.jxmpp.jid.Jid;
import eu.siacs.conversations.xmpp.Jid;
public class DirectConnectionUtils { public class DirectConnectionUtils {
@ -25,7 +24,8 @@ public class DirectConnectionUtils {
} }
while (interfaces.hasMoreElements()) { while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement(); NetworkInterface networkInterface = interfaces.nextElement();
final Enumeration<InetAddress> inetAddressEnumeration = networkInterface.getInetAddresses(); final Enumeration<InetAddress> inetAddressEnumeration =
networkInterface.getInetAddresses();
while (inetAddressEnumeration.hasMoreElements()) { while (inetAddressEnumeration.hasMoreElements()) {
final InetAddress inetAddress = inetAddressEnumeration.nextElement(); final InetAddress inetAddress = inetAddressEnumeration.nextElement();
if (inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) { if (inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) {
@ -50,7 +50,8 @@ public class DirectConnectionUtils {
SecureRandom random = new SecureRandom(); SecureRandom random = new SecureRandom();
ArrayList<JingleCandidate> candidates = new ArrayList<>(); ArrayList<JingleCandidate> candidates = new ArrayList<>();
for (InetAddress inetAddress : getLocalAddresses()) { for (InetAddress inetAddress : getLocalAddresses()) {
final JingleCandidate candidate = new JingleCandidate(UUID.randomUUID().toString(), true); final JingleCandidate candidate =
new JingleCandidate(UUID.randomUUID().toString(), true);
candidate.setHost(inetAddress.getHostAddress()); candidate.setHost(inetAddress.getHostAddress());
candidate.setPort(random.nextInt(60000) + 1024); candidate.setPort(random.nextInt(60000) + 1024);
candidate.setType(JingleCandidate.TYPE_DIRECT); candidate.setType(JingleCandidate.TYPE_DIRECT);
@ -60,5 +61,4 @@ public class DirectConnectionUtils {
} }
return candidates; return candidates;
} }
} }

View file

@ -0,0 +1,153 @@
package eu.siacs.conversations.xmpp.jingle;
import im.conversations.android.xml.Element;
import java.util.ArrayList;
import java.util.List;
import org.jxmpp.jid.Jid;
public class JingleCandidate {
public static int TYPE_UNKNOWN;
public static int TYPE_DIRECT = 0;
public static int TYPE_PROXY = 1;
private final boolean ours;
private boolean usedByCounterpart = false;
private final String cid;
private String host;
private int port;
private int type;
private Jid jid;
private int priority;
public JingleCandidate(String cid, boolean ours) {
this.ours = ours;
this.cid = cid;
}
public String getCid() {
return cid;
}
public void setHost(String host) {
this.host = host;
}
public String getHost() {
return this.host;
}
public void setJid(final Jid jid) {
this.jid = jid;
}
public Jid getJid() {
return this.jid;
}
public void setPort(int port) {
this.port = port;
}
public int getPort() {
return this.port;
}
public void setType(int type) {
this.type = type;
}
public void setType(String type) {
if (type == null) {
this.type = TYPE_UNKNOWN;
return;
}
switch (type) {
case "proxy":
this.type = TYPE_PROXY;
break;
case "direct":
this.type = TYPE_DIRECT;
break;
default:
this.type = TYPE_UNKNOWN;
break;
}
}
public void setPriority(int i) {
this.priority = i;
}
public int getPriority() {
return this.priority;
}
public boolean equals(JingleCandidate other) {
return this.getCid().equals(other.getCid());
}
public boolean equalValues(JingleCandidate other) {
return other != null
&& other.getHost().equals(this.getHost())
&& (other.getPort() == this.getPort());
}
public boolean isOurs() {
return ours;
}
public int getType() {
return this.type;
}
public static List<JingleCandidate> parse(final List<Element> elements) {
final List<JingleCandidate> candidates = new ArrayList<>();
for (final Element element : elements) {
if ("candidate".equals(element.getName())) {
candidates.add(JingleCandidate.parse(element));
}
}
return candidates;
}
public static JingleCandidate parse(Element element) {
final JingleCandidate candidate = new JingleCandidate(element.getAttribute("cid"), false);
candidate.setHost(element.getAttribute("host"));
candidate.setJid(element.getAttributeAsJid("jid"));
candidate.setType(element.getAttribute("type"));
candidate.setPriority(Integer.parseInt(element.getAttribute("priority")));
candidate.setPort(Integer.parseInt(element.getAttribute("port")));
return candidate;
}
public Element toElement() {
Element element = new Element("candidate");
element.setAttribute("cid", this.getCid());
element.setAttribute("host", this.getHost());
element.setAttribute("port", Integer.toString(this.getPort()));
if (jid != null) {
element.setAttribute("jid", jid);
}
element.setAttribute("priority", Integer.toString(this.getPriority()));
if (this.getType() == TYPE_DIRECT) {
element.setAttribute("type", "direct");
} else if (this.getType() == TYPE_PROXY) {
element.setAttribute("type", "proxy");
}
return element;
}
public void flagAsUsedByCounterpart() {
this.usedByCounterpart = true;
}
public boolean isUsedByCounterpart() {
return this.usedByCounterpart;
}
public String toString() {
return String.format(
"%s:%s (priority=%s,ours=%s)", getHost(), getPort(), getPriority(), isOurs());
}
}

View file

@ -1,15 +1,14 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
public enum Media { public enum Media {
VIDEO,
VIDEO, AUDIO, UNKNOWN; AUDIO,
UNKNOWN;
@Override @Override
@Nonnull @Nonnull

View file

@ -1,7 +1,6 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import java.util.List; import java.util.List;
public class MediaBuilder { public class MediaBuilder {
@ -43,6 +42,7 @@ public class MediaBuilder {
} }
public SessionDescription.Media createMedia() { public SessionDescription.Media createMedia() {
return new SessionDescription.Media(media, port, protocol, formats, connectionData, attributes); return new SessionDescription.Media(
media, port, protocol, formats, connectionData, attributes);
} }
} }

View file

@ -2,17 +2,16 @@ package eu.siacs.conversations.xmpp.jingle;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import im.conversations.android.axolotl.AxolotlService;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import org.whispersystems.libsignal.IdentityKey;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
public class OmemoVerification { public class OmemoVerification {
private final AtomicBoolean deviceIdWritten = new AtomicBoolean(false); private final AtomicBoolean deviceIdWritten = new AtomicBoolean(false);
private final AtomicBoolean sessionFingerprintWritten = new AtomicBoolean(false); private final AtomicBoolean identityKeyWritten = new AtomicBoolean(false);
private Integer deviceId; private Integer deviceId;
private String sessionFingerprint; private IdentityKey identityKey;
public void setDeviceId(final Integer id) { public void setDeviceId(final Integer id) {
if (deviceIdWritten.compareAndSet(false, true)) { if (deviceIdWritten.compareAndSet(false, true)) {
@ -31,31 +30,32 @@ public class OmemoVerification {
return this.deviceId != null; return this.deviceId != null;
} }
public void setSessionFingerprint(final String fingerprint) { public void setSessionFingerprint(final IdentityKey identityKey) {
Preconditions.checkNotNull(fingerprint, "Session fingerprint must not be null"); Preconditions.checkNotNull(identityKey, "IdentityKey must not be null");
if (sessionFingerprintWritten.compareAndSet(false, true)) { if (identityKeyWritten.compareAndSet(false, true)) {
this.sessionFingerprint = fingerprint; this.identityKey = identityKey;
return; return;
} }
throw new IllegalStateException("Session fingerprint has already been set"); throw new IllegalStateException("Identity Key has already been set");
} }
public String getFingerprint() { public IdentityKey getFingerprint() {
return this.sessionFingerprint; return this.identityKey;
} }
public void setOrEnsureEqual(AxolotlService.OmemoVerifiedPayload<?> omemoVerifiedPayload) { public void setOrEnsureEqual(AxolotlService.OmemoVerifiedPayload<?> omemoVerifiedPayload) {
setOrEnsureEqual(omemoVerifiedPayload.getDeviceId(), omemoVerifiedPayload.getFingerprint()); setOrEnsureEqual(omemoVerifiedPayload.getDeviceId(), omemoVerifiedPayload.getFingerprint());
} }
public void setOrEnsureEqual(final int deviceId, final String sessionFingerprint) { public void setOrEnsureEqual(final int deviceId, final IdentityKey identityKey) {
Preconditions.checkNotNull(sessionFingerprint, "Session fingerprint must not be null"); Preconditions.checkNotNull(identityKey, "IdentityKey must not be null");
if (this.deviceIdWritten.get() || this.sessionFingerprintWritten.get()) { if (this.deviceIdWritten.get() || this.identityKeyWritten.get()) {
if (this.sessionFingerprint == null) { if (this.identityKey == null) {
throw new IllegalStateException("No session fingerprint has been previously provided"); throw new IllegalStateException(
"No session fingerprint has been previously provided");
} }
if (!sessionFingerprint.equals(this.sessionFingerprint)) { if (!identityKey.equals(this.identityKey)) {
throw new SecurityException("Session Fingerprints did not match"); throw new SecurityException("IdentityKeys did not match");
} }
if (this.deviceId == null) { if (this.deviceId == null) {
throw new IllegalStateException("No Device Id has been previously provided"); throw new IllegalStateException("No Device Id has been previously provided");
@ -64,20 +64,20 @@ public class OmemoVerification {
throw new IllegalStateException("Device Ids did not match"); throw new IllegalStateException("Device Ids did not match");
} }
} else { } else {
this.setSessionFingerprint(sessionFingerprint); this.setSessionFingerprint(identityKey);
this.setDeviceId(deviceId); this.setDeviceId(deviceId);
} }
} }
public boolean hasFingerprint() { public boolean hasFingerprint() {
return this.sessionFingerprint != null; return this.identityKey != null;
} }
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("deviceId", deviceId) .add("deviceId", deviceId)
.add("fingerprint", sessionFingerprint) .add("fingerprint", identityKey)
.toString(); .toString();
} }
} }

View file

@ -1,19 +1,20 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import java.util.Map;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
import java.util.Map;
public class OmemoVerifiedRtpContentMap extends RtpContentMap { public class OmemoVerifiedRtpContentMap extends RtpContentMap {
public OmemoVerifiedRtpContentMap(Group group, Map<String, DescriptionTransport> contents) { public OmemoVerifiedRtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
super(group, contents); super(group, contents);
for (final DescriptionTransport descriptionTransport : contents.values()) { for (final DescriptionTransport descriptionTransport : contents.values()) {
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) { if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint(); ((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport)
.ensureNoPlaintextFingerprint();
continue; continue;
} }
throw new IllegalStateException("OmemoVerifiedRtpContentMap contains non-verified transport info"); throw new IllegalStateException(
"OmemoVerifiedRtpContentMap contains non-verified transport info");
} }
} }
} }

View file

@ -1,10 +1,9 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import eu.siacs.conversations.entities.Account; import org.jxmpp.jid.Jid;
import eu.siacs.conversations.xmpp.Jid;
public interface OngoingRtpSession { public interface OngoingRtpSession {
Account getAccount();
Jid getWith(); Jid getWith();
String getSessionId(); String getSessionId();
} }

View file

@ -12,15 +12,6 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
@ -29,6 +20,12 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
public class RtpContentMap { public class RtpContentMap {
@ -136,7 +133,8 @@ public class RtpContentMap {
if (setup == null) { if (setup == null) {
throw new SecurityException( throw new SecurityException(
String.format( String.format(
"Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", "Use of DTLS-SRTP (XEP-0320) is required for content %s but"
+ " missing setup attribute",
entry.getKey())); entry.getKey()));
} }
if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) { if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
@ -317,7 +315,8 @@ public class RtpContentMap {
} }
public RtpContentMap activeContents() { public RtpContentMap activeContents() {
return new RtpContentMap(group, Maps.filterValues(this.contents, dt -> dt.senders != Content.Senders.NONE)); return new RtpContentMap(
group, Maps.filterValues(this.contents, dt -> dt.senders != Content.Senders.NONE));
} }
public Diff diff(final RtpContentMap rtpContentMap) { public Diff diff(final RtpContentMap rtpContentMap) {

View file

@ -0,0 +1,21 @@
package eu.siacs.conversations.xmpp.jingle;
public enum RtpEndUserState {
INCOMING_CALL, // received a 'propose' message
CONNECTING, // session-initiate or session-accepted but no webrtc peer connection yet
CONNECTED, // session-accepted and webrtc peer connection is connected
RECONNECTING, // session-accepted and webrtc peer connection was connected once but is currently
// disconnected or failed
INCOMING_CONTENT_ADD, // session-accepted with a pending, incoming content-add
FINDING_DEVICE, // 'propose' has been sent out; no 184 ack yet
RINGING, // 'propose' has been sent out and it has been 184 acked
ACCEPTING_CALL, // 'proceed' message has been sent; but no session-initiate has been received
ENDING_CALL, // libwebrt says 'closed' but session-terminate hasnt gone through
ENDED, // close UI
DECLINED_OR_BUSY, // other party declined; no retry button
CONNECTIVITY_ERROR, // network error; retry button
CONNECTIVITY_LOST_ERROR, // network error but for call duration > 0
RETRACTED, // user pressed home or power button during 'ringing' - shows retry button
APPLICATION_ERROR, // something rather bad happened; libwebrtc failed or we got in IQ-error
SECURITY_ERROR // problem with DTLS (missing) or verification
}

View file

@ -2,24 +2,20 @@ package eu.siacs.conversations.xmpp.jingle;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.common.base.CharMatcher; import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import im.conversations.android.xml.Namespace;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class SessionDescription { public class SessionDescription {
@ -128,7 +124,8 @@ public class SessionDescription {
return sessionDescriptionBuilder.createSessionDescription(); return sessionDescriptionBuilder.createSessionDescription();
} }
public static SessionDescription of(final RtpContentMap contentMap, final boolean isInitiatorContentMap) { public static SessionDescription of(
final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
final ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create(); final ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>(); final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>();
@ -297,7 +294,8 @@ public class SessionDescription {
mediaAttributes.put("mid", name); mediaAttributes.put("mid", name);
mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), ""); mediaAttributes.put(
descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), "");
if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP) || group != null) { if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP) || group != null) {
mediaAttributes.put("rtcp-mux", ""); mediaAttributes.put("rtcp-mux", "");
} }

View file

@ -1,7 +1,6 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import java.util.List; import java.util.List;
public class SessionDescriptionBuilder { public class SessionDescriptionBuilder {

View file

@ -1,20 +1,19 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import static java.util.Arrays.asList;
import android.content.Context; import android.content.Context;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.ToneGenerator; import android.media.ToneGenerator;
import android.util.Log; import android.util.Log;
import eu.siacs.conversations.Config;
import im.conversations.android.xmpp.manager.JingleConnectionManager;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import eu.siacs.conversations.Config; public class ToneManager {
import static java.util.Arrays.asList;
class ToneManager {
private final ToneGenerator toneGenerator; private final ToneGenerator toneGenerator;
private final Context context; private final Context context;
@ -25,7 +24,9 @@ class ToneManager {
private ScheduledFuture<?> currentResetFuture; private ScheduledFuture<?> currentResetFuture;
private boolean appRtcAudioManagerHasControl = false; private boolean appRtcAudioManagerHasControl = false;
ToneManager(final Context context) { private static volatile ToneManager INSTANCE;
private ToneManager(final Context context) {
ToneGenerator toneGenerator; ToneGenerator toneGenerator;
try { try {
toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 60); toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 60);
@ -34,12 +35,17 @@ class ToneManager {
toneGenerator = null; toneGenerator = null;
} }
this.toneGenerator = toneGenerator; this.toneGenerator = toneGenerator;
this.context = context; this.context = context.getApplicationContext();
} }
private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) { private static ToneState of(
final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
if (isInitiator) { if (isInitiator) {
if (asList(RtpEndUserState.FINDING_DEVICE, RtpEndUserState.RINGING, RtpEndUserState.CONNECTING).contains(state)) { if (asList(
RtpEndUserState.FINDING_DEVICE,
RtpEndUserState.RINGING,
RtpEndUserState.CONNECTING)
.contains(state)) {
return ToneState.RINGING; return ToneState.RINGING;
} }
if (state == RtpEndUserState.DECLINED_OR_BUSY) { if (state == RtpEndUserState.DECLINED_OR_BUSY) {
@ -67,15 +73,17 @@ class ToneManager {
return ToneState.NULL; return ToneState.NULL;
} }
void transition(final RtpEndUserState state, final Set<Media> media) { public void transition(final RtpEndUserState state, final Set<Media> media) {
transition(state, of(true, state, media), media); transition(state, of(true, state, media), media);
} }
void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) { void transition(
final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
transition(state, of(isInitiator, state, media), media); transition(state, of(isInitiator, state, media), media);
} }
private synchronized void transition(final RtpEndUserState endUserState, final ToneState state, final Set<Media> media) { private synchronized void transition(
final RtpEndUserState endUserState, final ToneState state, final Set<Media> media) {
final RtpEndUserState normalizeEndUserState = normalize(endUserState); final RtpEndUserState normalizeEndUserState = normalize(endUserState);
if (this.endUserState == normalizeEndUserState) { if (this.endUserState == normalizeEndUserState) {
return; return;
@ -133,29 +141,50 @@ class ToneManager {
} }
private void scheduleConnected() { private void scheduleConnected() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { this.currentTone =
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
() -> {
startTone(ToneGenerator.TONE_PROP_PROMPT, 200); startTone(ToneGenerator.TONE_PROP_PROMPT, 200);
}, 0, TimeUnit.SECONDS); },
0,
TimeUnit.SECONDS);
} }
private void scheduleEnding() { private void scheduleEnding() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { this.currentTone =
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
() -> {
startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
}, 0, TimeUnit.SECONDS); },
this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 375, TimeUnit.MILLISECONDS); 0,
TimeUnit.SECONDS);
this.currentResetFuture =
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
this::resetAudioManager, 375, TimeUnit.MILLISECONDS);
} }
private void scheduleBusy() { private void scheduleBusy() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { this.currentTone =
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
() -> {
startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
}, 0, TimeUnit.SECONDS); },
this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 2500, TimeUnit.MILLISECONDS); 0,
TimeUnit.SECONDS);
this.currentResetFuture =
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
this::resetAudioManager, 2500, TimeUnit.MILLISECONDS);
} }
private void scheduleWaitingTone() { private void scheduleWaitingTone() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> { this.currentTone =
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
() -> {
startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750); startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750);
}, 0, 3, TimeUnit.SECONDS); },
0,
3,
TimeUnit.SECONDS);
} }
private boolean noResetScheduled() { private boolean noResetScheduled() {
@ -181,34 +210,65 @@ class ToneManager {
private void configureAudioManagerForCall(final Set<Media> media) { private void configureAudioManagerForCall(final Set<Media> media) {
if (appRtcAudioManagerHasControl) { if (appRtcAudioManagerHasControl) {
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not configure audio manager because RTC has control"); Log.d(
Config.LOGTAG,
ToneManager.class.getName()
+ ": do not configure audio manager because RTC has control");
return; return;
} }
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); final AudioManager audioManager =
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (audioManager == null) { if (audioManager == null) {
return; return;
} }
final boolean isSpeakerPhone = media.contains(Media.VIDEO); final boolean isSpeakerPhone = media.contains(Media.VIDEO);
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager into communication mode. speaker=" + isSpeakerPhone); Log.d(
Config.LOGTAG,
ToneManager.class.getName()
+ ": putting AudioManager into communication mode. speaker="
+ isSpeakerPhone);
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.setSpeakerphoneOn(isSpeakerPhone); audioManager.setSpeakerphoneOn(isSpeakerPhone);
} }
private void resetAudioManager() { private void resetAudioManager() {
if (appRtcAudioManagerHasControl) { if (appRtcAudioManagerHasControl) {
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not reset audio manager because RTC has control"); Log.d(
Config.LOGTAG,
ToneManager.class.getName()
+ ": do not reset audio manager because RTC has control");
return; return;
} }
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); final AudioManager audioManager =
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (audioManager == null) { if (audioManager == null) {
return; return;
} }
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager back into normal mode"); Log.d(
Config.LOGTAG,
ToneManager.class.getName() + ": putting AudioManager back into normal mode");
audioManager.setMode(AudioManager.MODE_NORMAL); audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.setSpeakerphoneOn(false); audioManager.setSpeakerphoneOn(false);
} }
public static ToneManager getInstance(final Context context) {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (ToneManager.class) {
if (INSTANCE != null) {
return INSTANCE;
}
INSTANCE = new ToneManager(context);
return INSTANCE;
}
}
private enum ToneState { private enum ToneState {
NULL, RINGING, CONNECTED, BUSY, ENDING_CALL NULL,
RINGING,
CONNECTED,
BUSY,
ENDING_CALL
} }
} }

View file

@ -1,23 +1,18 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.util.Log; import android.util.Log;
import com.google.common.base.CaseFormat; import com.google.common.base.CaseFormat;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import eu.siacs.conversations.Config;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.webrtc.MediaStreamTrack; import org.webrtc.MediaStreamTrack;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
import org.webrtc.RtpSender; import org.webrtc.RtpSender;
import org.webrtc.RtpTransceiver; import org.webrtc.RtpTransceiver;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import eu.siacs.conversations.Config;
class TrackWrapper<T extends MediaStreamTrack> { class TrackWrapper<T extends MediaStreamTrack> {
public final T track; public final T track;
public final RtpSender rtpSender; public final RtpSender rtpSender;
@ -43,7 +38,9 @@ class TrackWrapper<T extends MediaStreamTrack> {
final RtpTransceiver transceiver = final RtpTransceiver transceiver =
peerConnection == null ? null : getTransceiver(peerConnection, trackWrapper); peerConnection == null ? null : getTransceiver(peerConnection, trackWrapper);
if (transceiver == null) { if (transceiver == null) {
Log.w(Config.LOGTAG, "unable to detect transceiver for " + trackWrapper.rtpSender.id()); Log.w(
Config.LOGTAG,
"unable to detect transceiver for " + trackWrapper.getRtpSenderId());
return Optional.of(trackWrapper.track); return Optional.of(trackWrapper.track);
} }
final RtpTransceiver.RtpTransceiverDirection direction = transceiver.getDirection(); final RtpTransceiver.RtpTransceiverDirection direction = transceiver.getDirection();
@ -56,11 +53,22 @@ class TrackWrapper<T extends MediaStreamTrack> {
} }
} }
public String getRtpSenderId() {
try {
return track.id();
} catch (final IllegalStateException e) {
return null;
}
}
public static <T extends MediaStreamTrack> RtpTransceiver getTransceiver( public static <T extends MediaStreamTrack> RtpTransceiver getTransceiver(
@Nonnull final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) { @Nonnull final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) {
final RtpSender rtpSender = trackWrapper.rtpSender; final String rtpSenderId = trackWrapper.getRtpSenderId();
if (rtpSenderId == null) {
return null;
}
for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) { for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) {
if (transceiver.getSender().id().equals(rtpSender.id())) { if (transceiver.getSender().id().equals(rtpSenderId)) {
return transceiver; return transceiver;
} }
} }

View file

@ -2,12 +2,15 @@ package eu.siacs.conversations.xmpp.jingle;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import eu.siacs.conversations.Config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Set;
import javax.annotation.Nullable;
import org.webrtc.Camera2Enumerator; import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerationAndroid; import org.webrtc.CameraEnumerationAndroid;
import org.webrtc.CameraEnumerator; import org.webrtc.CameraEnumerator;
@ -17,14 +20,6 @@ import org.webrtc.PeerConnectionFactory;
import org.webrtc.SurfaceTextureHelper; import org.webrtc.SurfaceTextureHelper;
import org.webrtc.VideoSource; import org.webrtc.VideoSource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Set;
import javax.annotation.Nullable;
import eu.siacs.conversations.Config;
class VideoSourceWrapper { class VideoSourceWrapper {
private static final int CAPTURING_RESOLUTION = 1920; private static final int CAPTURING_RESOLUTION = 1920;

View file

@ -5,7 +5,6 @@ import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
@ -13,7 +12,17 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.services.AppRTCAudioManager;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.webrtc.AudioSource; import org.webrtc.AudioSource;
import org.webrtc.AudioTrack; import org.webrtc.AudioTrack;
import org.webrtc.CandidatePairChangeEvent; import org.webrtc.CandidatePairChangeEvent;
@ -35,21 +44,6 @@ import org.webrtc.VideoTrack;
import org.webrtc.audio.JavaAudioDeviceModule; import org.webrtc.audio.JavaAudioDeviceModule;
import org.webrtc.voiceengine.WebRtcAudioEffects; import org.webrtc.voiceengine.WebRtcAudioEffects;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.XmppConnectionService;
@SuppressWarnings("UnstableApiUsage") @SuppressWarnings("UnstableApiUsage")
public class WebRTCWrapper { public class WebRTCWrapper {
@ -206,7 +200,6 @@ public class WebRTCWrapper {
@Nullable private PeerConnectionFactory peerConnectionFactory = null; @Nullable private PeerConnectionFactory peerConnectionFactory = null;
@Nullable private PeerConnection peerConnection = null; @Nullable private PeerConnection peerConnection = null;
private AppRTCAudioManager appRTCAudioManager = null; private AppRTCAudioManager appRTCAudioManager = null;
private ToneManager toneManager = null;
private Context context = null; private Context context = null;
private EglBase eglBase = null; private EglBase eglBase = null;
private VideoSourceWrapper videoSourceWrapper; private VideoSourceWrapper videoSourceWrapper;
@ -223,8 +216,16 @@ public class WebRTCWrapper {
} }
} }
private static void dispose(final VideoTrack videoTrack) {
try {
videoTrack.dispose();
} catch (final IllegalStateException e) {
Log.e(Config.LOGTAG, "unable to dispose of video track", e);
}
}
public void setup( public void setup(
final XmppConnectionService service, final Context service,
@Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) @Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
throws InitializationException { throws InitializationException {
try { try {
@ -241,11 +242,10 @@ public class WebRTCWrapper {
throw new InitializationException("Unable to create EGL base", e); throw new InitializationException("Unable to create EGL base", e);
} }
this.context = service; this.context = service;
this.toneManager = service.getJingleConnectionManager().toneManager;
mainHandler.post( mainHandler.post(
() -> { () -> {
appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference); appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
toneManager.setAppRtcAudioManagerHasControl(true); ToneManager.getInstance(context).setAppRtcAudioManagerHasControl(true);
appRTCAudioManager.start(audioManagerEvents); appRTCAudioManager.start(audioManagerEvents);
eventCallback.onAudioDeviceChanged( eventCallback.onAudioDeviceChanged(
appRTCAudioManager.getSelectedAudioDevice(), appRTCAudioManager.getSelectedAudioDevice(),
@ -449,15 +449,19 @@ public class WebRTCWrapper {
final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
final AppRTCAudioManager audioManager = this.appRTCAudioManager; final AppRTCAudioManager audioManager = this.appRTCAudioManager;
final EglBase eglBase = this.eglBase; final EglBase eglBase = this.eglBase;
final var localVideoTrack = this.localVideoTrack;
if (peerConnection != null) { if (peerConnection != null) {
this.peerConnection = null; this.peerConnection = null;
dispose(peerConnection); dispose(peerConnection);
} }
if (audioManager != null) { if (audioManager != null) {
toneManager.setAppRtcAudioManagerHasControl(false); ToneManager.getInstance(context).setAppRtcAudioManagerHasControl(false);
mainHandler.post(audioManager::stop); mainHandler.post(audioManager::stop);
} }
if (localVideoTrack != null) {
this.localVideoTrack = null; this.localVideoTrack = null;
dispose(localVideoTrack.track);
}
this.remoteVideoTrack = null; this.remoteVideoTrack = null;
if (videoSourceWrapper != null) { if (videoSourceWrapper != null) {
this.videoSourceWrapper = null; this.videoSourceWrapper = null;
@ -469,8 +473,8 @@ public class WebRTCWrapper {
videoSourceWrapper.dispose(); videoSourceWrapper.dispose();
} }
if (eglBase != null) { if (eglBase != null) {
eglBase.release();
this.eglBase = null; this.eglBase = null;
eglBase.release();
} }
if (peerConnectionFactory != null) { if (peerConnectionFactory != null) {
this.peerConnectionFactory = null; this.peerConnectionFactory = null;

View file

@ -1,20 +1,16 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
import im.conversations.android.xml.Element;
import im.conversations.android.xml.Namespace;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
public class Content extends Element { public class Content extends Element {
public Content(final Creator creator, final Senders senders, final String name) { public Content(final Creator creator, final Senders senders, final String name) {
@ -99,7 +95,6 @@ public class Content extends Element {
} }
} }
public void setTransport(GenericTransportInfo transportInfo) { public void setTransport(GenericTransportInfo transportInfo) {
this.addChild(transportInfo); this.addChild(transportInfo);
} }

View file

@ -1,22 +1,14 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import im.conversations.android.xml.Element;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.xml.Element;
public class FileTransferDescription extends GenericDescription { public class FileTransferDescription extends GenericDescription {
public static List<String> NAMESPACES = Arrays.asList( public static List<String> NAMESPACES =
Version.FT_3.namespace, Arrays.asList(Version.FT_3.namespace, Version.FT_4.namespace, Version.FT_5.namespace);
Version.FT_4.namespace,
Version.FT_5.namespace
);
private FileTransferDescription(String name, String namespace) { private FileTransferDescription(String name, String namespace) {
super(name, namespace); super(name, namespace);
@ -45,27 +37,15 @@ public class FileTransferDescription extends GenericDescription {
} }
} }
public static FileTransferDescription of(DownloadableFile file, Version version, XmppAxolotlMessage axolotlMessage) {
final FileTransferDescription description = new FileTransferDescription("description", version.getNamespace());
final Element fileElement;
if (version == Version.FT_3) {
Element offer = description.addChild("offer");
fileElement = offer.addChild("file");
} else {
fileElement = description.addChild("file");
}
fileElement.addChild("size").setContent(Long.toString(file.getExpectedSize()));
fileElement.addChild("name").setContent(file.getName());
if (axolotlMessage != null) {
fileElement.addChild(axolotlMessage.toElement());
}
return description;
}
public static FileTransferDescription upgrade(final Element element) { public static FileTransferDescription upgrade(final Element element) {
Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); Preconditions.checkArgument(
Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace"); "description".equals(element.getName()),
final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace()); "Name of provided element is not description");
Preconditions.checkArgument(
NAMESPACES.contains(element.getNamespace()),
"Element does not match a file transfer namespace");
final FileTransferDescription description =
new FileTransferDescription("description", element.getNamespace());
description.setAttributes(element.getAttributes()); description.setAttributes(element.getAttributes());
description.setChildren(element.getChildren()); description.setChildren(element.getChildren());
return description; return description;

View file

@ -1,8 +1,7 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import im.conversations.android.xml.Element;
import eu.siacs.conversations.xml.Element;
public class GenericDescription extends Element { public class GenericDescription extends Element {
@ -12,7 +11,8 @@ public class GenericDescription extends Element {
public static GenericDescription upgrade(final Element element) { public static GenericDescription upgrade(final Element element) {
Preconditions.checkArgument("description".equals(element.getName())); Preconditions.checkArgument("description".equals(element.getName()));
final GenericDescription description = new GenericDescription("description", element.getNamespace()); final GenericDescription description =
new GenericDescription("description", element.getNamespace());
description.setAttributes(element.getAttributes()); description.setAttributes(element.getAttributes());
description.setChildren(element.getChildren()); description.setChildren(element.getChildren());
return description; return description;

View file

@ -1,8 +1,7 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import im.conversations.android.xml.Element;
import eu.siacs.conversations.xml.Element;
public class GenericTransportInfo extends Element { public class GenericTransportInfo extends Element {
@ -12,7 +11,8 @@ public class GenericTransportInfo extends Element {
public static GenericTransportInfo upgrade(final Element element) { public static GenericTransportInfo upgrade(final Element element) {
Preconditions.checkArgument("transport".equals(element.getName())); Preconditions.checkArgument("transport".equals(element.getName()));
final GenericTransportInfo transport = new GenericTransportInfo("transport", element.getNamespace()); final GenericTransportInfo transport =
new GenericTransportInfo("transport", element.getNamespace());
transport.setAttributes(element.getAttributes()); transport.setAttributes(element.getAttributes());
transport.setChildren(element.getChildren()); transport.setChildren(element.getChildren());
return transport; return transport;

View file

@ -2,13 +2,11 @@ package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import im.conversations.android.xml.Element;
import im.conversations.android.xml.Namespace;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
public class Group extends Element { public class Group extends Element {
private Group() { private Group() {

View file

@ -1,9 +1,8 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import im.conversations.android.xml.Element;
import eu.siacs.conversations.xml.Element; import im.conversations.android.xml.Namespace;
import eu.siacs.conversations.xml.Namespace;
public class IbbTransportInfo extends GenericTransportInfo { public class IbbTransportInfo extends GenericTransportInfo {
@ -36,9 +35,13 @@ public class IbbTransportInfo extends GenericTransportInfo {
} }
public static IbbTransportInfo upgrade(final Element element) { public static IbbTransportInfo upgrade(final Element element) {
Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); Preconditions.checkArgument(
Preconditions.checkArgument(Namespace.JINGLE_TRANSPORTS_IBB.equals(element.getNamespace()), "Element does not match ibb transport namespace"); "transport".equals(element.getName()), "Name of provided element is not transport");
final IbbTransportInfo transportInfo = new IbbTransportInfo("transport", Namespace.JINGLE_TRANSPORTS_IBB); Preconditions.checkArgument(
Namespace.JINGLE_TRANSPORTS_IBB.equals(element.getNamespace()),
"Element does not match ibb transport namespace");
final IbbTransportInfo transportInfo =
new IbbTransportInfo("transport", Namespace.JINGLE_TRANSPORTS_IBB);
transportInfo.setAttributes(element.getAttributes()); transportInfo.setAttributes(element.getAttributes());
transportInfo.setChildren(element.getChildren()); transportInfo.setChildren(element.getChildren());
return transportInfo; return transportInfo;

View file

@ -1,7 +1,6 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
@ -11,7 +10,9 @@ import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
import im.conversations.android.xml.Element;
import im.conversations.android.xml.Namespace;
import java.util.HashMap; import java.util.HashMap;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -20,10 +21,6 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
public class IceUdpTransportInfo extends GenericTransportInfo { public class IceUdpTransportInfo extends GenericTransportInfo {
public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo(); public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo();
@ -63,7 +60,10 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
} }
public static IceUdpTransportInfo of( public static IceUdpTransportInfo of(
final Credentials credentials, final Setup setup, final String hash, final String fingerprint) { final Credentials credentials,
final Setup setup,
final String hash,
final String fingerprint) {
final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo(); final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint)); iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint));
iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag); iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag);

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