Compare commits
190 commits
Author | SHA1 | Date | |
---|---|---|---|
69866e591c | |||
506e4e1d0c | |||
c858b5346f | |||
e6bf595388 | |||
9127d68197 | |||
340bf45095 | |||
acfcde8416 | |||
4f654044b4 | |||
1b3c7b6a42 | |||
a4fe60dece | |||
03cf48f4c1 | |||
4d5445d123 | |||
4bfcf209d7 | |||
5b777ef657 | |||
d52cbb8e8c | |||
cc07f86bf4 | |||
f13f15cc91 | |||
405eeadd95 | |||
75a4008aee | |||
4fae8d4e11 | |||
805d0db486 | |||
779e6fa61e | |||
da528776db | |||
4fd96e740f | |||
4139c11771 | |||
1e884ec435 | |||
86d9264ee5 | |||
0f6f9b0001 | |||
e22fcab844 | |||
e3f5f6404b | |||
7c820f7b32 | |||
ee1c938f2a | |||
d9e8918727 | |||
97f54b6673 | |||
58c5bd0f1b | |||
bb2d077b7c | |||
b2c348a1df | |||
9a0c2226c1 | |||
e971b77539 | |||
c1ef2ac628 | |||
eb15dc1260 | |||
26d856e91f | |||
9819ef7d05 | |||
417e801811 | |||
0d134a919e | |||
260654f171 | |||
cfaf6162e6 | |||
e4fb793769 | |||
f1fbf15fea | |||
f9b3d42a8a | |||
a67979adf8 | |||
8be8d7df8f | |||
2e5e2ff6fe | |||
807078b24f | |||
4addeaa356 | |||
100c735636 | |||
b2414434dc | |||
0c4771e2a8 | |||
177320d8fe | |||
9c64f9c24c | |||
786a6c4c2a | |||
be6f4300da | |||
c2bf9d0413 | |||
303f14200f | |||
1a924d3efd | |||
86ef179c42 | |||
5e79dd8b68 | |||
3c207c28b4 | |||
9c95554782 | |||
ac2866a682 | |||
cf17a2ac6d | |||
c3f5273813 | |||
6ef2997b5e | |||
b8f3472af0 | |||
d54978f593 | |||
99c11fba17 | |||
cf5910e96e | |||
677cfcd34c | |||
2abcb1b4e4 | |||
49b4f54285 | |||
1be1334794 | |||
63df518c19 | |||
63bfbfb40a | |||
44ac7190a9 | |||
bfafad6c65 | |||
f5203b082b | |||
eafa93d132 | |||
d7ab5e1a4b | |||
d136928322 | |||
0727b0aba6 | |||
1f22c5f534 | |||
7d42da8c34 | |||
09b28358ab | |||
7567dcff5e | |||
b80fe9802a | |||
fe9b3b8ed9 | |||
cdcd323c36 | |||
867db9d54c | |||
87e33a779f | |||
c105c3420e | |||
2212c63810 | |||
d6edea8ddf | |||
bca253faa4 | |||
68e9f25da2 | |||
a1e97461f9 | |||
bf9b0b18f9 | |||
a09cc126ea | |||
b0010307c0 | |||
b5a47000c9 | |||
7d34c894d0 | |||
5866974eff | |||
3c42066a7c | |||
6845380be5 | |||
eeac779e25 | |||
35360fde91 | |||
a204bf9ec1 | |||
79eebe68e2 | |||
268bef4433 | |||
69d212141b | |||
94c8b9ed04 | |||
2d10a561e4 | |||
acb297ac96 | |||
405445afbe | |||
56a462833e | |||
2728a96ab9 | |||
7e2bff9d03 | |||
4c09b20aa4 | |||
fbb900d4ad | |||
6c24cb12dd | |||
a69b4b14a5 | |||
be3a8dc5e1 | |||
9b62861a64 | |||
dc371d7017 | |||
a43160b13d | |||
458f0ef280 | |||
3f59dd2688 | |||
ca0a0c07fc | |||
bed6b07bdd | |||
870393df8e | |||
e2ea1f9437 | |||
3be56b6775 | |||
58b1e26367 | |||
c077e4e8da | |||
f1e1cf9653 | |||
e073f22ec0 | |||
57d264d72e | |||
9a855a57ac | |||
ddcab5fb58 | |||
fe32526de8 | |||
164ac450d4 | |||
d2794ccf32 | |||
f16603742f | |||
f982885d2e | |||
8df97067bb | |||
bd343eafa0 | |||
c31fa7ed2b | |||
d25cc059c5 | |||
359ef330df | |||
de06bfb8f0 | |||
1e6aed759b | |||
1a09b3ed05 | |||
90e613f94e | |||
09db9e574b | |||
f5faa8fc4d | |||
bfa61d56af | |||
da65960fd1 | |||
6983aedddc | |||
27952c00ed | |||
944c48e00b | |||
26bff8028a | |||
873644f528 | |||
199a1cdc64 | |||
43a82e504b | |||
a2b21d97eb | |||
6458c6e9f9 | |||
1b438117a3 | |||
78af8cbd87 | |||
482dc8cfe9 | |||
3e9029dc8f | |||
38c612d35d | |||
07c1669813 | |||
20962554a4 | |||
6b232f7a5a | |||
9e7bbcc272 | |||
49bf92f7ca | |||
2c32f9738c | |||
7ee3e07946 | |||
94dde9f433 | |||
5d79cfbf0d | |||
80d97c3fcc |
15
annotation-processor/build.gradle
Normal 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'
|
||||
|
||||
}
|
|
@ -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
|
@ -0,0 +1,6 @@
|
|||
apply plugin: "java-library"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
|
@ -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 "";
|
||||
}
|
|
@ -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
|
@ -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
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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>
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
120
app/src/main/AndroidManifest.xml
Normal 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>
|
16
app/src/main/assets/logback.xml
Normal 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>
|
10
app/src/main/java/eu/siacs/conversations/Config.java
Normal 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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -21,37 +21,28 @@ import android.media.AudioRecord;
|
|||
import android.media.MediaRecorder;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.webrtc.ThreadUtils;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.utils.AppRTCUtils;
|
||||
import eu.siacs.conversations.xmpp.jingle.Media;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import org.webrtc.ThreadUtils;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.utils.AppRTCUtils;
|
||||
import eu.siacs.conversations.xmpp.jingle.Media;
|
||||
|
||||
/**
|
||||
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
|
||||
*/
|
||||
/** AppRTCAudioManager manages all audio related parts of the AppRTC demo. */
|
||||
public class AppRTCAudioManager {
|
||||
|
||||
private static CountDownLatch microphoneLatch;
|
||||
|
||||
private final Context apprtcContext;
|
||||
// Contains speakerphone setting: auto, true or false
|
||||
@Nullable
|
||||
private SpeakerPhonePreference speakerPhonePreference;
|
||||
@Nullable private SpeakerPhonePreference speakerPhonePreference;
|
||||
// Handles all tasks related to Bluetooth headset devices.
|
||||
private final AppRTCBluetoothManager bluetoothManager;
|
||||
@Nullable
|
||||
private final AudioManager audioManager;
|
||||
@Nullable
|
||||
private AudioManagerEvents audioManagerEvents;
|
||||
@Nullable private final AudioManager audioManager;
|
||||
@Nullable private AudioManagerEvents audioManagerEvents;
|
||||
private AudioManagerState amState;
|
||||
private boolean savedIsSpeakerPhoneOn;
|
||||
private boolean savedIsMicrophoneMute;
|
||||
|
@ -74,18 +65,17 @@ public class AppRTCAudioManager {
|
|||
// relative to the view screen of a device and can therefore be used to
|
||||
// assist device switching (close to ear <=> use headset earpiece if
|
||||
// available, far from ear <=> use speaker phone).
|
||||
@Nullable
|
||||
private AppRTCProximitySensor proximitySensor;
|
||||
@Nullable private AppRTCProximitySensor proximitySensor;
|
||||
// Contains a list of available audio devices. A Set collection is used to
|
||||
// avoid duplicate elements.
|
||||
private Set<AudioDevice> audioDevices = new HashSet<>();
|
||||
// Broadcast receiver for wired headset intent broadcasts.
|
||||
private final BroadcastReceiver wiredHeadsetReceiver;
|
||||
// Callback method for changes in audio focus.
|
||||
@Nullable
|
||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
||||
@Nullable private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
||||
|
||||
private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) {
|
||||
private AppRTCAudioManager(
|
||||
Context context, final SpeakerPhonePreference speakerPhonePreference) {
|
||||
Log.d(Config.LOGTAG, "ctor");
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
apprtcContext = context;
|
||||
|
@ -102,7 +92,9 @@ public class AppRTCAudioManager {
|
|||
// Create and initialize the proximity sensor.
|
||||
// Tablet devices (e.g. Nexus 7) does not support proximity sensors.
|
||||
// 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.
|
||||
// Example: user holds his hand over the device (closer than ~5 cm),
|
||||
// or removes his hand from the device.
|
||||
|
@ -121,10 +113,9 @@ public class AppRTCAudioManager {
|
|||
updateAudioDeviceState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construction.
|
||||
*/
|
||||
public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) {
|
||||
/** Construction. */
|
||||
public static AppRTCAudioManager create(
|
||||
Context context, SpeakerPhonePreference speakerPhonePreference) {
|
||||
return new AppRTCAudioManager(context, speakerPhonePreference);
|
||||
}
|
||||
|
||||
|
@ -137,17 +128,18 @@ public class AppRTCAudioManager {
|
|||
final int channel = AudioFormat.CHANNEL_IN_MONO;
|
||||
final int format = AudioFormat.ENCODING_PCM_16BIT;
|
||||
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();
|
||||
final short[] buffer = new short[bufferSize];
|
||||
final int audioStatus = audioRecord.read(buffer, 0, bufferSize);
|
||||
if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION || audioStatus == AudioRecord.STATE_UNINITIALIZED)
|
||||
available = false;
|
||||
if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION
|
||||
|| audioStatus == AudioRecord.STATE_UNINITIALIZED) available = false;
|
||||
} catch (Exception e) {
|
||||
available = false;
|
||||
} finally {
|
||||
release(audioRecord);
|
||||
|
||||
}
|
||||
microphoneLatch.countDown();
|
||||
return available;
|
||||
|
@ -160,13 +152,13 @@ public class AppRTCAudioManager {
|
|||
try {
|
||||
audioRecord.release();
|
||||
} catch (Exception e) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when the proximity sensor reports a state change,
|
||||
* e.g. from "NEAR to FAR" or from "FAR to NEAR".
|
||||
* This method is called when the proximity sensor reports a state change, e.g. from "NEAR to
|
||||
* FAR" or from "FAR to NEAR".
|
||||
*/
|
||||
private void onProximitySensorChangedState() {
|
||||
if (speakerPhonePreference != SpeakerPhonePreference.AUTO) {
|
||||
|
@ -174,7 +166,8 @@ public class AppRTCAudioManager {
|
|||
}
|
||||
// The proximity sensor should only be activated when there are exactly two
|
||||
// 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)) {
|
||||
if (proximitySensor.sensorReportsNearState()) {
|
||||
// Sensor reports that a "handset is being held up to a person's ear",
|
||||
|
@ -204,12 +197,17 @@ public class AppRTCAudioManager {
|
|||
savedIsMicrophoneMute = audioManager.isMicrophoneMute();
|
||||
hasWiredHeadset = hasWiredHeadset();
|
||||
// Create an AudioManager.OnAudioFocusChangeListener instance.
|
||||
audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
|
||||
// Called on the listener to notify if the audio focus for this listener has 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
|
||||
audioFocusChangeListener =
|
||||
new AudioManager.OnAudioFocusChangeListener() {
|
||||
// Called on the listener to notify if the audio focus for this listener has
|
||||
// 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.
|
||||
// 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.
|
||||
@Override
|
||||
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.
|
||||
int result = audioManager.requestAudioFocus(audioFocusChangeListener,
|
||||
AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
|
||||
int result =
|
||||
audioManager.requestAudioFocus(
|
||||
audioFocusChangeListener,
|
||||
AudioManager.STREAM_VOICE_CALL,
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams");
|
||||
} else {
|
||||
|
@ -282,7 +283,7 @@ public class AppRTCAudioManager {
|
|||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -312,9 +313,7 @@ public class AppRTCAudioManager {
|
|||
audioManagerEvents = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes selection of the currently active audio device.
|
||||
*/
|
||||
/** Changes selection of the currently active audio device. */
|
||||
private void setAudioDeviceInternal(AudioDevice device) {
|
||||
Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
|
||||
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
|
||||
|
@ -335,8 +334,8 @@ public class AppRTCAudioManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Changes default audio device.
|
||||
* TODO(henrika): add usage of this method in the AppRTCMobile client.
|
||||
* Changes default audio device. TODO(henrika): add usage of this method in the AppRTCMobile
|
||||
* client.
|
||||
*/
|
||||
public void setDefaultAudioDevice(AudioDevice defaultDevice) {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
|
@ -359,9 +358,7 @@ public class AppRTCAudioManager {
|
|||
updateAudioDeviceState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes selection of the currently active audio device.
|
||||
*/
|
||||
/** Changes selection of the currently active audio device. */
|
||||
public void selectAudioDevice(AudioDevice device) {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
if (!audioDevices.contains(device)) {
|
||||
|
@ -371,39 +368,29 @@ public class AppRTCAudioManager {
|
|||
updateAudioDeviceState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current set of available/selectable audio devices.
|
||||
*/
|
||||
/** Returns current set of available/selectable audio devices. */
|
||||
public Set<AudioDevice> getAudioDevices() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
return Collections.unmodifiableSet(new HashSet<>(audioDevices));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently selected audio device.
|
||||
*/
|
||||
/** Returns the currently selected audio device. */
|
||||
public AudioDevice getSelectedAudioDevice() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
return selectedAudioDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for receiver registration.
|
||||
*/
|
||||
/** Helper method for receiver registration. */
|
||||
private void registerReceiver(BroadcastReceiver receiver, IntentFilter 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) {
|
||||
apprtcContext.unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the speaker phone mode.
|
||||
*/
|
||||
/** Sets the speaker phone mode. */
|
||||
private void setSpeakerphoneOn(boolean on) {
|
||||
boolean wasOn = audioManager.isSpeakerphoneOn();
|
||||
if (wasOn == on) {
|
||||
|
@ -412,9 +399,7 @@ public class AppRTCAudioManager {
|
|||
audioManager.setSpeakerphoneOn(on);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the microphone mute state.
|
||||
*/
|
||||
/** Sets the microphone mute state. */
|
||||
private void setMicrophoneMute(boolean on) {
|
||||
boolean wasMuted = audioManager.isMicrophoneMute();
|
||||
if (wasMuted == on) {
|
||||
|
@ -423,19 +408,15 @@ public class AppRTCAudioManager {
|
|||
audioManager.setMicrophoneMute(on);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current earpiece state.
|
||||
*/
|
||||
/** Gets the current earpiece state. */
|
||||
private boolean hasEarpiece() {
|
||||
return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a wired headset is connected or not.
|
||||
* This is not a valid indication that audio playback is actually over
|
||||
* 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.
|
||||
* Checks whether a wired headset is connected or not. This is not a valid indication that audio
|
||||
* playback is actually over 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.
|
||||
*/
|
||||
@Deprecated
|
||||
private boolean hasWiredHeadset() {
|
||||
|
@ -458,18 +439,30 @@ public class AppRTCAudioManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Updates list of possible audio devices and make new device selection.
|
||||
* TODO(henrika): add unit test to verify all state transitions.
|
||||
* Updates list of possible audio devices and make new device selection. TODO(henrika): add unit
|
||||
* test to verify all state transitions.
|
||||
*/
|
||||
public void updateAudioDeviceState() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
Log.d(Config.LOGTAG, "--- updateAudioDeviceState: "
|
||||
+ "wired headset=" + hasWiredHeadset + ", "
|
||||
+ "BT state=" + bluetoothManager.getState());
|
||||
Log.d(Config.LOGTAG, "Device status: "
|
||||
+ "available=" + audioDevices + ", "
|
||||
+ "selected=" + selectedAudioDevice + ", "
|
||||
+ "user selected=" + userSelectedAudioDevice);
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"--- updateAudioDeviceState: "
|
||||
+ "wired headset="
|
||||
+ hasWiredHeadset
|
||||
+ ", "
|
||||
+ "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
|
||||
// change accordingly.
|
||||
// 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.
|
||||
boolean needBluetoothAudioStop =
|
||||
(bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
|
||||
|| bluetoothManager.getState()
|
||||
== AppRTCBluetoothManager.State.SCO_CONNECTING)
|
||||
&& (userSelectedAudioDevice != AudioDevice.NONE
|
||||
&& userSelectedAudioDevice != AudioDevice.BLUETOOTH);
|
||||
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
||||
Log.d(Config.LOGTAG, "Need BT audio: start=" + needBluetoothAudioStart + ", "
|
||||
+ "stop=" + needBluetoothAudioStop + ", "
|
||||
+ "BT state=" + bluetoothManager.getState());
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"Need BT audio: start="
|
||||
+ needBluetoothAudioStart
|
||||
+ ", "
|
||||
+ "stop="
|
||||
+ needBluetoothAudioStop
|
||||
+ ", "
|
||||
+ "BT state="
|
||||
+ bluetoothManager.getState());
|
||||
}
|
||||
// Start or stop Bluetooth SCO connection given states set earlier.
|
||||
if (needBluetoothAudioStop) {
|
||||
|
@ -563,7 +564,8 @@ public class AppRTCAudioManager {
|
|||
} else {
|
||||
// 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).
|
||||
// |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE
|
||||
// |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or
|
||||
// AudioDevice.EARPIECE
|
||||
// depending on the user's selection.
|
||||
newAudioDevice = defaultAudioDevice;
|
||||
}
|
||||
|
@ -571,9 +573,14 @@ public class AppRTCAudioManager {
|
|||
if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
|
||||
// Do the required device switch.
|
||||
setAudioDeviceInternal(newAudioDevice);
|
||||
Log.d(Config.LOGTAG, "New device status: "
|
||||
+ "available=" + audioDevices + ", "
|
||||
+ "selected=" + newAudioDevice);
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"New device status: "
|
||||
+ "available="
|
||||
+ audioDevices
|
||||
+ ", "
|
||||
+ "selected="
|
||||
+ newAudioDevice);
|
||||
if (audioManagerEvents != null) {
|
||||
// Notify a listening client that audio device has been changed.
|
||||
audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
|
||||
|
@ -582,15 +589,16 @@ public class AppRTCAudioManager {
|
|||
Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
|
||||
}
|
||||
|
||||
/**
|
||||
* AudioDevice is the names of possible audio devices that we currently
|
||||
* support.
|
||||
*/
|
||||
public enum AudioDevice {SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE}
|
||||
/** AudioDevice is the names of possible audio devices that we currently support. */
|
||||
public enum AudioDevice {
|
||||
SPEAKER_PHONE,
|
||||
WIRED_HEADSET,
|
||||
EARPIECE,
|
||||
BLUETOOTH,
|
||||
NONE
|
||||
}
|
||||
|
||||
/**
|
||||
* AudioManager state.
|
||||
*/
|
||||
/** AudioManager state. */
|
||||
public enum AudioManagerState {
|
||||
UNINITIALIZED,
|
||||
PREINITIALIZED,
|
||||
|
@ -598,7 +606,9 @@ public class AppRTCAudioManager {
|
|||
}
|
||||
|
||||
public enum SpeakerPhonePreference {
|
||||
AUTO, EARPIECE, SPEAKER;
|
||||
AUTO,
|
||||
EARPIECE,
|
||||
SPEAKER;
|
||||
|
||||
public static SpeakerPhonePreference of(final Set<Media> media) {
|
||||
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 {
|
||||
// Callback fired once audio device is changed or list of available audio devices changed.
|
||||
void onAudioDeviceChanged(
|
||||
|
@ -630,10 +638,20 @@ public class AppRTCAudioManager {
|
|||
int state = intent.getIntExtra("state", STATE_UNPLUGGED);
|
||||
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
|
||||
String name = intent.getStringExtra("name");
|
||||
Log.d(Config.LOGTAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": "
|
||||
+ "a=" + intent.getAction() + ", s="
|
||||
+ (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m="
|
||||
+ (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb="
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"WiredHeadsetReceiver.onReceive"
|
||||
+ AppRTCUtils.getThreadInfo()
|
||||
+ ": "
|
||||
+ "a="
|
||||
+ intent.getAction()
|
||||
+ ", s="
|
||||
+ (state == STATE_UNPLUGGED ? "unplugged" : "plugged")
|
||||
+ ", m="
|
||||
+ (microphone == HAS_MIC ? "mic" : "no mic")
|
||||
+ ", n="
|
||||
+ name
|
||||
+ ", sb="
|
||||
+ isInitialStickyBroadcast());
|
||||
hasWiredHeadset = (state == STATE_PLUGGED);
|
||||
updateAudioDeviceState();
|
|
@ -25,19 +25,14 @@ import android.os.Build;
|
|||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
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.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. */
|
||||
public class AppRTCBluetoothManager {
|
|
@ -16,22 +16,17 @@ import android.hardware.SensorEventListener;
|
|||
import android.hardware.SensorManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.webrtc.ThreadUtils;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.utils.AppRTCUtils;
|
||||
import org.webrtc.ThreadUtils;
|
||||
|
||||
/**
|
||||
* AppRTCProximitySensor manages functions related to the proximity sensor in
|
||||
* the AppRTC demo.
|
||||
* On most device, the proximity sensor is implemented as a boolean-sensor.
|
||||
* It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX
|
||||
* value i.e. the LUX value of the light sensor is compared with a threshold.
|
||||
* A LUX-value more than the threshold means the proximity sensor returns "FAR".
|
||||
* Anything less than the threshold value and the sensor returns "NEAR".
|
||||
* AppRTCProximitySensor manages functions related to the proximity sensor in the AppRTC demo. On
|
||||
* most device, the proximity sensor is implemented as a boolean-sensor. It returns just two values
|
||||
* "NEAR" or "FAR". Thresholding is done on the LUX value i.e. the LUX value of the light sensor is
|
||||
* compared with a threshold. 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 {
|
||||
// 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 Runnable onSensorStateListener;
|
||||
private final SensorManager sensorManager;
|
||||
@Nullable
|
||||
private Sensor proximitySensor;
|
||||
@Nullable private Sensor proximitySensor;
|
||||
private boolean lastStateReportIsNear;
|
||||
|
||||
private AppRTCProximitySensor(Context context, Runnable sensorStateListener) {
|
||||
|
@ -50,17 +44,12 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
|||
sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construction
|
||||
*/
|
||||
/** Construction */
|
||||
static AppRTCProximitySensor create(Context context, Runnable 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() {
|
||||
threadChecker.checkIsOnValidThread();
|
||||
Log.d(Config.LOGTAG, "start" + AppRTCUtils.getThreadInfo());
|
||||
|
@ -72,9 +61,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the proximity sensor.
|
||||
*/
|
||||
/** Deactivate the proximity sensor. */
|
||||
public void stop() {
|
||||
threadChecker.checkIsOnValidThread();
|
||||
Log.d(Config.LOGTAG, "stop" + AppRTCUtils.getThreadInfo());
|
||||
|
@ -84,9 +71,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
|||
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() {
|
||||
threadChecker.checkIsOnValidThread();
|
||||
return lastStateReportIsNear;
|
||||
|
@ -120,15 +105,22 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
|||
if (onSensorStateListener != null) {
|
||||
onSensorStateListener.run();
|
||||
}
|
||||
Log.d(Config.LOGTAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": "
|
||||
+ "accuracy=" + event.accuracy + ", timestamp=" + event.timestamp + ", distance="
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"onSensorChanged"
|
||||
+ AppRTCUtils.getThreadInfo()
|
||||
+ ": "
|
||||
+ "accuracy="
|
||||
+ event.accuracy
|
||||
+ ", timestamp="
|
||||
+ event.timestamp
|
||||
+ ", distance="
|
||||
+ event.values[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7)
|
||||
* does not support this type of sensor and false will be returned in such
|
||||
* cases.
|
||||
* Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) does not support
|
||||
* this type of sensor and false will be returned in such cases.
|
||||
*/
|
||||
private boolean initDefaultSensor() {
|
||||
if (proximitySensor != null) {
|
||||
|
@ -142,9 +134,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for logging information about the proximity sensor.
|
||||
*/
|
||||
/** Helper method for logging information about the proximity sensor. */
|
||||
private void logProximitySensorInfo() {
|
||||
if (proximitySensor == null) {
|
||||
return;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 haven’t 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
|
||||
}
|
||||
}
|
|
@ -4,10 +4,8 @@ import com.google.common.base.MoreObjects;
|
|||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ContentAddition {
|
||||
|
|
@ -10,8 +10,7 @@ import java.util.ArrayList;
|
|||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import org.jxmpp.jid.Jid;
|
||||
|
||||
public class DirectConnectionUtils {
|
||||
|
||||
|
@ -25,18 +24,19 @@ public class DirectConnectionUtils {
|
|||
}
|
||||
while (interfaces.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = interfaces.nextElement();
|
||||
final Enumeration<InetAddress> inetAddressEnumeration = networkInterface.getInetAddresses();
|
||||
final Enumeration<InetAddress> inetAddressEnumeration =
|
||||
networkInterface.getInetAddresses();
|
||||
while (inetAddressEnumeration.hasMoreElements()) {
|
||||
final InetAddress inetAddress = inetAddressEnumeration.nextElement();
|
||||
if (inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) {
|
||||
continue;
|
||||
}
|
||||
if (inetAddress instanceof Inet6Address) {
|
||||
//let's get rid of scope
|
||||
// let's get rid of scope
|
||||
try {
|
||||
addresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
|
||||
} catch (UnknownHostException e) {
|
||||
//ignored
|
||||
// ignored
|
||||
}
|
||||
} else {
|
||||
addresses.add(inetAddress);
|
||||
|
@ -50,7 +50,8 @@ public class DirectConnectionUtils {
|
|||
SecureRandom random = new SecureRandom();
|
||||
ArrayList<JingleCandidate> candidates = new ArrayList<>();
|
||||
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.setPort(random.nextInt(60000) + 1024);
|
||||
candidate.setType(JingleCandidate.TYPE_DIRECT);
|
||||
|
@ -60,5 +61,4 @@ public class DirectConnectionUtils {
|
|||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -1,15 +1,14 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public enum Media {
|
||||
|
||||
VIDEO, AUDIO, UNKNOWN;
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
UNKNOWN;
|
||||
|
||||
@Override
|
||||
@Nonnull
|
|
@ -1,7 +1,6 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MediaBuilder {
|
||||
|
@ -10,7 +9,7 @@ public class MediaBuilder {
|
|||
private String protocol;
|
||||
private List<Integer> formats;
|
||||
private String connectionData;
|
||||
private ArrayListMultimap<String,String> attributes;
|
||||
private ArrayListMultimap<String, String> attributes;
|
||||
|
||||
public MediaBuilder setMedia(String media) {
|
||||
this.media = media;
|
||||
|
@ -37,12 +36,13 @@ public class MediaBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public MediaBuilder setAttributes(ArrayListMultimap<String,String> attributes) {
|
||||
public MediaBuilder setAttributes(ArrayListMultimap<String, String> attributes) {
|
||||
this.attributes = attributes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionDescription.Media createMedia() {
|
||||
return new SessionDescription.Media(media, port, protocol, formats, connectionData, attributes);
|
||||
return new SessionDescription.Media(
|
||||
media, port, protocol, formats, connectionData, attributes);
|
||||
}
|
||||
}
|
|
@ -2,17 +2,16 @@ package eu.siacs.conversations.xmpp.jingle;
|
|||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import im.conversations.android.axolotl.AxolotlService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
public class OmemoVerification {
|
||||
|
||||
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 String sessionFingerprint;
|
||||
private IdentityKey identityKey;
|
||||
|
||||
public void setDeviceId(final Integer id) {
|
||||
if (deviceIdWritten.compareAndSet(false, true)) {
|
||||
|
@ -31,31 +30,32 @@ public class OmemoVerification {
|
|||
return this.deviceId != null;
|
||||
}
|
||||
|
||||
public void setSessionFingerprint(final String fingerprint) {
|
||||
Preconditions.checkNotNull(fingerprint, "Session fingerprint must not be null");
|
||||
if (sessionFingerprintWritten.compareAndSet(false, true)) {
|
||||
this.sessionFingerprint = fingerprint;
|
||||
public void setSessionFingerprint(final IdentityKey identityKey) {
|
||||
Preconditions.checkNotNull(identityKey, "IdentityKey must not be null");
|
||||
if (identityKeyWritten.compareAndSet(false, true)) {
|
||||
this.identityKey = identityKey;
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Session fingerprint has already been set");
|
||||
throw new IllegalStateException("Identity Key has already been set");
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
return this.sessionFingerprint;
|
||||
public IdentityKey getFingerprint() {
|
||||
return this.identityKey;
|
||||
}
|
||||
|
||||
public void setOrEnsureEqual(AxolotlService.OmemoVerifiedPayload<?> omemoVerifiedPayload) {
|
||||
setOrEnsureEqual(omemoVerifiedPayload.getDeviceId(), omemoVerifiedPayload.getFingerprint());
|
||||
}
|
||||
|
||||
public void setOrEnsureEqual(final int deviceId, final String sessionFingerprint) {
|
||||
Preconditions.checkNotNull(sessionFingerprint, "Session fingerprint must not be null");
|
||||
if (this.deviceIdWritten.get() || this.sessionFingerprintWritten.get()) {
|
||||
if (this.sessionFingerprint == null) {
|
||||
throw new IllegalStateException("No session fingerprint has been previously provided");
|
||||
public void setOrEnsureEqual(final int deviceId, final IdentityKey identityKey) {
|
||||
Preconditions.checkNotNull(identityKey, "IdentityKey must not be null");
|
||||
if (this.deviceIdWritten.get() || this.identityKeyWritten.get()) {
|
||||
if (this.identityKey == null) {
|
||||
throw new IllegalStateException(
|
||||
"No session fingerprint has been previously provided");
|
||||
}
|
||||
if (!sessionFingerprint.equals(this.sessionFingerprint)) {
|
||||
throw new SecurityException("Session Fingerprints did not match");
|
||||
if (!identityKey.equals(this.identityKey)) {
|
||||
throw new SecurityException("IdentityKeys did not match");
|
||||
}
|
||||
if (this.deviceId == null) {
|
||||
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");
|
||||
}
|
||||
} else {
|
||||
this.setSessionFingerprint(sessionFingerprint);
|
||||
this.setSessionFingerprint(identityKey);
|
||||
this.setDeviceId(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasFingerprint() {
|
||||
return this.sessionFingerprint != null;
|
||||
return this.identityKey != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("deviceId", deviceId)
|
||||
.add("fingerprint", sessionFingerprint)
|
||||
.add("fingerprint", identityKey)
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
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.OmemoVerifiedIceUdpTransportInfo;
|
||||
import java.util.Map;
|
||||
|
||||
public class OmemoVerifiedRtpContentMap extends RtpContentMap {
|
||||
public OmemoVerifiedRtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
|
||||
super(group, contents);
|
||||
for(final DescriptionTransport descriptionTransport : contents.values()) {
|
||||
for (final DescriptionTransport descriptionTransport : contents.values()) {
|
||||
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
|
||||
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint();
|
||||
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport)
|
||||
.ensureNoPlaintextFingerprint();
|
||||
continue;
|
||||
}
|
||||
throw new IllegalStateException("OmemoVerifiedRtpContentMap contains non-verified transport info");
|
||||
throw new IllegalStateException(
|
||||
"OmemoVerifiedRtpContentMap contains non-verified transport info");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import org.jxmpp.jid.Jid;
|
||||
|
||||
public interface OngoingRtpSession {
|
||||
Account getAccount();
|
||||
Jid getWith();
|
||||
|
||||
String getSessionId();
|
||||
}
|
|
@ -12,15 +12,6 @@ import com.google.common.collect.ImmutableSet;
|
|||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Maps;
|
||||
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.GenericDescription;
|
||||
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.OmemoVerifiedIceUdpTransportInfo;
|
||||
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 {
|
||||
|
||||
|
@ -94,7 +91,7 @@ public class RtpContentMap {
|
|||
}
|
||||
|
||||
public Set<Content.Senders> getSenders() {
|
||||
return ImmutableSet.copyOf(Collections2.transform(contents.values(),dt -> dt.senders));
|
||||
return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders));
|
||||
}
|
||||
|
||||
public List<String> getNames() {
|
||||
|
@ -136,7 +133,8 @@ public class RtpContentMap {
|
|||
if (setup == null) {
|
||||
throw new SecurityException(
|
||||
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()));
|
||||
}
|
||||
if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
|
||||
|
@ -317,7 +315,8 @@ public class RtpContentMap {
|
|||
}
|
||||
|
||||
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) {
|
|
@ -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
|
||||
}
|
|
@ -2,24 +2,20 @@ package eu.siacs.conversations.xmpp.jingle;
|
|||
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
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.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||
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 {
|
||||
|
||||
|
@ -128,7 +124,8 @@ public class SessionDescription {
|
|||
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 ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
|
||||
final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>();
|
||||
|
@ -297,7 +294,8 @@ public class SessionDescription {
|
|||
|
||||
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) {
|
||||
mediaAttributes.put("rtcp-mux", "");
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SessionDescriptionBuilder {
|
||||
private int version;
|
||||
private String name;
|
||||
private String connectionData;
|
||||
private ArrayListMultimap<String,String> attributes;
|
||||
private ArrayListMultimap<String, String> attributes;
|
||||
private List<SessionDescription.Media> media;
|
||||
|
||||
public SessionDescriptionBuilder setVersion(int version) {
|
||||
|
@ -26,7 +25,7 @@ public class SessionDescriptionBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public SessionDescriptionBuilder setAttributes(ArrayListMultimap<String,String> attributes) {
|
||||
public SessionDescriptionBuilder setAttributes(ArrayListMultimap<String, String> attributes) {
|
||||
this.attributes = attributes;
|
||||
return this;
|
||||
}
|
|
@ -1,20 +1,19 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.ToneGenerator;
|
||||
import android.util.Log;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import im.conversations.android.xmpp.manager.JingleConnectionManager;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
class ToneManager {
|
||||
public class ToneManager {
|
||||
|
||||
private final ToneGenerator toneGenerator;
|
||||
private final Context context;
|
||||
|
@ -25,7 +24,9 @@ class ToneManager {
|
|||
private ScheduledFuture<?> currentResetFuture;
|
||||
private boolean appRtcAudioManagerHasControl = false;
|
||||
|
||||
ToneManager(final Context context) {
|
||||
private static volatile ToneManager INSTANCE;
|
||||
|
||||
private ToneManager(final Context context) {
|
||||
ToneGenerator toneGenerator;
|
||||
try {
|
||||
toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 60);
|
||||
|
@ -34,12 +35,17 @@ class ToneManager {
|
|||
toneGenerator = null;
|
||||
}
|
||||
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 (asList(RtpEndUserState.FINDING_DEVICE, RtpEndUserState.RINGING, RtpEndUserState.CONNECTING).contains(state)) {
|
||||
if (asList(
|
||||
RtpEndUserState.FINDING_DEVICE,
|
||||
RtpEndUserState.RINGING,
|
||||
RtpEndUserState.CONNECTING)
|
||||
.contains(state)) {
|
||||
return ToneState.RINGING;
|
||||
}
|
||||
if (state == RtpEndUserState.DECLINED_OR_BUSY) {
|
||||
|
@ -67,15 +73,17 @@ class ToneManager {
|
|||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
if (this.endUserState == normalizeEndUserState) {
|
||||
return;
|
||||
|
@ -111,7 +119,7 @@ class ToneManager {
|
|||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unable to handle transition to "+state);
|
||||
throw new IllegalStateException("Unable to handle transition to " + state);
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
|
@ -133,29 +141,50 @@ class ToneManager {
|
|||
}
|
||||
|
||||
private void scheduleConnected() {
|
||||
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
|
||||
this.currentTone =
|
||||
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
|
||||
() -> {
|
||||
startTone(ToneGenerator.TONE_PROP_PROMPT, 200);
|
||||
}, 0, TimeUnit.SECONDS);
|
||||
},
|
||||
0,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void scheduleEnding() {
|
||||
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
|
||||
this.currentTone =
|
||||
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
|
||||
() -> {
|
||||
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() {
|
||||
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
|
||||
this.currentTone =
|
||||
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
|
||||
() -> {
|
||||
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() {
|
||||
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> {
|
||||
this.currentTone =
|
||||
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
|
||||
() -> {
|
||||
startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750);
|
||||
}, 0, 3, TimeUnit.SECONDS);
|
||||
},
|
||||
0,
|
||||
3,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private boolean noResetScheduled() {
|
||||
|
@ -181,34 +210,65 @@ class ToneManager {
|
|||
|
||||
private void configureAudioManagerForCall(final Set<Media> media) {
|
||||
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;
|
||||
}
|
||||
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
final AudioManager audioManager =
|
||||
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
if (audioManager == null) {
|
||||
return;
|
||||
}
|
||||
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.setSpeakerphoneOn(isSpeakerPhone);
|
||||
}
|
||||
|
||||
private void resetAudioManager() {
|
||||
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;
|
||||
}
|
||||
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
final AudioManager audioManager =
|
||||
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
if (audioManager == null) {
|
||||
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.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 {
|
||||
NULL, RINGING, CONNECTED, BUSY, ENDING_CALL
|
||||
NULL,
|
||||
RINGING,
|
||||
CONNECTED,
|
||||
BUSY,
|
||||
ENDING_CALL
|
||||
}
|
||||
}
|
|
@ -1,23 +1,18 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.google.common.base.Optional;
|
||||
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.PeerConnection;
|
||||
import org.webrtc.RtpSender;
|
||||
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> {
|
||||
public final T track;
|
||||
public final RtpSender rtpSender;
|
||||
|
@ -43,7 +38,9 @@ class TrackWrapper<T extends MediaStreamTrack> {
|
|||
final RtpTransceiver transceiver =
|
||||
peerConnection == null ? null : getTransceiver(peerConnection, trackWrapper);
|
||||
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);
|
||||
}
|
||||
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(
|
||||
@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()) {
|
||||
if (transceiver.getSender().id().equals(rtpSender.id())) {
|
||||
if (transceiver.getSender().id().equals(rtpSenderId)) {
|
||||
return transceiver;
|
||||
}
|
||||
}
|
|
@ -2,12 +2,15 @@ package eu.siacs.conversations.xmpp.jingle;
|
|||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
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.CameraEnumerationAndroid;
|
||||
import org.webrtc.CameraEnumerator;
|
||||
|
@ -17,14 +20,6 @@ import org.webrtc.PeerConnectionFactory;
|
|||
import org.webrtc.SurfaceTextureHelper;
|
||||
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 {
|
||||
|
||||
private static final int CAPTURING_RESOLUTION = 1920;
|
|
@ -5,7 +5,6 @@ import android.os.Build;
|
|||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Preconditions;
|
||||
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.MoreExecutors;
|
||||
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.AudioTrack;
|
||||
import org.webrtc.CandidatePairChangeEvent;
|
||||
|
@ -35,21 +44,6 @@ import org.webrtc.VideoTrack;
|
|||
import org.webrtc.audio.JavaAudioDeviceModule;
|
||||
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")
|
||||
public class WebRTCWrapper {
|
||||
|
||||
|
@ -206,7 +200,6 @@ public class WebRTCWrapper {
|
|||
@Nullable private PeerConnectionFactory peerConnectionFactory = null;
|
||||
@Nullable private PeerConnection peerConnection = null;
|
||||
private AppRTCAudioManager appRTCAudioManager = null;
|
||||
private ToneManager toneManager = null;
|
||||
private Context context = null;
|
||||
private EglBase eglBase = null;
|
||||
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(
|
||||
final XmppConnectionService service,
|
||||
final Context service,
|
||||
@Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
|
||||
throws InitializationException {
|
||||
try {
|
||||
|
@ -241,11 +242,10 @@ public class WebRTCWrapper {
|
|||
throw new InitializationException("Unable to create EGL base", e);
|
||||
}
|
||||
this.context = service;
|
||||
this.toneManager = service.getJingleConnectionManager().toneManager;
|
||||
mainHandler.post(
|
||||
() -> {
|
||||
appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
|
||||
toneManager.setAppRtcAudioManagerHasControl(true);
|
||||
ToneManager.getInstance(context).setAppRtcAudioManagerHasControl(true);
|
||||
appRTCAudioManager.start(audioManagerEvents);
|
||||
eventCallback.onAudioDeviceChanged(
|
||||
appRTCAudioManager.getSelectedAudioDevice(),
|
||||
|
@ -449,15 +449,19 @@ public class WebRTCWrapper {
|
|||
final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
|
||||
final AppRTCAudioManager audioManager = this.appRTCAudioManager;
|
||||
final EglBase eglBase = this.eglBase;
|
||||
final var localVideoTrack = this.localVideoTrack;
|
||||
if (peerConnection != null) {
|
||||
this.peerConnection = null;
|
||||
dispose(peerConnection);
|
||||
}
|
||||
if (audioManager != null) {
|
||||
toneManager.setAppRtcAudioManagerHasControl(false);
|
||||
ToneManager.getInstance(context).setAppRtcAudioManagerHasControl(false);
|
||||
mainHandler.post(audioManager::stop);
|
||||
}
|
||||
if (localVideoTrack != null) {
|
||||
this.localVideoTrack = null;
|
||||
dispose(localVideoTrack.track);
|
||||
}
|
||||
this.remoteVideoTrack = null;
|
||||
if (videoSourceWrapper != null) {
|
||||
this.videoSourceWrapper = null;
|
||||
|
@ -469,8 +473,8 @@ public class WebRTCWrapper {
|
|||
videoSourceWrapper.dispose();
|
||||
}
|
||||
if (eglBase != null) {
|
||||
eglBase.release();
|
||||
this.eglBase = null;
|
||||
eglBase.release();
|
||||
}
|
||||
if (peerConnectionFactory != null) {
|
||||
this.peerConnectionFactory = null;
|
|
@ -1,20 +1,16 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
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.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 Content(final Creator creator, final Senders senders, final String name) {
|
||||
|
@ -99,7 +95,6 @@ public class Content extends Element {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public void setTransport(GenericTransportInfo transportInfo) {
|
||||
this.addChild(transportInfo);
|
||||
}
|
||||
|
@ -140,7 +135,7 @@ public class Content extends Element {
|
|||
} else if (attributes.contains("recvonly")) {
|
||||
return initiator ? RESPONDER : INITIATOR;
|
||||
}
|
||||
Log.w(Config.LOGTAG,"assuming default value for senders");
|
||||
Log.w(Config.LOGTAG, "assuming default value for senders");
|
||||
// If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is
|
||||
// present, "sendrecv" SHOULD be assumed as the default
|
||||
// https://www.rfc-editor.org/rfc/rfc4566
|
|
@ -1,22 +1,14 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import im.conversations.android.xml.Element;
|
||||
import java.util.Arrays;
|
||||
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 static List<String> NAMESPACES = Arrays.asList(
|
||||
Version.FT_3.namespace,
|
||||
Version.FT_4.namespace,
|
||||
Version.FT_5.namespace
|
||||
);
|
||||
|
||||
public static List<String> NAMESPACES =
|
||||
Arrays.asList(Version.FT_3.namespace, Version.FT_4.namespace, Version.FT_5.namespace);
|
||||
|
||||
private FileTransferDescription(String name, String 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) {
|
||||
Preconditions.checkArgument("description".equals(element.getName()), "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());
|
||||
Preconditions.checkArgument(
|
||||
"description".equals(element.getName()),
|
||||
"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.setChildren(element.getChildren());
|
||||
return description;
|
|
@ -1,8 +1,7 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import im.conversations.android.xml.Element;
|
||||
|
||||
public class GenericDescription extends Element {
|
||||
|
||||
|
@ -12,7 +11,8 @@ public class GenericDescription extends Element {
|
|||
|
||||
public static GenericDescription upgrade(final Element element) {
|
||||
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.setChildren(element.getChildren());
|
||||
return description;
|
|
@ -1,8 +1,7 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import im.conversations.android.xml.Element;
|
||||
|
||||
public class GenericTransportInfo extends Element {
|
||||
|
||||
|
@ -12,7 +11,8 @@ public class GenericTransportInfo extends Element {
|
|||
|
||||
public static GenericTransportInfo upgrade(final Element element) {
|
||||
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.setChildren(element.getChildren());
|
||||
return transport;
|
|
@ -2,13 +2,11 @@ package eu.siacs.conversations.xmpp.jingle.stanzas;
|
|||
|
||||
import com.google.common.base.Preconditions;
|
||||
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.List;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
|
||||
public class Group extends Element {
|
||||
|
||||
private Group() {
|
||||
|
@ -45,10 +43,10 @@ public class Group extends Element {
|
|||
final String[] parts = input.split(" ");
|
||||
if (parts.length >= 2) {
|
||||
final String semantics = parts[0];
|
||||
for(int i = 1; i < parts.length; ++i) {
|
||||
for (int i = 1; i < parts.length; ++i) {
|
||||
tagBuilder.add(parts[i]);
|
||||
}
|
||||
return new Group(semantics,tagBuilder.build());
|
||||
return new Group(semantics, tagBuilder.build());
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import im.conversations.android.xml.Element;
|
||||
import im.conversations.android.xml.Namespace;
|
||||
|
||||
public class IbbTransportInfo extends GenericTransportInfo {
|
||||
|
||||
|
@ -36,9 +35,13 @@ public class IbbTransportInfo extends GenericTransportInfo {
|
|||
}
|
||||
|
||||
public static IbbTransportInfo upgrade(final Element element) {
|
||||
Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport");
|
||||
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);
|
||||
Preconditions.checkArgument(
|
||||
"transport".equals(element.getName()), "Name of provided element is not transport");
|
||||
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.setChildren(element.getChildren());
|
||||
return transportInfo;
|
|
@ -1,7 +1,6 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.MoreObjects;
|
||||
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.ImmutableList;
|
||||
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.Hashtable;
|
||||
import java.util.LinkedHashMap;
|
||||
|
@ -20,10 +21,6 @@ import java.util.Locale;
|
|||
import java.util.Map;
|
||||
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 static final IceUdpTransportInfo STUB = new IceUdpTransportInfo();
|
||||
|
@ -63,7 +60,10 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
|||
}
|
||||
|
||||
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();
|
||||
iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint));
|
||||
iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag);
|