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.media.MediaRecorder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
import org.webrtc.ThreadUtils;
|
import eu.siacs.conversations.utils.AppRTCUtils;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.Media;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import org.webrtc.ThreadUtils;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
/** AppRTCAudioManager manages all audio related parts of the AppRTC demo. */
|
||||||
import eu.siacs.conversations.utils.AppRTCUtils;
|
|
||||||
import eu.siacs.conversations.xmpp.jingle.Media;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
|
|
||||||
*/
|
|
||||||
public class AppRTCAudioManager {
|
public class AppRTCAudioManager {
|
||||||
|
|
||||||
private static CountDownLatch microphoneLatch;
|
private static CountDownLatch microphoneLatch;
|
||||||
|
|
||||||
private final Context apprtcContext;
|
private final Context apprtcContext;
|
||||||
// Contains speakerphone setting: auto, true or false
|
// Contains speakerphone setting: auto, true or false
|
||||||
@Nullable
|
@Nullable private SpeakerPhonePreference speakerPhonePreference;
|
||||||
private SpeakerPhonePreference speakerPhonePreference;
|
|
||||||
// Handles all tasks related to Bluetooth headset devices.
|
// Handles all tasks related to Bluetooth headset devices.
|
||||||
private final AppRTCBluetoothManager bluetoothManager;
|
private final AppRTCBluetoothManager bluetoothManager;
|
||||||
@Nullable
|
@Nullable private final AudioManager audioManager;
|
||||||
private final AudioManager audioManager;
|
@Nullable private AudioManagerEvents audioManagerEvents;
|
||||||
@Nullable
|
|
||||||
private AudioManagerEvents audioManagerEvents;
|
|
||||||
private AudioManagerState amState;
|
private AudioManagerState amState;
|
||||||
private boolean savedIsSpeakerPhoneOn;
|
private boolean savedIsSpeakerPhoneOn;
|
||||||
private boolean savedIsMicrophoneMute;
|
private boolean savedIsMicrophoneMute;
|
||||||
|
@ -74,18 +65,17 @@ public class AppRTCAudioManager {
|
||||||
// relative to the view screen of a device and can therefore be used to
|
// relative to the view screen of a device and can therefore be used to
|
||||||
// assist device switching (close to ear <=> use headset earpiece if
|
// assist device switching (close to ear <=> use headset earpiece if
|
||||||
// available, far from ear <=> use speaker phone).
|
// available, far from ear <=> use speaker phone).
|
||||||
@Nullable
|
@Nullable private AppRTCProximitySensor proximitySensor;
|
||||||
private AppRTCProximitySensor proximitySensor;
|
|
||||||
// Contains a list of available audio devices. A Set collection is used to
|
// Contains a list of available audio devices. A Set collection is used to
|
||||||
// avoid duplicate elements.
|
// avoid duplicate elements.
|
||||||
private Set<AudioDevice> audioDevices = new HashSet<>();
|
private Set<AudioDevice> audioDevices = new HashSet<>();
|
||||||
// Broadcast receiver for wired headset intent broadcasts.
|
// Broadcast receiver for wired headset intent broadcasts.
|
||||||
private final BroadcastReceiver wiredHeadsetReceiver;
|
private final BroadcastReceiver wiredHeadsetReceiver;
|
||||||
// Callback method for changes in audio focus.
|
// Callback method for changes in audio focus.
|
||||||
@Nullable
|
@Nullable private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
||||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
|
||||||
|
|
||||||
private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) {
|
private AppRTCAudioManager(
|
||||||
|
Context context, final SpeakerPhonePreference speakerPhonePreference) {
|
||||||
Log.d(Config.LOGTAG, "ctor");
|
Log.d(Config.LOGTAG, "ctor");
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
apprtcContext = context;
|
apprtcContext = context;
|
||||||
|
@ -102,11 +92,13 @@ public class AppRTCAudioManager {
|
||||||
// Create and initialize the proximity sensor.
|
// Create and initialize the proximity sensor.
|
||||||
// Tablet devices (e.g. Nexus 7) does not support proximity sensors.
|
// Tablet devices (e.g. Nexus 7) does not support proximity sensors.
|
||||||
// Note that, the sensor will not be active until start() has been called.
|
// Note that, the sensor will not be active until start() has been called.
|
||||||
proximitySensor = AppRTCProximitySensor.create(context,
|
proximitySensor =
|
||||||
// This method will be called each time a state change is detected.
|
AppRTCProximitySensor.create(
|
||||||
// Example: user holds his hand over the device (closer than ~5 cm),
|
context,
|
||||||
// or removes his hand from the device.
|
// This method will be called each time a state change is detected.
|
||||||
this::onProximitySensorChangedState);
|
// Example: user holds his hand over the device (closer than ~5 cm),
|
||||||
|
// or removes his hand from the device.
|
||||||
|
this::onProximitySensorChangedState);
|
||||||
Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice);
|
Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice);
|
||||||
AppRTCUtils.logDeviceInfo(Config.LOGTAG);
|
AppRTCUtils.logDeviceInfo(Config.LOGTAG);
|
||||||
}
|
}
|
||||||
|
@ -121,10 +113,9 @@ public class AppRTCAudioManager {
|
||||||
updateAudioDeviceState();
|
updateAudioDeviceState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Construction. */
|
||||||
* Construction.
|
public static AppRTCAudioManager create(
|
||||||
*/
|
Context context, SpeakerPhonePreference speakerPhonePreference) {
|
||||||
public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) {
|
|
||||||
return new AppRTCAudioManager(context, speakerPhonePreference);
|
return new AppRTCAudioManager(context, speakerPhonePreference);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,17 +128,18 @@ public class AppRTCAudioManager {
|
||||||
final int channel = AudioFormat.CHANNEL_IN_MONO;
|
final int channel = AudioFormat.CHANNEL_IN_MONO;
|
||||||
final int format = AudioFormat.ENCODING_PCM_16BIT;
|
final int format = AudioFormat.ENCODING_PCM_16BIT;
|
||||||
final int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format);
|
final int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format);
|
||||||
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channel, format, bufferSize);
|
audioRecord =
|
||||||
|
new AudioRecord(
|
||||||
|
MediaRecorder.AudioSource.MIC, sampleRate, channel, format, bufferSize);
|
||||||
audioRecord.startRecording();
|
audioRecord.startRecording();
|
||||||
final short[] buffer = new short[bufferSize];
|
final short[] buffer = new short[bufferSize];
|
||||||
final int audioStatus = audioRecord.read(buffer, 0, bufferSize);
|
final int audioStatus = audioRecord.read(buffer, 0, bufferSize);
|
||||||
if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION || audioStatus == AudioRecord.STATE_UNINITIALIZED)
|
if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION
|
||||||
available = false;
|
|| audioStatus == AudioRecord.STATE_UNINITIALIZED) available = false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
available = false;
|
available = false;
|
||||||
} finally {
|
} finally {
|
||||||
release(audioRecord);
|
release(audioRecord);
|
||||||
|
|
||||||
}
|
}
|
||||||
microphoneLatch.countDown();
|
microphoneLatch.countDown();
|
||||||
return available;
|
return available;
|
||||||
|
@ -160,13 +152,13 @@ public class AppRTCAudioManager {
|
||||||
try {
|
try {
|
||||||
audioRecord.release();
|
audioRecord.release();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called when the proximity sensor reports a state change,
|
* This method is called when the proximity sensor reports a state change, e.g. from "NEAR to
|
||||||
* e.g. from "NEAR to FAR" or from "FAR to NEAR".
|
* FAR" or from "FAR to NEAR".
|
||||||
*/
|
*/
|
||||||
private void onProximitySensorChangedState() {
|
private void onProximitySensorChangedState() {
|
||||||
if (speakerPhonePreference != SpeakerPhonePreference.AUTO) {
|
if (speakerPhonePreference != SpeakerPhonePreference.AUTO) {
|
||||||
|
@ -174,7 +166,8 @@ public class AppRTCAudioManager {
|
||||||
}
|
}
|
||||||
// The proximity sensor should only be activated when there are exactly two
|
// The proximity sensor should only be activated when there are exactly two
|
||||||
// available audio devices.
|
// available audio devices.
|
||||||
if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE)
|
if (audioDevices.size() == 2
|
||||||
|
&& audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE)
|
||||||
&& audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
|
&& audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
|
||||||
if (proximitySensor.sensorReportsNearState()) {
|
if (proximitySensor.sensorReportsNearState()) {
|
||||||
// Sensor reports that a "handset is being held up to a person's ear",
|
// Sensor reports that a "handset is being held up to a person's ear",
|
||||||
|
@ -204,48 +197,56 @@ public class AppRTCAudioManager {
|
||||||
savedIsMicrophoneMute = audioManager.isMicrophoneMute();
|
savedIsMicrophoneMute = audioManager.isMicrophoneMute();
|
||||||
hasWiredHeadset = hasWiredHeadset();
|
hasWiredHeadset = hasWiredHeadset();
|
||||||
// Create an AudioManager.OnAudioFocusChangeListener instance.
|
// Create an AudioManager.OnAudioFocusChangeListener instance.
|
||||||
audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
|
audioFocusChangeListener =
|
||||||
// Called on the listener to notify if the audio focus for this listener has been changed.
|
new AudioManager.OnAudioFocusChangeListener() {
|
||||||
// The |focusChange| value indicates whether the focus was gained, whether the focus was lost,
|
// Called on the listener to notify if the audio focus for this listener has
|
||||||
// and whether that loss is transient, or whether the new focus holder will hold it for an
|
// been changed.
|
||||||
// unknown amount of time.
|
// The |focusChange| value indicates whether the focus was gained, whether the
|
||||||
// TODO(henrika): possibly extend support of handling audio-focus changes. Only contains
|
// focus was lost,
|
||||||
// logging for now.
|
// and whether that loss is transient, or whether the new focus holder will hold
|
||||||
@Override
|
// it for an
|
||||||
public void onAudioFocusChange(int focusChange) {
|
// unknown amount of time.
|
||||||
final String typeOfChange;
|
// TODO(henrika): possibly extend support of handling audio-focus changes. Only
|
||||||
switch (focusChange) {
|
// contains
|
||||||
case AudioManager.AUDIOFOCUS_GAIN:
|
// logging for now.
|
||||||
typeOfChange = "AUDIOFOCUS_GAIN";
|
@Override
|
||||||
break;
|
public void onAudioFocusChange(int focusChange) {
|
||||||
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
|
final String typeOfChange;
|
||||||
typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT";
|
switch (focusChange) {
|
||||||
break;
|
case AudioManager.AUDIOFOCUS_GAIN:
|
||||||
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
|
typeOfChange = "AUDIOFOCUS_GAIN";
|
||||||
typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
|
break;
|
||||||
break;
|
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
|
||||||
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
|
typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT";
|
||||||
typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
|
break;
|
||||||
break;
|
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
|
||||||
case AudioManager.AUDIOFOCUS_LOSS:
|
typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
|
||||||
typeOfChange = "AUDIOFOCUS_LOSS";
|
break;
|
||||||
break;
|
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
|
||||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
|
||||||
typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT";
|
break;
|
||||||
break;
|
case AudioManager.AUDIOFOCUS_LOSS:
|
||||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
typeOfChange = "AUDIOFOCUS_LOSS";
|
||||||
typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
|
break;
|
||||||
break;
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||||
default:
|
typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT";
|
||||||
typeOfChange = "AUDIOFOCUS_INVALID";
|
break;
|
||||||
break;
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
||||||
}
|
typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
|
||||||
Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange);
|
break;
|
||||||
}
|
default:
|
||||||
};
|
typeOfChange = "AUDIOFOCUS_INVALID";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange);
|
||||||
|
}
|
||||||
|
};
|
||||||
// Request audio playout focus (without ducking) and install listener for changes in focus.
|
// Request audio playout focus (without ducking) and install listener for changes in focus.
|
||||||
int result = audioManager.requestAudioFocus(audioFocusChangeListener,
|
int result =
|
||||||
AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
|
audioManager.requestAudioFocus(
|
||||||
|
audioFocusChangeListener,
|
||||||
|
AudioManager.STREAM_VOICE_CALL,
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
|
||||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||||
Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams");
|
Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams");
|
||||||
} else {
|
} else {
|
||||||
|
@ -282,7 +283,7 @@ public class AppRTCAudioManager {
|
||||||
try {
|
try {
|
||||||
latch.await();
|
latch.await();
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
//ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,9 +313,7 @@ public class AppRTCAudioManager {
|
||||||
audioManagerEvents = null;
|
audioManagerEvents = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Changes selection of the currently active audio device. */
|
||||||
* Changes selection of the currently active audio device.
|
|
||||||
*/
|
|
||||||
private void setAudioDeviceInternal(AudioDevice device) {
|
private void setAudioDeviceInternal(AudioDevice device) {
|
||||||
Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
|
Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
|
||||||
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
|
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
|
||||||
|
@ -335,8 +334,8 @@ public class AppRTCAudioManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes default audio device.
|
* Changes default audio device. TODO(henrika): add usage of this method in the AppRTCMobile
|
||||||
* TODO(henrika): add usage of this method in the AppRTCMobile client.
|
* client.
|
||||||
*/
|
*/
|
||||||
public void setDefaultAudioDevice(AudioDevice defaultDevice) {
|
public void setDefaultAudioDevice(AudioDevice defaultDevice) {
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
@ -359,9 +358,7 @@ public class AppRTCAudioManager {
|
||||||
updateAudioDeviceState();
|
updateAudioDeviceState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Changes selection of the currently active audio device. */
|
||||||
* Changes selection of the currently active audio device.
|
|
||||||
*/
|
|
||||||
public void selectAudioDevice(AudioDevice device) {
|
public void selectAudioDevice(AudioDevice device) {
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
if (!audioDevices.contains(device)) {
|
if (!audioDevices.contains(device)) {
|
||||||
|
@ -371,39 +368,29 @@ public class AppRTCAudioManager {
|
||||||
updateAudioDeviceState();
|
updateAudioDeviceState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns current set of available/selectable audio devices. */
|
||||||
* Returns current set of available/selectable audio devices.
|
|
||||||
*/
|
|
||||||
public Set<AudioDevice> getAudioDevices() {
|
public Set<AudioDevice> getAudioDevices() {
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
return Collections.unmodifiableSet(new HashSet<>(audioDevices));
|
return Collections.unmodifiableSet(new HashSet<>(audioDevices));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the currently selected audio device. */
|
||||||
* Returns the currently selected audio device.
|
|
||||||
*/
|
|
||||||
public AudioDevice getSelectedAudioDevice() {
|
public AudioDevice getSelectedAudioDevice() {
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
return selectedAudioDevice;
|
return selectedAudioDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Helper method for receiver registration. */
|
||||||
* Helper method for receiver registration.
|
|
||||||
*/
|
|
||||||
private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
||||||
apprtcContext.registerReceiver(receiver, filter);
|
apprtcContext.registerReceiver(receiver, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Helper method for unregistration of an existing receiver. */
|
||||||
* Helper method for unregistration of an existing receiver.
|
|
||||||
*/
|
|
||||||
private void unregisterReceiver(BroadcastReceiver receiver) {
|
private void unregisterReceiver(BroadcastReceiver receiver) {
|
||||||
apprtcContext.unregisterReceiver(receiver);
|
apprtcContext.unregisterReceiver(receiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Sets the speaker phone mode. */
|
||||||
* Sets the speaker phone mode.
|
|
||||||
*/
|
|
||||||
private void setSpeakerphoneOn(boolean on) {
|
private void setSpeakerphoneOn(boolean on) {
|
||||||
boolean wasOn = audioManager.isSpeakerphoneOn();
|
boolean wasOn = audioManager.isSpeakerphoneOn();
|
||||||
if (wasOn == on) {
|
if (wasOn == on) {
|
||||||
|
@ -412,9 +399,7 @@ public class AppRTCAudioManager {
|
||||||
audioManager.setSpeakerphoneOn(on);
|
audioManager.setSpeakerphoneOn(on);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Sets the microphone mute state. */
|
||||||
* Sets the microphone mute state.
|
|
||||||
*/
|
|
||||||
private void setMicrophoneMute(boolean on) {
|
private void setMicrophoneMute(boolean on) {
|
||||||
boolean wasMuted = audioManager.isMicrophoneMute();
|
boolean wasMuted = audioManager.isMicrophoneMute();
|
||||||
if (wasMuted == on) {
|
if (wasMuted == on) {
|
||||||
|
@ -423,19 +408,15 @@ public class AppRTCAudioManager {
|
||||||
audioManager.setMicrophoneMute(on);
|
audioManager.setMicrophoneMute(on);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Gets the current earpiece state. */
|
||||||
* Gets the current earpiece state.
|
|
||||||
*/
|
|
||||||
private boolean hasEarpiece() {
|
private boolean hasEarpiece() {
|
||||||
return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
|
return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a wired headset is connected or not.
|
* Checks whether a wired headset is connected or not. This is not a valid indication that audio
|
||||||
* This is not a valid indication that audio playback is actually over
|
* playback is actually over the wired headset as audio routing depends on other conditions. We
|
||||||
* the wired headset as audio routing depends on other conditions. We
|
* only use it as an early indicator (during initialization) of an attached wired headset.
|
||||||
* only use it as an early indicator (during initialization) of an attached
|
|
||||||
* wired headset.
|
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
private boolean hasWiredHeadset() {
|
private boolean hasWiredHeadset() {
|
||||||
|
@ -458,18 +439,30 @@ public class AppRTCAudioManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates list of possible audio devices and make new device selection.
|
* Updates list of possible audio devices and make new device selection. TODO(henrika): add unit
|
||||||
* TODO(henrika): add unit test to verify all state transitions.
|
* test to verify all state transitions.
|
||||||
*/
|
*/
|
||||||
public void updateAudioDeviceState() {
|
public void updateAudioDeviceState() {
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
Log.d(Config.LOGTAG, "--- updateAudioDeviceState: "
|
Log.d(
|
||||||
+ "wired headset=" + hasWiredHeadset + ", "
|
Config.LOGTAG,
|
||||||
+ "BT state=" + bluetoothManager.getState());
|
"--- updateAudioDeviceState: "
|
||||||
Log.d(Config.LOGTAG, "Device status: "
|
+ "wired headset="
|
||||||
+ "available=" + audioDevices + ", "
|
+ hasWiredHeadset
|
||||||
+ "selected=" + selectedAudioDevice + ", "
|
+ ", "
|
||||||
+ "user selected=" + userSelectedAudioDevice);
|
+ "BT state="
|
||||||
|
+ bluetoothManager.getState());
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"Device status: "
|
||||||
|
+ "available="
|
||||||
|
+ audioDevices
|
||||||
|
+ ", "
|
||||||
|
+ "selected="
|
||||||
|
+ selectedAudioDevice
|
||||||
|
+ ", "
|
||||||
|
+ "user selected="
|
||||||
|
+ userSelectedAudioDevice);
|
||||||
// Check if any Bluetooth headset is connected. The internal BT state will
|
// Check if any Bluetooth headset is connected. The internal BT state will
|
||||||
// change accordingly.
|
// change accordingly.
|
||||||
// TODO(henrika): perhaps wrap required state into BT manager.
|
// TODO(henrika): perhaps wrap required state into BT manager.
|
||||||
|
@ -521,20 +514,28 @@ public class AppRTCAudioManager {
|
||||||
boolean needBluetoothAudioStart =
|
boolean needBluetoothAudioStart =
|
||||||
bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
||||||
&& (userSelectedAudioDevice == AudioDevice.NONE
|
&& (userSelectedAudioDevice == AudioDevice.NONE
|
||||||
|| userSelectedAudioDevice == AudioDevice.BLUETOOTH);
|
|| userSelectedAudioDevice == AudioDevice.BLUETOOTH);
|
||||||
// Need to stop Bluetooth audio if user selected different device and
|
// Need to stop Bluetooth audio if user selected different device and
|
||||||
// Bluetooth SCO connection is established or in the process.
|
// Bluetooth SCO connection is established or in the process.
|
||||||
boolean needBluetoothAudioStop =
|
boolean needBluetoothAudioStop =
|
||||||
(bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
(bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
||||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
|
|| bluetoothManager.getState()
|
||||||
|
== AppRTCBluetoothManager.State.SCO_CONNECTING)
|
||||||
&& (userSelectedAudioDevice != AudioDevice.NONE
|
&& (userSelectedAudioDevice != AudioDevice.NONE
|
||||||
&& userSelectedAudioDevice != AudioDevice.BLUETOOTH);
|
&& userSelectedAudioDevice != AudioDevice.BLUETOOTH);
|
||||||
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
||||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
||||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
||||||
Log.d(Config.LOGTAG, "Need BT audio: start=" + needBluetoothAudioStart + ", "
|
Log.d(
|
||||||
+ "stop=" + needBluetoothAudioStop + ", "
|
Config.LOGTAG,
|
||||||
+ "BT state=" + bluetoothManager.getState());
|
"Need BT audio: start="
|
||||||
|
+ needBluetoothAudioStart
|
||||||
|
+ ", "
|
||||||
|
+ "stop="
|
||||||
|
+ needBluetoothAudioStop
|
||||||
|
+ ", "
|
||||||
|
+ "BT state="
|
||||||
|
+ bluetoothManager.getState());
|
||||||
}
|
}
|
||||||
// Start or stop Bluetooth SCO connection given states set earlier.
|
// Start or stop Bluetooth SCO connection given states set earlier.
|
||||||
if (needBluetoothAudioStop) {
|
if (needBluetoothAudioStop) {
|
||||||
|
@ -563,7 +564,8 @@ public class AppRTCAudioManager {
|
||||||
} else {
|
} else {
|
||||||
// No wired headset and no Bluetooth, hence the audio-device list can contain speaker
|
// No wired headset and no Bluetooth, hence the audio-device list can contain speaker
|
||||||
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
||||||
// |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE
|
// |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or
|
||||||
|
// AudioDevice.EARPIECE
|
||||||
// depending on the user's selection.
|
// depending on the user's selection.
|
||||||
newAudioDevice = defaultAudioDevice;
|
newAudioDevice = defaultAudioDevice;
|
||||||
}
|
}
|
||||||
|
@ -571,9 +573,14 @@ public class AppRTCAudioManager {
|
||||||
if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
|
if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
|
||||||
// Do the required device switch.
|
// Do the required device switch.
|
||||||
setAudioDeviceInternal(newAudioDevice);
|
setAudioDeviceInternal(newAudioDevice);
|
||||||
Log.d(Config.LOGTAG, "New device status: "
|
Log.d(
|
||||||
+ "available=" + audioDevices + ", "
|
Config.LOGTAG,
|
||||||
+ "selected=" + newAudioDevice);
|
"New device status: "
|
||||||
|
+ "available="
|
||||||
|
+ audioDevices
|
||||||
|
+ ", "
|
||||||
|
+ "selected="
|
||||||
|
+ newAudioDevice);
|
||||||
if (audioManagerEvents != null) {
|
if (audioManagerEvents != null) {
|
||||||
// Notify a listening client that audio device has been changed.
|
// Notify a listening client that audio device has been changed.
|
||||||
audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
|
audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
|
||||||
|
@ -582,15 +589,16 @@ public class AppRTCAudioManager {
|
||||||
Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
|
Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** AudioDevice is the names of possible audio devices that we currently support. */
|
||||||
* AudioDevice is the names of possible audio devices that we currently
|
public enum AudioDevice {
|
||||||
* support.
|
SPEAKER_PHONE,
|
||||||
*/
|
WIRED_HEADSET,
|
||||||
public enum AudioDevice {SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE}
|
EARPIECE,
|
||||||
|
BLUETOOTH,
|
||||||
|
NONE
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/** AudioManager state. */
|
||||||
* AudioManager state.
|
|
||||||
*/
|
|
||||||
public enum AudioManagerState {
|
public enum AudioManagerState {
|
||||||
UNINITIALIZED,
|
UNINITIALIZED,
|
||||||
PREINITIALIZED,
|
PREINITIALIZED,
|
||||||
|
@ -598,7 +606,9 @@ public class AppRTCAudioManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SpeakerPhonePreference {
|
public enum SpeakerPhonePreference {
|
||||||
AUTO, EARPIECE, SPEAKER;
|
AUTO,
|
||||||
|
EARPIECE,
|
||||||
|
SPEAKER;
|
||||||
|
|
||||||
public static SpeakerPhonePreference of(final Set<Media> media) {
|
public static SpeakerPhonePreference of(final Set<Media> media) {
|
||||||
if (media.contains(Media.VIDEO)) {
|
if (media.contains(Media.VIDEO)) {
|
||||||
|
@ -609,9 +619,7 @@ public class AppRTCAudioManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Selected audio device change event. */
|
||||||
* Selected audio device change event.
|
|
||||||
*/
|
|
||||||
public interface AudioManagerEvents {
|
public interface AudioManagerEvents {
|
||||||
// Callback fired once audio device is changed or list of available audio devices changed.
|
// Callback fired once audio device is changed or list of available audio devices changed.
|
||||||
void onAudioDeviceChanged(
|
void onAudioDeviceChanged(
|
||||||
|
@ -630,11 +638,21 @@ public class AppRTCAudioManager {
|
||||||
int state = intent.getIntExtra("state", STATE_UNPLUGGED);
|
int state = intent.getIntExtra("state", STATE_UNPLUGGED);
|
||||||
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
|
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
|
||||||
String name = intent.getStringExtra("name");
|
String name = intent.getStringExtra("name");
|
||||||
Log.d(Config.LOGTAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": "
|
Log.d(
|
||||||
+ "a=" + intent.getAction() + ", s="
|
Config.LOGTAG,
|
||||||
+ (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m="
|
"WiredHeadsetReceiver.onReceive"
|
||||||
+ (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb="
|
+ AppRTCUtils.getThreadInfo()
|
||||||
+ isInitialStickyBroadcast());
|
+ ": "
|
||||||
|
+ "a="
|
||||||
|
+ intent.getAction()
|
||||||
|
+ ", s="
|
||||||
|
+ (state == STATE_UNPLUGGED ? "unplugged" : "plugged")
|
||||||
|
+ ", m="
|
||||||
|
+ (microphone == HAS_MIC ? "mic" : "no mic")
|
||||||
|
+ ", n="
|
||||||
|
+ name
|
||||||
|
+ ", sb="
|
||||||
|
+ isInitialStickyBroadcast());
|
||||||
hasWiredHeadset = (state == STATE_PLUGGED);
|
hasWiredHeadset = (state == STATE_PLUGGED);
|
||||||
updateAudioDeviceState();
|
updateAudioDeviceState();
|
||||||
}
|
}
|
|
@ -25,19 +25,14 @@ import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
import org.webrtc.ThreadUtils;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.utils.AppRTCUtils;
|
import eu.siacs.conversations.utils.AppRTCUtils;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.webrtc.ThreadUtils;
|
||||||
|
|
||||||
/** AppRTCProximitySensor manages functions related to Bluetoth devices in the AppRTC demo. */
|
/** AppRTCProximitySensor manages functions related to Bluetoth devices in the AppRTC demo. */
|
||||||
public class AppRTCBluetoothManager {
|
public class AppRTCBluetoothManager {
|
|
@ -16,22 +16,17 @@ import android.hardware.SensorEventListener;
|
||||||
import android.hardware.SensorManager;
|
import android.hardware.SensorManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.webrtc.ThreadUtils;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.utils.AppRTCUtils;
|
import eu.siacs.conversations.utils.AppRTCUtils;
|
||||||
|
import org.webrtc.ThreadUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AppRTCProximitySensor manages functions related to the proximity sensor in
|
* AppRTCProximitySensor manages functions related to the proximity sensor in the AppRTC demo. On
|
||||||
* the AppRTC demo.
|
* most device, the proximity sensor is implemented as a boolean-sensor. It returns just two values
|
||||||
* On most device, the proximity sensor is implemented as a boolean-sensor.
|
* "NEAR" or "FAR". Thresholding is done on the LUX value i.e. the LUX value of the light sensor is
|
||||||
* It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX
|
* compared with a threshold. A LUX-value more than the threshold means the proximity sensor returns
|
||||||
* value i.e. the LUX value of the light sensor is compared with a threshold.
|
* "FAR". Anything less than the threshold value and the sensor returns "NEAR".
|
||||||
* A LUX-value more than the threshold means the proximity sensor returns "FAR".
|
|
||||||
* Anything less than the threshold value and the sensor returns "NEAR".
|
|
||||||
*/
|
*/
|
||||||
public class AppRTCProximitySensor implements SensorEventListener {
|
public class AppRTCProximitySensor implements SensorEventListener {
|
||||||
// This class should be created, started and stopped on one thread
|
// This class should be created, started and stopped on one thread
|
||||||
|
@ -40,8 +35,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
||||||
private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker();
|
private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker();
|
||||||
private final Runnable onSensorStateListener;
|
private final Runnable onSensorStateListener;
|
||||||
private final SensorManager sensorManager;
|
private final SensorManager sensorManager;
|
||||||
@Nullable
|
@Nullable private Sensor proximitySensor;
|
||||||
private Sensor proximitySensor;
|
|
||||||
private boolean lastStateReportIsNear;
|
private boolean lastStateReportIsNear;
|
||||||
|
|
||||||
private AppRTCProximitySensor(Context context, Runnable sensorStateListener) {
|
private AppRTCProximitySensor(Context context, Runnable sensorStateListener) {
|
||||||
|
@ -50,17 +44,12 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
||||||
sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE));
|
sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Construction */
|
||||||
* Construction
|
|
||||||
*/
|
|
||||||
static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) {
|
static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) {
|
||||||
return new AppRTCProximitySensor(context, sensorStateListener);
|
return new AppRTCProximitySensor(context, sensorStateListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Activate the proximity sensor. Also do initialization if called for the first time. */
|
||||||
* Activate the proximity sensor. Also do initialization if called for the
|
|
||||||
* first time.
|
|
||||||
*/
|
|
||||||
public boolean start() {
|
public boolean start() {
|
||||||
threadChecker.checkIsOnValidThread();
|
threadChecker.checkIsOnValidThread();
|
||||||
Log.d(Config.LOGTAG, "start" + AppRTCUtils.getThreadInfo());
|
Log.d(Config.LOGTAG, "start" + AppRTCUtils.getThreadInfo());
|
||||||
|
@ -72,9 +61,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Deactivate the proximity sensor. */
|
||||||
* Deactivate the proximity sensor.
|
|
||||||
*/
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
threadChecker.checkIsOnValidThread();
|
threadChecker.checkIsOnValidThread();
|
||||||
Log.d(Config.LOGTAG, "stop" + AppRTCUtils.getThreadInfo());
|
Log.d(Config.LOGTAG, "stop" + AppRTCUtils.getThreadInfo());
|
||||||
|
@ -84,9 +71,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
||||||
sensorManager.unregisterListener(this, proximitySensor);
|
sensorManager.unregisterListener(this, proximitySensor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Getter for last reported state. Set to true if "near" is reported. */
|
||||||
* Getter for last reported state. Set to true if "near" is reported.
|
|
||||||
*/
|
|
||||||
public boolean sensorReportsNearState() {
|
public boolean sensorReportsNearState() {
|
||||||
threadChecker.checkIsOnValidThread();
|
threadChecker.checkIsOnValidThread();
|
||||||
return lastStateReportIsNear;
|
return lastStateReportIsNear;
|
||||||
|
@ -120,15 +105,22 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
||||||
if (onSensorStateListener != null) {
|
if (onSensorStateListener != null) {
|
||||||
onSensorStateListener.run();
|
onSensorStateListener.run();
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": "
|
Log.d(
|
||||||
+ "accuracy=" + event.accuracy + ", timestamp=" + event.timestamp + ", distance="
|
Config.LOGTAG,
|
||||||
+ event.values[0]);
|
"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)
|
* Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) does not support
|
||||||
* does not support this type of sensor and false will be returned in such
|
* this type of sensor and false will be returned in such cases.
|
||||||
* cases.
|
|
||||||
*/
|
*/
|
||||||
private boolean initDefaultSensor() {
|
private boolean initDefaultSensor() {
|
||||||
if (proximitySensor != null) {
|
if (proximitySensor != null) {
|
||||||
|
@ -142,9 +134,7 @@ public class AppRTCProximitySensor implements SensorEventListener {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Helper method for logging information about the proximity sensor. */
|
||||||
* Helper method for logging information about the proximity sensor.
|
|
||||||
*/
|
|
||||||
private void logProximitySensorInfo() {
|
private void logProximitySensorInfo() {
|
||||||
if (proximitySensor == null) {
|
if (proximitySensor == null) {
|
||||||
return;
|
return;
|
|
@ -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.base.Objects;
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public final class ContentAddition {
|
public final class ContentAddition {
|
||||||
|
|
|
@ -10,8 +10,7 @@ import java.util.ArrayList;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import org.jxmpp.jid.Jid;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
|
||||||
|
|
||||||
public class DirectConnectionUtils {
|
public class DirectConnectionUtils {
|
||||||
|
|
||||||
|
@ -25,18 +24,19 @@ public class DirectConnectionUtils {
|
||||||
}
|
}
|
||||||
while (interfaces.hasMoreElements()) {
|
while (interfaces.hasMoreElements()) {
|
||||||
NetworkInterface networkInterface = interfaces.nextElement();
|
NetworkInterface networkInterface = interfaces.nextElement();
|
||||||
final Enumeration<InetAddress> inetAddressEnumeration = networkInterface.getInetAddresses();
|
final Enumeration<InetAddress> inetAddressEnumeration =
|
||||||
|
networkInterface.getInetAddresses();
|
||||||
while (inetAddressEnumeration.hasMoreElements()) {
|
while (inetAddressEnumeration.hasMoreElements()) {
|
||||||
final InetAddress inetAddress = inetAddressEnumeration.nextElement();
|
final InetAddress inetAddress = inetAddressEnumeration.nextElement();
|
||||||
if (inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) {
|
if (inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (inetAddress instanceof Inet6Address) {
|
if (inetAddress instanceof Inet6Address) {
|
||||||
//let's get rid of scope
|
// let's get rid of scope
|
||||||
try {
|
try {
|
||||||
addresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
|
addresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
//ignored
|
// ignored
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addresses.add(inetAddress);
|
addresses.add(inetAddress);
|
||||||
|
@ -50,7 +50,8 @@ public class DirectConnectionUtils {
|
||||||
SecureRandom random = new SecureRandom();
|
SecureRandom random = new SecureRandom();
|
||||||
ArrayList<JingleCandidate> candidates = new ArrayList<>();
|
ArrayList<JingleCandidate> candidates = new ArrayList<>();
|
||||||
for (InetAddress inetAddress : getLocalAddresses()) {
|
for (InetAddress inetAddress : getLocalAddresses()) {
|
||||||
final JingleCandidate candidate = new JingleCandidate(UUID.randomUUID().toString(), true);
|
final JingleCandidate candidate =
|
||||||
|
new JingleCandidate(UUID.randomUUID().toString(), true);
|
||||||
candidate.setHost(inetAddress.getHostAddress());
|
candidate.setHost(inetAddress.getHostAddress());
|
||||||
candidate.setPort(random.nextInt(60000) + 1024);
|
candidate.setPort(random.nextInt(60000) + 1024);
|
||||||
candidate.setType(JingleCandidate.TYPE_DIRECT);
|
candidate.setType(JingleCandidate.TYPE_DIRECT);
|
||||||
|
@ -60,5 +61,4 @@ public class DirectConnectionUtils {
|
||||||
}
|
}
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public enum Media {
|
public enum Media {
|
||||||
|
VIDEO,
|
||||||
VIDEO, AUDIO, UNKNOWN;
|
AUDIO,
|
||||||
|
UNKNOWN;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nonnull
|
@Nonnull
|
|
@ -1,7 +1,6 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
import com.google.common.collect.ArrayListMultimap;
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class MediaBuilder {
|
public class MediaBuilder {
|
||||||
|
@ -10,7 +9,7 @@ public class MediaBuilder {
|
||||||
private String protocol;
|
private String protocol;
|
||||||
private List<Integer> formats;
|
private List<Integer> formats;
|
||||||
private String connectionData;
|
private String connectionData;
|
||||||
private ArrayListMultimap<String,String> attributes;
|
private ArrayListMultimap<String, String> attributes;
|
||||||
|
|
||||||
public MediaBuilder setMedia(String media) {
|
public MediaBuilder setMedia(String media) {
|
||||||
this.media = media;
|
this.media = media;
|
||||||
|
@ -37,12 +36,13 @@ public class MediaBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaBuilder setAttributes(ArrayListMultimap<String,String> attributes) {
|
public MediaBuilder setAttributes(ArrayListMultimap<String, String> attributes) {
|
||||||
this.attributes = attributes;
|
this.attributes = attributes;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SessionDescription.Media createMedia() {
|
public SessionDescription.Media createMedia() {
|
||||||
return new SessionDescription.Media(media, port, protocol, formats, connectionData, attributes);
|
return new SessionDescription.Media(
|
||||||
|
media, port, protocol, formats, connectionData, attributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,17 +2,16 @@ package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects;
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import im.conversations.android.axolotl.AxolotlService;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
|
||||||
|
|
||||||
public class OmemoVerification {
|
public class OmemoVerification {
|
||||||
|
|
||||||
private final AtomicBoolean deviceIdWritten = new AtomicBoolean(false);
|
private final AtomicBoolean deviceIdWritten = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean sessionFingerprintWritten = new AtomicBoolean(false);
|
private final AtomicBoolean identityKeyWritten = new AtomicBoolean(false);
|
||||||
private Integer deviceId;
|
private Integer deviceId;
|
||||||
private String sessionFingerprint;
|
private IdentityKey identityKey;
|
||||||
|
|
||||||
public void setDeviceId(final Integer id) {
|
public void setDeviceId(final Integer id) {
|
||||||
if (deviceIdWritten.compareAndSet(false, true)) {
|
if (deviceIdWritten.compareAndSet(false, true)) {
|
||||||
|
@ -31,31 +30,32 @@ public class OmemoVerification {
|
||||||
return this.deviceId != null;
|
return this.deviceId != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSessionFingerprint(final String fingerprint) {
|
public void setSessionFingerprint(final IdentityKey identityKey) {
|
||||||
Preconditions.checkNotNull(fingerprint, "Session fingerprint must not be null");
|
Preconditions.checkNotNull(identityKey, "IdentityKey must not be null");
|
||||||
if (sessionFingerprintWritten.compareAndSet(false, true)) {
|
if (identityKeyWritten.compareAndSet(false, true)) {
|
||||||
this.sessionFingerprint = fingerprint;
|
this.identityKey = identityKey;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new IllegalStateException("Session fingerprint has already been set");
|
throw new IllegalStateException("Identity Key has already been set");
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFingerprint() {
|
public IdentityKey getFingerprint() {
|
||||||
return this.sessionFingerprint;
|
return this.identityKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOrEnsureEqual(AxolotlService.OmemoVerifiedPayload<?> omemoVerifiedPayload) {
|
public void setOrEnsureEqual(AxolotlService.OmemoVerifiedPayload<?> omemoVerifiedPayload) {
|
||||||
setOrEnsureEqual(omemoVerifiedPayload.getDeviceId(), omemoVerifiedPayload.getFingerprint());
|
setOrEnsureEqual(omemoVerifiedPayload.getDeviceId(), omemoVerifiedPayload.getFingerprint());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOrEnsureEqual(final int deviceId, final String sessionFingerprint) {
|
public void setOrEnsureEqual(final int deviceId, final IdentityKey identityKey) {
|
||||||
Preconditions.checkNotNull(sessionFingerprint, "Session fingerprint must not be null");
|
Preconditions.checkNotNull(identityKey, "IdentityKey must not be null");
|
||||||
if (this.deviceIdWritten.get() || this.sessionFingerprintWritten.get()) {
|
if (this.deviceIdWritten.get() || this.identityKeyWritten.get()) {
|
||||||
if (this.sessionFingerprint == null) {
|
if (this.identityKey == null) {
|
||||||
throw new IllegalStateException("No session fingerprint has been previously provided");
|
throw new IllegalStateException(
|
||||||
|
"No session fingerprint has been previously provided");
|
||||||
}
|
}
|
||||||
if (!sessionFingerprint.equals(this.sessionFingerprint)) {
|
if (!identityKey.equals(this.identityKey)) {
|
||||||
throw new SecurityException("Session Fingerprints did not match");
|
throw new SecurityException("IdentityKeys did not match");
|
||||||
}
|
}
|
||||||
if (this.deviceId == null) {
|
if (this.deviceId == null) {
|
||||||
throw new IllegalStateException("No Device Id has been previously provided");
|
throw new IllegalStateException("No Device Id has been previously provided");
|
||||||
|
@ -64,20 +64,20 @@ public class OmemoVerification {
|
||||||
throw new IllegalStateException("Device Ids did not match");
|
throw new IllegalStateException("Device Ids did not match");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setSessionFingerprint(sessionFingerprint);
|
this.setSessionFingerprint(identityKey);
|
||||||
this.setDeviceId(deviceId);
|
this.setDeviceId(deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasFingerprint() {
|
public boolean hasFingerprint() {
|
||||||
return this.sessionFingerprint != null;
|
return this.identityKey != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return MoreObjects.toStringHelper(this)
|
return MoreObjects.toStringHelper(this)
|
||||||
.add("deviceId", deviceId)
|
.add("deviceId", deviceId)
|
||||||
.add("fingerprint", sessionFingerprint)
|
.add("fingerprint", identityKey)
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,19 +1,20 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class OmemoVerifiedRtpContentMap extends RtpContentMap {
|
public class OmemoVerifiedRtpContentMap extends RtpContentMap {
|
||||||
public OmemoVerifiedRtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
|
public OmemoVerifiedRtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
|
||||||
super(group, contents);
|
super(group, contents);
|
||||||
for(final DescriptionTransport descriptionTransport : contents.values()) {
|
for (final DescriptionTransport descriptionTransport : contents.values()) {
|
||||||
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
|
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
|
||||||
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint();
|
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport)
|
||||||
|
.ensureNoPlaintextFingerprint();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
throw new IllegalStateException("OmemoVerifiedRtpContentMap contains non-verified transport info");
|
throw new IllegalStateException(
|
||||||
|
"OmemoVerifiedRtpContentMap contains non-verified transport info");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
public interface OnPrimaryCandidateFound {
|
public interface OnPrimaryCandidateFound {
|
||||||
void onPrimaryCandidateFound(boolean success, JingleCandidate canditate);
|
void onPrimaryCandidateFound(boolean success, JingleCandidate canditate);
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
public interface OnTransportConnected {
|
public interface OnTransportConnected {
|
||||||
void failed();
|
void failed();
|
||||||
|
|
||||||
void established();
|
void established();
|
||||||
}
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
import eu.siacs.conversations.entities.Account;
|
import org.jxmpp.jid.Jid;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
|
||||||
|
|
||||||
public interface OngoingRtpSession {
|
public interface OngoingRtpSession {
|
||||||
Account getAccount();
|
|
||||||
Jid getWith();
|
Jid getWith();
|
||||||
|
|
||||||
String getSessionId();
|
String getSessionId();
|
||||||
}
|
}
|
|
@ -12,15 +12,6 @@ import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
|
||||||
|
@ -29,6 +20,12 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public class RtpContentMap {
|
public class RtpContentMap {
|
||||||
|
|
||||||
|
@ -94,7 +91,7 @@ public class RtpContentMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<Content.Senders> getSenders() {
|
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() {
|
public List<String> getNames() {
|
||||||
|
@ -136,7 +133,8 @@ public class RtpContentMap {
|
||||||
if (setup == null) {
|
if (setup == null) {
|
||||||
throw new SecurityException(
|
throw new SecurityException(
|
||||||
String.format(
|
String.format(
|
||||||
"Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute",
|
"Use of DTLS-SRTP (XEP-0320) is required for content %s but"
|
||||||
|
+ " missing setup attribute",
|
||||||
entry.getKey()));
|
entry.getKey()));
|
||||||
}
|
}
|
||||||
if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
|
if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
|
||||||
|
@ -317,7 +315,8 @@ public class RtpContentMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
public RtpContentMap activeContents() {
|
public RtpContentMap activeContents() {
|
||||||
return new RtpContentMap(group, Maps.filterValues(this.contents, dt -> dt.senders != Content.Senders.NONE));
|
return new RtpContentMap(
|
||||||
|
group, Maps.filterValues(this.contents, dt -> dt.senders != Content.Senders.NONE));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Diff diff(final RtpContentMap rtpContentMap) {
|
public Diff diff(final RtpContentMap rtpContentMap) {
|
||||||
|
@ -343,10 +342,10 @@ public class RtpContentMap {
|
||||||
final IceUdpTransportInfo iceUdpTransportInfo =
|
final IceUdpTransportInfo iceUdpTransportInfo =
|
||||||
IceUdpTransportInfo.of(credentials, setup, dtls.hash, dtls.fingerprint);
|
IceUdpTransportInfo.of(credentials, setup, dtls.hash, dtls.fingerprint);
|
||||||
final Map<String, DescriptionTransport> combined = merge(contents, modification.contents);
|
final Map<String, DescriptionTransport> combined = merge(contents, modification.contents);
|
||||||
/*new ImmutableMap.Builder<String, DescriptionTransport>()
|
/*new ImmutableMap.Builder<String, DescriptionTransport>()
|
||||||
.putAll(contents)
|
.putAll(contents)
|
||||||
.putAll(modification.contents)
|
.putAll(modification.contents)
|
||||||
.build();*/
|
.build();*/
|
||||||
final Map<String, DescriptionTransport> combinedFixedTransport =
|
final Map<String, DescriptionTransport> combinedFixedTransport =
|
||||||
Maps.transformValues(
|
Maps.transformValues(
|
||||||
combined,
|
combined,
|
|
@ -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.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
import com.google.common.base.CharMatcher;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.ArrayListMultimap;
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
||||||
|
import im.conversations.android.xml.Namespace;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class SessionDescription {
|
public class SessionDescription {
|
||||||
|
|
||||||
|
@ -128,7 +124,8 @@ public class SessionDescription {
|
||||||
return sessionDescriptionBuilder.createSessionDescription();
|
return sessionDescriptionBuilder.createSessionDescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SessionDescription of(final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
|
public static SessionDescription of(
|
||||||
|
final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
|
||||||
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
|
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
|
||||||
final ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
|
final ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
|
||||||
final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>();
|
final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>();
|
||||||
|
@ -297,7 +294,8 @@ public class SessionDescription {
|
||||||
|
|
||||||
mediaAttributes.put("mid", name);
|
mediaAttributes.put("mid", name);
|
||||||
|
|
||||||
mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), "");
|
mediaAttributes.put(
|
||||||
|
descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), "");
|
||||||
if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP) || group != null) {
|
if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP) || group != null) {
|
||||||
mediaAttributes.put("rtcp-mux", "");
|
mediaAttributes.put("rtcp-mux", "");
|
||||||
}
|
}
|
|
@ -1,14 +1,13 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
import com.google.common.collect.ArrayListMultimap;
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class SessionDescriptionBuilder {
|
public class SessionDescriptionBuilder {
|
||||||
private int version;
|
private int version;
|
||||||
private String name;
|
private String name;
|
||||||
private String connectionData;
|
private String connectionData;
|
||||||
private ArrayListMultimap<String,String> attributes;
|
private ArrayListMultimap<String, String> attributes;
|
||||||
private List<SessionDescription.Media> media;
|
private List<SessionDescription.Media> media;
|
||||||
|
|
||||||
public SessionDescriptionBuilder setVersion(int version) {
|
public SessionDescriptionBuilder setVersion(int version) {
|
||||||
|
@ -26,7 +25,7 @@ public class SessionDescriptionBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SessionDescriptionBuilder setAttributes(ArrayListMultimap<String,String> attributes) {
|
public SessionDescriptionBuilder setAttributes(ArrayListMultimap<String, String> attributes) {
|
||||||
this.attributes = attributes;
|
this.attributes = attributes;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
|
@ -1,20 +1,19 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.ToneGenerator;
|
import android.media.ToneGenerator;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import im.conversations.android.xmpp.manager.JingleConnectionManager;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
public class ToneManager {
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
|
||||||
|
|
||||||
class ToneManager {
|
|
||||||
|
|
||||||
private final ToneGenerator toneGenerator;
|
private final ToneGenerator toneGenerator;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
@ -25,7 +24,9 @@ class ToneManager {
|
||||||
private ScheduledFuture<?> currentResetFuture;
|
private ScheduledFuture<?> currentResetFuture;
|
||||||
private boolean appRtcAudioManagerHasControl = false;
|
private boolean appRtcAudioManagerHasControl = false;
|
||||||
|
|
||||||
ToneManager(final Context context) {
|
private static volatile ToneManager INSTANCE;
|
||||||
|
|
||||||
|
private ToneManager(final Context context) {
|
||||||
ToneGenerator toneGenerator;
|
ToneGenerator toneGenerator;
|
||||||
try {
|
try {
|
||||||
toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 60);
|
toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 60);
|
||||||
|
@ -34,12 +35,17 @@ class ToneManager {
|
||||||
toneGenerator = null;
|
toneGenerator = null;
|
||||||
}
|
}
|
||||||
this.toneGenerator = toneGenerator;
|
this.toneGenerator = toneGenerator;
|
||||||
this.context = context;
|
this.context = context.getApplicationContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
|
private static ToneState of(
|
||||||
|
final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
|
||||||
if (isInitiator) {
|
if (isInitiator) {
|
||||||
if (asList(RtpEndUserState.FINDING_DEVICE, RtpEndUserState.RINGING, RtpEndUserState.CONNECTING).contains(state)) {
|
if (asList(
|
||||||
|
RtpEndUserState.FINDING_DEVICE,
|
||||||
|
RtpEndUserState.RINGING,
|
||||||
|
RtpEndUserState.CONNECTING)
|
||||||
|
.contains(state)) {
|
||||||
return ToneState.RINGING;
|
return ToneState.RINGING;
|
||||||
}
|
}
|
||||||
if (state == RtpEndUserState.DECLINED_OR_BUSY) {
|
if (state == RtpEndUserState.DECLINED_OR_BUSY) {
|
||||||
|
@ -67,15 +73,17 @@ class ToneManager {
|
||||||
return ToneState.NULL;
|
return ToneState.NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
void transition(final RtpEndUserState state, final Set<Media> media) {
|
public void transition(final RtpEndUserState state, final Set<Media> media) {
|
||||||
transition(state, of(true, state, media), media);
|
transition(state, of(true, state, media), media);
|
||||||
}
|
}
|
||||||
|
|
||||||
void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
|
void transition(
|
||||||
|
final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
|
||||||
transition(state, of(isInitiator, state, media), media);
|
transition(state, of(isInitiator, state, media), media);
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void transition(final RtpEndUserState endUserState, final ToneState state, final Set<Media> media) {
|
private synchronized void transition(
|
||||||
|
final RtpEndUserState endUserState, final ToneState state, final Set<Media> media) {
|
||||||
final RtpEndUserState normalizeEndUserState = normalize(endUserState);
|
final RtpEndUserState normalizeEndUserState = normalize(endUserState);
|
||||||
if (this.endUserState == normalizeEndUserState) {
|
if (this.endUserState == normalizeEndUserState) {
|
||||||
return;
|
return;
|
||||||
|
@ -111,7 +119,7 @@ class ToneManager {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unable to handle transition to "+state);
|
throw new IllegalStateException("Unable to handle transition to " + state);
|
||||||
}
|
}
|
||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
@ -133,29 +141,50 @@ class ToneManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleConnected() {
|
private void scheduleConnected() {
|
||||||
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
|
this.currentTone =
|
||||||
startTone(ToneGenerator.TONE_PROP_PROMPT, 200);
|
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
|
||||||
}, 0, TimeUnit.SECONDS);
|
() -> {
|
||||||
|
startTone(ToneGenerator.TONE_PROP_PROMPT, 200);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleEnding() {
|
private void scheduleEnding() {
|
||||||
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
|
this.currentTone =
|
||||||
startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
|
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
|
||||||
}, 0, TimeUnit.SECONDS);
|
() -> {
|
||||||
this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 375, TimeUnit.MILLISECONDS);
|
startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
|
this.currentResetFuture =
|
||||||
|
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
|
||||||
|
this::resetAudioManager, 375, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleBusy() {
|
private void scheduleBusy() {
|
||||||
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
|
this.currentTone =
|
||||||
startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
|
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
|
||||||
}, 0, TimeUnit.SECONDS);
|
() -> {
|
||||||
this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 2500, TimeUnit.MILLISECONDS);
|
startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
|
this.currentResetFuture =
|
||||||
|
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
|
||||||
|
this::resetAudioManager, 2500, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleWaitingTone() {
|
private void scheduleWaitingTone() {
|
||||||
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> {
|
this.currentTone =
|
||||||
startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750);
|
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
|
||||||
}, 0, 3, TimeUnit.SECONDS);
|
() -> {
|
||||||
|
startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean noResetScheduled() {
|
private boolean noResetScheduled() {
|
||||||
|
@ -181,34 +210,65 @@ class ToneManager {
|
||||||
|
|
||||||
private void configureAudioManagerForCall(final Set<Media> media) {
|
private void configureAudioManagerForCall(final Set<Media> media) {
|
||||||
if (appRtcAudioManagerHasControl) {
|
if (appRtcAudioManagerHasControl) {
|
||||||
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not configure audio manager because RTC has control");
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
ToneManager.class.getName()
|
||||||
|
+ ": do not configure audio manager because RTC has control");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
final AudioManager audioManager =
|
||||||
|
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
if (audioManager == null) {
|
if (audioManager == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final boolean isSpeakerPhone = media.contains(Media.VIDEO);
|
final boolean isSpeakerPhone = media.contains(Media.VIDEO);
|
||||||
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager into communication mode. speaker=" + isSpeakerPhone);
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
ToneManager.class.getName()
|
||||||
|
+ ": putting AudioManager into communication mode. speaker="
|
||||||
|
+ isSpeakerPhone);
|
||||||
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
||||||
audioManager.setSpeakerphoneOn(isSpeakerPhone);
|
audioManager.setSpeakerphoneOn(isSpeakerPhone);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetAudioManager() {
|
private void resetAudioManager() {
|
||||||
if (appRtcAudioManagerHasControl) {
|
if (appRtcAudioManagerHasControl) {
|
||||||
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not reset audio manager because RTC has control");
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
ToneManager.class.getName()
|
||||||
|
+ ": do not reset audio manager because RTC has control");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
final AudioManager audioManager =
|
||||||
|
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
if (audioManager == null) {
|
if (audioManager == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager back into normal mode");
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
ToneManager.class.getName() + ": putting AudioManager back into normal mode");
|
||||||
audioManager.setMode(AudioManager.MODE_NORMAL);
|
audioManager.setMode(AudioManager.MODE_NORMAL);
|
||||||
audioManager.setSpeakerphoneOn(false);
|
audioManager.setSpeakerphoneOn(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ToneManager getInstance(final Context context) {
|
||||||
|
if (INSTANCE != null) {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
synchronized (ToneManager.class) {
|
||||||
|
if (INSTANCE != null) {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
INSTANCE = new ToneManager(context);
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private enum ToneState {
|
private enum ToneState {
|
||||||
NULL, RINGING, CONNECTED, BUSY, ENDING_CALL
|
NULL,
|
||||||
|
RINGING,
|
||||||
|
CONNECTED,
|
||||||
|
BUSY,
|
||||||
|
ENDING_CALL
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,23 +1,18 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.common.base.CaseFormat;
|
import com.google.common.base.CaseFormat;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import org.webrtc.MediaStreamTrack;
|
import org.webrtc.MediaStreamTrack;
|
||||||
import org.webrtc.PeerConnection;
|
import org.webrtc.PeerConnection;
|
||||||
import org.webrtc.RtpSender;
|
import org.webrtc.RtpSender;
|
||||||
import org.webrtc.RtpTransceiver;
|
import org.webrtc.RtpTransceiver;
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
|
||||||
|
|
||||||
class TrackWrapper<T extends MediaStreamTrack> {
|
class TrackWrapper<T extends MediaStreamTrack> {
|
||||||
public final T track;
|
public final T track;
|
||||||
public final RtpSender rtpSender;
|
public final RtpSender rtpSender;
|
||||||
|
@ -43,7 +38,9 @@ class TrackWrapper<T extends MediaStreamTrack> {
|
||||||
final RtpTransceiver transceiver =
|
final RtpTransceiver transceiver =
|
||||||
peerConnection == null ? null : getTransceiver(peerConnection, trackWrapper);
|
peerConnection == null ? null : getTransceiver(peerConnection, trackWrapper);
|
||||||
if (transceiver == null) {
|
if (transceiver == null) {
|
||||||
Log.w(Config.LOGTAG, "unable to detect transceiver for " + trackWrapper.rtpSender.id());
|
Log.w(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"unable to detect transceiver for " + trackWrapper.getRtpSenderId());
|
||||||
return Optional.of(trackWrapper.track);
|
return Optional.of(trackWrapper.track);
|
||||||
}
|
}
|
||||||
final RtpTransceiver.RtpTransceiverDirection direction = transceiver.getDirection();
|
final RtpTransceiver.RtpTransceiverDirection direction = transceiver.getDirection();
|
||||||
|
@ -56,11 +53,22 @@ class TrackWrapper<T extends MediaStreamTrack> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRtpSenderId() {
|
||||||
|
try {
|
||||||
|
return track.id();
|
||||||
|
} catch (final IllegalStateException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static <T extends MediaStreamTrack> RtpTransceiver getTransceiver(
|
public static <T extends MediaStreamTrack> RtpTransceiver getTransceiver(
|
||||||
@Nonnull final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) {
|
@Nonnull final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) {
|
||||||
final RtpSender rtpSender = trackWrapper.rtpSender;
|
final String rtpSenderId = trackWrapper.getRtpSenderId();
|
||||||
|
if (rtpSenderId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) {
|
for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) {
|
||||||
if (transceiver.getSender().id().equals(rtpSender.id())) {
|
if (transceiver.getSender().id().equals(rtpSenderId)) {
|
||||||
return transceiver;
|
return transceiver;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,12 +2,15 @@ package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import org.webrtc.Camera2Enumerator;
|
import org.webrtc.Camera2Enumerator;
|
||||||
import org.webrtc.CameraEnumerationAndroid;
|
import org.webrtc.CameraEnumerationAndroid;
|
||||||
import org.webrtc.CameraEnumerator;
|
import org.webrtc.CameraEnumerator;
|
||||||
|
@ -17,14 +20,6 @@ import org.webrtc.PeerConnectionFactory;
|
||||||
import org.webrtc.SurfaceTextureHelper;
|
import org.webrtc.SurfaceTextureHelper;
|
||||||
import org.webrtc.VideoSource;
|
import org.webrtc.VideoSource;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
|
||||||
|
|
||||||
class VideoSourceWrapper {
|
class VideoSourceWrapper {
|
||||||
|
|
||||||
private static final int CAPTURING_RESOLUTION = 1920;
|
private static final int CAPTURING_RESOLUTION = 1920;
|
|
@ -5,7 +5,6 @@ import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
@ -13,7 +12,17 @@ import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.services.AppRTCAudioManager;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import org.webrtc.AudioSource;
|
import org.webrtc.AudioSource;
|
||||||
import org.webrtc.AudioTrack;
|
import org.webrtc.AudioTrack;
|
||||||
import org.webrtc.CandidatePairChangeEvent;
|
import org.webrtc.CandidatePairChangeEvent;
|
||||||
|
@ -35,21 +44,6 @@ import org.webrtc.VideoTrack;
|
||||||
import org.webrtc.audio.JavaAudioDeviceModule;
|
import org.webrtc.audio.JavaAudioDeviceModule;
|
||||||
import org.webrtc.voiceengine.WebRtcAudioEffects;
|
import org.webrtc.voiceengine.WebRtcAudioEffects;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
|
||||||
import eu.siacs.conversations.services.AppRTCAudioManager;
|
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
|
||||||
|
|
||||||
@SuppressWarnings("UnstableApiUsage")
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
public class WebRTCWrapper {
|
public class WebRTCWrapper {
|
||||||
|
|
||||||
|
@ -206,7 +200,6 @@ public class WebRTCWrapper {
|
||||||
@Nullable private PeerConnectionFactory peerConnectionFactory = null;
|
@Nullable private PeerConnectionFactory peerConnectionFactory = null;
|
||||||
@Nullable private PeerConnection peerConnection = null;
|
@Nullable private PeerConnection peerConnection = null;
|
||||||
private AppRTCAudioManager appRTCAudioManager = null;
|
private AppRTCAudioManager appRTCAudioManager = null;
|
||||||
private ToneManager toneManager = null;
|
|
||||||
private Context context = null;
|
private Context context = null;
|
||||||
private EglBase eglBase = null;
|
private EglBase eglBase = null;
|
||||||
private VideoSourceWrapper videoSourceWrapper;
|
private VideoSourceWrapper videoSourceWrapper;
|
||||||
|
@ -223,8 +216,16 @@ public class WebRTCWrapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void dispose(final VideoTrack videoTrack) {
|
||||||
|
try {
|
||||||
|
videoTrack.dispose();
|
||||||
|
} catch (final IllegalStateException e) {
|
||||||
|
Log.e(Config.LOGTAG, "unable to dispose of video track", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setup(
|
public void setup(
|
||||||
final XmppConnectionService service,
|
final Context service,
|
||||||
@Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
|
@Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
|
||||||
throws InitializationException {
|
throws InitializationException {
|
||||||
try {
|
try {
|
||||||
|
@ -241,11 +242,10 @@ public class WebRTCWrapper {
|
||||||
throw new InitializationException("Unable to create EGL base", e);
|
throw new InitializationException("Unable to create EGL base", e);
|
||||||
}
|
}
|
||||||
this.context = service;
|
this.context = service;
|
||||||
this.toneManager = service.getJingleConnectionManager().toneManager;
|
|
||||||
mainHandler.post(
|
mainHandler.post(
|
||||||
() -> {
|
() -> {
|
||||||
appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
|
appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
|
||||||
toneManager.setAppRtcAudioManagerHasControl(true);
|
ToneManager.getInstance(context).setAppRtcAudioManagerHasControl(true);
|
||||||
appRTCAudioManager.start(audioManagerEvents);
|
appRTCAudioManager.start(audioManagerEvents);
|
||||||
eventCallback.onAudioDeviceChanged(
|
eventCallback.onAudioDeviceChanged(
|
||||||
appRTCAudioManager.getSelectedAudioDevice(),
|
appRTCAudioManager.getSelectedAudioDevice(),
|
||||||
|
@ -449,15 +449,19 @@ public class WebRTCWrapper {
|
||||||
final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
|
final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
|
||||||
final AppRTCAudioManager audioManager = this.appRTCAudioManager;
|
final AppRTCAudioManager audioManager = this.appRTCAudioManager;
|
||||||
final EglBase eglBase = this.eglBase;
|
final EglBase eglBase = this.eglBase;
|
||||||
|
final var localVideoTrack = this.localVideoTrack;
|
||||||
if (peerConnection != null) {
|
if (peerConnection != null) {
|
||||||
this.peerConnection = null;
|
this.peerConnection = null;
|
||||||
dispose(peerConnection);
|
dispose(peerConnection);
|
||||||
}
|
}
|
||||||
if (audioManager != null) {
|
if (audioManager != null) {
|
||||||
toneManager.setAppRtcAudioManagerHasControl(false);
|
ToneManager.getInstance(context).setAppRtcAudioManagerHasControl(false);
|
||||||
mainHandler.post(audioManager::stop);
|
mainHandler.post(audioManager::stop);
|
||||||
}
|
}
|
||||||
this.localVideoTrack = null;
|
if (localVideoTrack != null) {
|
||||||
|
this.localVideoTrack = null;
|
||||||
|
dispose(localVideoTrack.track);
|
||||||
|
}
|
||||||
this.remoteVideoTrack = null;
|
this.remoteVideoTrack = null;
|
||||||
if (videoSourceWrapper != null) {
|
if (videoSourceWrapper != null) {
|
||||||
this.videoSourceWrapper = null;
|
this.videoSourceWrapper = null;
|
||||||
|
@ -469,8 +473,8 @@ public class WebRTCWrapper {
|
||||||
videoSourceWrapper.dispose();
|
videoSourceWrapper.dispose();
|
||||||
}
|
}
|
||||||
if (eglBase != null) {
|
if (eglBase != null) {
|
||||||
eglBase.release();
|
|
||||||
this.eglBase = null;
|
this.eglBase = null;
|
||||||
|
eglBase.release();
|
||||||
}
|
}
|
||||||
if (peerConnectionFactory != null) {
|
if (peerConnectionFactory != null) {
|
||||||
this.peerConnectionFactory = null;
|
this.peerConnectionFactory = null;
|
|
@ -1,20 +1,16 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
||||||
|
import im.conversations.android.xml.Element;
|
||||||
|
import im.conversations.android.xml.Namespace;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
|
||||||
import eu.siacs.conversations.xml.Element;
|
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
|
||||||
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
|
||||||
|
|
||||||
public class Content extends Element {
|
public class Content extends Element {
|
||||||
|
|
||||||
public Content(final Creator creator, final Senders senders, final String name) {
|
public Content(final Creator creator, final Senders senders, final String name) {
|
||||||
|
@ -99,7 +95,6 @@ public class Content extends Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void setTransport(GenericTransportInfo transportInfo) {
|
public void setTransport(GenericTransportInfo transportInfo) {
|
||||||
this.addChild(transportInfo);
|
this.addChild(transportInfo);
|
||||||
}
|
}
|
||||||
|
@ -140,7 +135,7 @@ public class Content extends Element {
|
||||||
} else if (attributes.contains("recvonly")) {
|
} else if (attributes.contains("recvonly")) {
|
||||||
return initiator ? RESPONDER : INITIATOR;
|
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
|
// If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is
|
||||||
// present, "sendrecv" SHOULD be assumed as the default
|
// present, "sendrecv" SHOULD be assumed as the default
|
||||||
// https://www.rfc-editor.org/rfc/rfc4566
|
// https://www.rfc-editor.org/rfc/rfc4566
|
|
@ -1,22 +1,14 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import im.conversations.android.xml.Element;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
|
|
||||||
import eu.siacs.conversations.entities.DownloadableFile;
|
|
||||||
import eu.siacs.conversations.xml.Element;
|
|
||||||
|
|
||||||
public class FileTransferDescription extends GenericDescription {
|
public class FileTransferDescription extends GenericDescription {
|
||||||
|
|
||||||
public static List<String> NAMESPACES = Arrays.asList(
|
public static List<String> NAMESPACES =
|
||||||
Version.FT_3.namespace,
|
Arrays.asList(Version.FT_3.namespace, Version.FT_4.namespace, Version.FT_5.namespace);
|
||||||
Version.FT_4.namespace,
|
|
||||||
Version.FT_5.namespace
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
private FileTransferDescription(String name, String namespace) {
|
private FileTransferDescription(String name, String namespace) {
|
||||||
super(name, namespace);
|
super(name, namespace);
|
||||||
|
@ -45,27 +37,15 @@ public class FileTransferDescription extends GenericDescription {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FileTransferDescription of(DownloadableFile file, Version version, XmppAxolotlMessage axolotlMessage) {
|
|
||||||
final FileTransferDescription description = new FileTransferDescription("description", version.getNamespace());
|
|
||||||
final Element fileElement;
|
|
||||||
if (version == Version.FT_3) {
|
|
||||||
Element offer = description.addChild("offer");
|
|
||||||
fileElement = offer.addChild("file");
|
|
||||||
} else {
|
|
||||||
fileElement = description.addChild("file");
|
|
||||||
}
|
|
||||||
fileElement.addChild("size").setContent(Long.toString(file.getExpectedSize()));
|
|
||||||
fileElement.addChild("name").setContent(file.getName());
|
|
||||||
if (axolotlMessage != null) {
|
|
||||||
fileElement.addChild(axolotlMessage.toElement());
|
|
||||||
}
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FileTransferDescription upgrade(final Element element) {
|
public static FileTransferDescription upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description");
|
Preconditions.checkArgument(
|
||||||
Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace");
|
"description".equals(element.getName()),
|
||||||
final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace());
|
"Name of provided element is not description");
|
||||||
|
Preconditions.checkArgument(
|
||||||
|
NAMESPACES.contains(element.getNamespace()),
|
||||||
|
"Element does not match a file transfer namespace");
|
||||||
|
final FileTransferDescription description =
|
||||||
|
new FileTransferDescription("description", element.getNamespace());
|
||||||
description.setAttributes(element.getAttributes());
|
description.setAttributes(element.getAttributes());
|
||||||
description.setChildren(element.getChildren());
|
description.setChildren(element.getChildren());
|
||||||
return description;
|
return description;
|
|
@ -1,8 +1,7 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import im.conversations.android.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Element;
|
|
||||||
|
|
||||||
public class GenericDescription extends Element {
|
public class GenericDescription extends Element {
|
||||||
|
|
||||||
|
@ -12,7 +11,8 @@ public class GenericDescription extends Element {
|
||||||
|
|
||||||
public static GenericDescription upgrade(final Element element) {
|
public static GenericDescription upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("description".equals(element.getName()));
|
Preconditions.checkArgument("description".equals(element.getName()));
|
||||||
final GenericDescription description = new GenericDescription("description", element.getNamespace());
|
final GenericDescription description =
|
||||||
|
new GenericDescription("description", element.getNamespace());
|
||||||
description.setAttributes(element.getAttributes());
|
description.setAttributes(element.getAttributes());
|
||||||
description.setChildren(element.getChildren());
|
description.setChildren(element.getChildren());
|
||||||
return description;
|
return description;
|
|
@ -1,8 +1,7 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import im.conversations.android.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Element;
|
|
||||||
|
|
||||||
public class GenericTransportInfo extends Element {
|
public class GenericTransportInfo extends Element {
|
||||||
|
|
||||||
|
@ -12,7 +11,8 @@ public class GenericTransportInfo extends Element {
|
||||||
|
|
||||||
public static GenericTransportInfo upgrade(final Element element) {
|
public static GenericTransportInfo upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("transport".equals(element.getName()));
|
Preconditions.checkArgument("transport".equals(element.getName()));
|
||||||
final GenericTransportInfo transport = new GenericTransportInfo("transport", element.getNamespace());
|
final GenericTransportInfo transport =
|
||||||
|
new GenericTransportInfo("transport", element.getNamespace());
|
||||||
transport.setAttributes(element.getAttributes());
|
transport.setAttributes(element.getAttributes());
|
||||||
transport.setChildren(element.getChildren());
|
transport.setChildren(element.getChildren());
|
||||||
return transport;
|
return transport;
|
|
@ -2,13 +2,11 @@ package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import im.conversations.android.xml.Element;
|
||||||
|
import im.conversations.android.xml.Namespace;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import eu.siacs.conversations.xml.Element;
|
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
|
||||||
|
|
||||||
public class Group extends Element {
|
public class Group extends Element {
|
||||||
|
|
||||||
private Group() {
|
private Group() {
|
||||||
|
@ -45,10 +43,10 @@ public class Group extends Element {
|
||||||
final String[] parts = input.split(" ");
|
final String[] parts = input.split(" ");
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
final String semantics = parts[0];
|
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]);
|
tagBuilder.add(parts[i]);
|
||||||
}
|
}
|
||||||
return new Group(semantics,tagBuilder.build());
|
return new Group(semantics, tagBuilder.build());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
|
@ -1,9 +1,8 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import im.conversations.android.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Element;
|
import im.conversations.android.xml.Namespace;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
|
||||||
|
|
||||||
public class IbbTransportInfo extends GenericTransportInfo {
|
public class IbbTransportInfo extends GenericTransportInfo {
|
||||||
|
|
||||||
|
@ -36,9 +35,13 @@ public class IbbTransportInfo extends GenericTransportInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IbbTransportInfo upgrade(final Element element) {
|
public static IbbTransportInfo upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport");
|
Preconditions.checkArgument(
|
||||||
Preconditions.checkArgument(Namespace.JINGLE_TRANSPORTS_IBB.equals(element.getNamespace()), "Element does not match ibb transport namespace");
|
"transport".equals(element.getName()), "Name of provided element is not transport");
|
||||||
final IbbTransportInfo transportInfo = new IbbTransportInfo("transport", Namespace.JINGLE_TRANSPORTS_IBB);
|
Preconditions.checkArgument(
|
||||||
|
Namespace.JINGLE_TRANSPORTS_IBB.equals(element.getNamespace()),
|
||||||
|
"Element does not match ibb transport namespace");
|
||||||
|
final IbbTransportInfo transportInfo =
|
||||||
|
new IbbTransportInfo("transport", Namespace.JINGLE_TRANSPORTS_IBB);
|
||||||
transportInfo.setAttributes(element.getAttributes());
|
transportInfo.setAttributes(element.getAttributes());
|
||||||
transportInfo.setChildren(element.getChildren());
|
transportInfo.setChildren(element.getChildren());
|
||||||
return transportInfo;
|
return transportInfo;
|
|
@ -1,7 +1,6 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.base.MoreObjects;
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
|
@ -11,7 +10,9 @@ import com.google.common.collect.ArrayListMultimap;
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
||||||
|
import im.conversations.android.xml.Element;
|
||||||
|
import im.conversations.android.xml.Namespace;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Hashtable;
|
import java.util.Hashtable;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
@ -20,10 +21,6 @@ import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import eu.siacs.conversations.xml.Element;
|
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
|
||||||
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
|
||||||
|
|
||||||
public class IceUdpTransportInfo extends GenericTransportInfo {
|
public class IceUdpTransportInfo extends GenericTransportInfo {
|
||||||
|
|
||||||
public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo();
|
public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo();
|
||||||
|
@ -63,7 +60,10 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IceUdpTransportInfo of(
|
public static IceUdpTransportInfo of(
|
||||||
final Credentials credentials, final Setup setup, final String hash, final String fingerprint) {
|
final Credentials credentials,
|
||||||
|
final Setup setup,
|
||||||
|
final String hash,
|
||||||
|
final String fingerprint) {
|
||||||
final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
|
final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
|
||||||
iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint));
|
iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint));
|
||||||
iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag);
|
iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag);
|