Merge pull request 'master' (#18) from Arne/monocles_chat:master into master

Reviewed-on: https://codeberg.org/Pirujo/monocles_chat_translate/pulls/18
This commit is contained in:
Pirujo 2023-01-06 08:54:44 +00:00
commit 342ddbee6e
42 changed files with 1314 additions and 208 deletions

View file

@ -35,7 +35,7 @@ configurations {
}
dependencies {
playstoreImplementation('com.google.firebase:firebase-messaging:23.1.0') {
playstoreImplementation('com.google.firebase:firebase-messaging:23.1.1') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@ -46,7 +46,8 @@ dependencies {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'exifinterface'
}
implementation 'ch.threema:webrtc-android:100.0.0'
implementation 'im.conversations.webrtc:webrtc-android:104.0.0'
//implementation 'org.snikket:webrtc-android:107.0.0'
implementation 'org.jitsi:org.otr4j:0.23'
implementation 'org.bouncycastle:bcmail-jdk15on:1.64'
implementation 'org.gnu.inet:libidn:1.15'
@ -94,7 +95,6 @@ dependencies {
}
ext {
travisBuild = System.getenv("TRAVIS") == "true"
preDexEnabled = System.getProperty("pre-dex", "true")
}
@ -108,8 +108,8 @@ android {
targetSdkVersion 32
//versionNameSuffix " beta_(2022-11-29)" // " beta_(XXXX-XX-XX)" // activate for beta versions
versionCode 125
versionName "1.5.14"
versionCode 126
versionName "1.5.15"
//resConfigs "en"
archivesBaseName += "-$versionName"

View file

@ -0,0 +1,16 @@
* Verbesserung des SASL-Mechanismus
* Langes Drücken zum Kopieren eines Links in einer Nachricht erlauben
* Standardeinstellung für automatisches Wiederversenden auf 0 setzen
* Material you entfernen und auf runde Buttons zurücksetzen
* Provider-Url entfernen
* Benutzernamen in Gruppenchats größer machen
* Runde Avatare
* Datenbank-Upgrade
* Webrtc Verbesserungen
* SCRAM Verbesserungen
* Bugfixes
* Verbesserte russische Zeichenkette
* JingleRtpConnection für Content-Add vorbereiten
* Schalter/Video-Menüpunkt zum Telefonieren hinzufügen
* Handhabung von Jingle-Strophen für das Hinzufügen von Inhalten
* XMPP-Adresse nach Benutzereingabe abschneiden

View file

@ -0,0 +1,2 @@
* Ändere Farbe der Topbar zu Themefarbe
* Kritische Datenbank Fehler repariert für den Updateprozess

View file

@ -0,0 +1,16 @@
* improving SASL Mechanism
* allow long press to copy any link in a message
* set autoresend default to 0
* remove Material you and reset to round buttons
* remove providers url
* make username more bigger in groupchats
* round avatars
* database upgrade
* webrtc improvements
* scram improvements
* bugfixes
* improved russian string
* prepare JingleRtpConnection for content-adds
* add switch to video menu item to call
* handle content-add jingle stanzas
* trim xmpp address after user input

View file

@ -0,0 +1,2 @@
* change color to theme of topbar
* critical database bugfix for updateprocess

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="logo_actionbar_white_background">#3DDC84</color>
</resources>

View file

@ -79,6 +79,9 @@
<intent>
<action android:name="android.intent.action.VIEW" />
</intent>
<intent>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
</intent>
</queries>
@ -128,6 +131,23 @@
android:scheme="package" />
</intent-filter>
</receiver>
<receiver
android:name=".services.UnifiedPushDistributor"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
<action android:name="org.unifiedpush.android.distributor.UNREGISTER" />
<action android:name="org.unifiedpush.android.distributor.feature.BYTES_MESSAGE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<data android:scheme="package"/>
</intent-filter>
</receiver>
<receiver android:name=".services.AlarmReceiver" />
<activity

View file

@ -1,7 +1,6 @@
package eu.siacs.conversations.crypto.sasl;
import android.util.Base64;
import android.util.Log;
import com.google.common.base.CaseFormat;
import com.google.common.base.Objects;
@ -13,14 +12,32 @@ import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.util.concurrent.ExecutionException;
import javax.crypto.SecretKey;
import javax.net.ssl.SSLSocket;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper;
abstract class ScramMechanism extends SaslMechanism {
public static final SecretKey EMPTY_KEY =
new SecretKey() {
@Override
public String getAlgorithm() {
return "HMAC";
}
@Override
public String getFormat() {
return "RAW";
}
@Override
public byte[] getEncoded() {
return new byte[0];
}
};
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
private static final Cache<CacheKey, KeyPair> CACHE =

View file

@ -15,7 +15,9 @@ public class ScramSha1 extends ScramMechanism {
@Override
protected HashFunction getHMac(final byte[] key) {
return Hashing.hmacSha1(key);
return (key == null || key.length == 0)
? Hashing.hmacSha1(EMPTY_KEY)
: Hashing.hmacSha1(key);
}
@Override

View file

@ -15,7 +15,9 @@ public class ScramSha1Plus extends ScramPlusMechanism {
@Override
protected HashFunction getHMac(final byte[] key) {
return Hashing.hmacSha1(key);
return (key == null || key.length == 0)
? Hashing.hmacSha1(EMPTY_KEY)
: Hashing.hmacSha1(key);
}
@Override

View file

@ -16,7 +16,9 @@ public class ScramSha256 extends ScramMechanism {
@Override
protected HashFunction getHMac(final byte[] key) {
return Hashing.hmacSha256(key);
return (key == null || key.length == 0)
? Hashing.hmacSha256(EMPTY_KEY)
: Hashing.hmacSha256(key);
}
@Override

View file

@ -15,7 +15,9 @@ public class ScramSha256Plus extends ScramPlusMechanism {
@Override
protected HashFunction getHMac(final byte[] key) {
return Hashing.hmacSha256(key);
return (key == null || key.length == 0)
? Hashing.hmacSha256(EMPTY_KEY)
: Hashing.hmacSha256(key);
}
@Override

View file

@ -19,7 +19,9 @@ public class ScramSha512 extends ScramMechanism {
@Override
protected HashFunction getHMac(final byte[] key) {
return Hashing.hmacSha512(key);
return (key == null || key.length == 0)
? Hashing.hmacSha512(EMPTY_KEY)
: Hashing.hmacSha512(key);
}
@Override

View file

@ -15,7 +15,9 @@ public class ScramSha512Plus extends ScramPlusMechanism {
@Override
protected HashFunction getHMac(final byte[] key) {
return Hashing.hmacSha512(key);
return (key == null || key.length == 0)
? Hashing.hmacSha512(EMPTY_KEY)
: Hashing.hmacSha512(key);
}
@Override

View file

@ -233,14 +233,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
this.displayName = displayName;
}
public XmppConnection.Identity getServerIdentity() {
if (xmppConnection == null) {
return XmppConnection.Identity.UNKNOWN;
} else {
return xmppConnection.getServerIdentity();
}
}
public Contact getSelfContact() {
return getRoster().getContact(jid);
}

View file

@ -5,6 +5,7 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Date;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@ -84,6 +85,36 @@ public abstract class AbstractParser {
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
return Math.min(dateFormat.parse(timestamp).getTime() + ms, System.currentTimeMillis());
}
public static long getTimestamp(final String input) throws ParseException {
if (input == null) {
throw new IllegalArgumentException("timestamp should not be null");
}
final String timestamp = input.replace("Z", "+0000");
final SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
final long milliseconds = getMilliseconds(timestamp);
final String formatted =
timestamp.substring(0, 19) + timestamp.substring(timestamp.length() - 5);
final Date date = simpleDateFormat.parse(formatted);
if (date == null) {
throw new IllegalArgumentException("Date was null");
}
return date.getTime() + milliseconds;
}
private static long getMilliseconds(final String timestamp) {
if (timestamp.length() >= 25 && timestamp.charAt(19) == '.') {
final String millis = timestamp.substring(19, timestamp.length() - 5);
try {
double fractions = Double.parseDouble("0" + millis);
return Math.round(1000 * fractions);
} catch (NumberFormatException e) {
return 0;
}
} else {
return 0;
}
}
protected void updateLastseen(final Account account, final Jid from) {
final Contact contact = account.getRoster().getContact(from);

View file

@ -452,6 +452,24 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
}
mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) && packet.getType() == IqPacket.TYPE.SET) {
final Jid transport = packet.getFrom();
final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH);
final boolean success =
push != null
&& mXmppConnectionService.processUnifiedPushMessage(
account, transport, push);
final IqPacket response;
if (success) {
response = packet.generateResponse(IqPacket.TYPE.RESULT);
} else {
response = packet.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error");
error.setAttribute("type", "cancel");
error.setAttribute("code", "404");
error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
}
mXmppConnectionService.sendIqPacket(account, response, null);
} else {
if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);

View file

@ -67,7 +67,7 @@ import eu.siacs.conversations.xmpp.mam.MamReference;
public class DatabaseBackend extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "history";
public static final int DATABASE_VERSION = 58; // = Conversations DATABASE_VERSION + 7
public static final int DATABASE_VERSION = 59; // = Conversations DATABASE_VERSION + 7
private static boolean requiresMessageIndexRebuild = false;
private static DatabaseBackend instance = null;
private static final List<String> DB_PRAGMAS = Collections.unmodifiableList(Arrays.asList(
@ -682,7 +682,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_MECHANISM + " TEXT");
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_CHANNEL_BINDING + " TEXT");
}
if (oldVersion < 51 && newVersion >= 51) {
if (oldVersion < 59 && newVersion >= 59) {
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_MECHANISM + " TEXT");
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_TOKEN + " TEXT");
}

View file

@ -0,0 +1,246 @@
package eu.siacs.conversations.persistance;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import androidx.annotation.Nullable;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import eu.siacs.conversations.Config;
public class UnifiedPushDatabase extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "unified-push-distributor";
private static final int DATABASE_VERSION = 1;
private static UnifiedPushDatabase instance;
public static UnifiedPushDatabase getInstance(final Context context) {
synchronized (UnifiedPushDatabase.class) {
if (instance == null) {
instance = new UnifiedPushDatabase(context.getApplicationContext());
}
return instance;
}
}
private UnifiedPushDatabase(@Nullable Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(final SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(
"CREATE TABLE push (account TEXT, transport TEXT, application TEXT NOT NULL, instance TEXT NOT NULL UNIQUE, endpoint TEXT, expiration NUMBER DEFAULT 0)");
}
public boolean register(final String application, final String instance) {
final SQLiteDatabase sqLiteDatabase = getWritableDatabase();
sqLiteDatabase.beginTransaction();
final Optional<String> existingApplication;
try (final Cursor cursor =
sqLiteDatabase.query(
"push",
new String[] {"application"},
"instance=?",
new String[] {instance},
null,
null,
null)) {
if (cursor != null && cursor.moveToFirst()) {
existingApplication = Optional.of(cursor.getString(0));
} else {
existingApplication = Optional.absent();
}
}
if (existingApplication.isPresent()) {
sqLiteDatabase.setTransactionSuccessful();
sqLiteDatabase.endTransaction();
return application.equals(existingApplication.get());
}
final ContentValues contentValues = new ContentValues();
contentValues.put("application", application);
contentValues.put("instance", instance);
contentValues.put("expiration", 0);
final long inserted = sqLiteDatabase.insert("push", null, contentValues);
if (inserted > 0) {
Log.d(Config.LOGTAG, "inserted new application/instance tuple into unified push db");
}
sqLiteDatabase.setTransactionSuccessful();
sqLiteDatabase.endTransaction();
return true;
}
public List<PushTarget> getRenewals(final String account, final String transport) {
final ImmutableList.Builder<PushTarget> renewalBuilder = ImmutableList.builder();
// TODO use a date somewhat in the future to account for period renewal triggers
final long expiration = System.currentTimeMillis();
final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
try (final Cursor cursor =
sqLiteDatabase.query(
"push",
new String[] {"application", "instance"},
"account <> ? OR transport <> ? OR expiration < " + expiration,
new String[] {account, transport},
null,
null,
null)) {
while (cursor != null && cursor.moveToNext()) {
renewalBuilder.add(
new PushTarget(
cursor.getString(cursor.getColumnIndexOrThrow("application")),
cursor.getString(cursor.getColumnIndexOrThrow("instance"))));
}
}
return renewalBuilder.build();
}
public ApplicationEndpoint getEndpoint(
final String account, final String transport, final String instance) {
final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
try (final Cursor cursor =
sqLiteDatabase.query(
"push",
new String[] {"application", "endpoint"},
"account = ? AND transport = ? AND instance = ? AND endpoint IS NOT NULL AND expiration >= "
+ System.currentTimeMillis(),
new String[] {account, transport, instance},
null,
null,
null)) {
if (cursor != null && cursor.moveToFirst()) {
return new ApplicationEndpoint(
cursor.getString(cursor.getColumnIndexOrThrow("application")),
cursor.getString(cursor.getColumnIndexOrThrow("endpoint")));
}
}
return null;
}
@Override
public void onUpgrade(
final SQLiteDatabase sqLiteDatabase, final int oldVersion, final int newVersion) {}
public boolean updateEndpoint(
final String instance,
final String account,
final String transport,
final String endpoint,
final long expiration) {
final SQLiteDatabase sqLiteDatabase = getWritableDatabase();
sqLiteDatabase.beginTransaction();
final String existingEndpoint;
try (final Cursor cursor =
sqLiteDatabase.query(
"push",
new String[] {"endpoint"},
"instance=?",
new String[] {instance},
null,
null,
null)) {
if (cursor != null && cursor.moveToFirst()) {
existingEndpoint = cursor.getString(0);
} else {
existingEndpoint = null;
}
}
final ContentValues contentValues = new ContentValues();
contentValues.put("account", account);
contentValues.put("transport", transport);
contentValues.put("endpoint", endpoint);
contentValues.put("expiration", expiration);
sqLiteDatabase.update("push", contentValues, "instance=?", new String[] {instance});
sqLiteDatabase.setTransactionSuccessful();
sqLiteDatabase.endTransaction();
return !endpoint.equals(existingEndpoint);
}
public List<PushTarget> getPushTargets(final String account, final String transport) {
final ImmutableList.Builder<PushTarget> renewalBuilder = ImmutableList.builder();
final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
try (final Cursor cursor =
sqLiteDatabase.query(
"push",
new String[] {"application", "instance"},
"account = ?",
new String[] {account},
null,
null,
null)) {
while (cursor != null && cursor.moveToNext()) {
renewalBuilder.add(
new PushTarget(
cursor.getString(cursor.getColumnIndexOrThrow("application")),
cursor.getString(cursor.getColumnIndexOrThrow("instance"))));
}
}
return renewalBuilder.build();
}
public boolean deleteInstance(final String instance) {
final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
final int rows = sqLiteDatabase.delete("push", "instance=?", new String[] {instance});
return rows >= 1;
}
public boolean deleteApplication(final String application) {
final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
final int rows = sqLiteDatabase.delete("push", "application=?", new String[] {application});
return rows >= 1;
}
public static class ApplicationEndpoint {
public final String application;
public final String endpoint;
public ApplicationEndpoint(String application, String endpoint) {
this.application = application;
this.endpoint = endpoint;
}
}
public static class PushTarget {
public final String application;
public final String instance;
public PushTarget(final String application, final String instance) {
this.application = application;
this.instance = instance;
}
@NotNull
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("application", application)
.add("instance", instance)
.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PushTarget that = (PushTarget) o;
return Objects.equal(application, that.application)
&& Objects.equal(instance, that.instance);
}
@Override
public int hashCode() {
return Objects.hashCode(application, instance);
}
}
}

View file

@ -1,5 +1,6 @@
package eu.siacs.conversations.services;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Context;
@ -12,19 +13,23 @@ import android.os.Bundle;
import android.os.IBinder;
import android.service.chooser.ChooserTarget;
import android.service.chooser.ChooserTargetService;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.utils.Compatibility;
@SuppressLint("Deprecated")
@TargetApi(Build.VERSION_CODES.M)
public class ContactChooserTargetService extends ChooserTargetService implements ServiceConnection {
private final Object lock = new Object();
private final int MAX_TARGETS = 5;
private static final int MAX_TARGETS = 5;
private XmppConnectionService mXmppConnectionService;
private static boolean textOnly(IntentFilter filter) {
@ -37,10 +42,10 @@ public class ContactChooserTargetService extends ChooserTargetService implements
}
@Override
public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) {
final ArrayList<ChooserTarget> chooserTargets = new ArrayList<>();
public List<ChooserTarget> onGetChooserTargets(
final ComponentName targetActivityName, final IntentFilter matchedFilter) {
if (!EventReceiver.hasEnabledAccounts(this)) {
return chooserTargets;
return Collections.emptyList();
}
final Intent intent = new Intent(this, XmppConnectionService.class);
intent.setAction("contact_chooser");
@ -48,37 +53,48 @@ public class ContactChooserTargetService extends ChooserTargetService implements
bindService(intent, this, Context.BIND_AUTO_CREATE);
try {
waitForService();
final ArrayList<Conversation> conversations = new ArrayList<>();
if (!mXmppConnectionService.areMessagesInitialized()) {
return chooserTargets;
return Collections.emptyList();
}
mXmppConnectionService.populateWithOrderedConversations(conversations, textOnly(matchedFilter));
final ComponentName componentName = new ComponentName(this, ConversationsActivity.class);
final ArrayList<Conversation> conversations = new ArrayList<>();
mXmppConnectionService.populateWithOrderedConversations(
conversations, textOnly(matchedFilter));
final ComponentName componentName =
new ComponentName(this, ConversationsActivity.class);
final int pixel = AvatarService.getSystemUiAvatarSize(this);
for (Conversation conversation : conversations) {
final ArrayList<ChooserTarget> chooserTargets = new ArrayList<>();
for (final Conversation conversation : conversations) {
if (conversation.sentMessagesCount() == 0) {
continue;
}
final String name = conversation.getName().toString();
final Icon icon = Icon.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation, pixel));
final Icon icon =
Icon.createWithBitmap(
mXmppConnectionService.getAvatarService().get(conversation, pixel));
final float score = 1 - (1.0f / MAX_TARGETS) * chooserTargets.size();
final Bundle extras = new Bundle();
extras.putString(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
chooserTargets.add(new ChooserTarget(name, icon, score, componentName, extras));
if (chooserTargets.size() >= MAX_TARGETS) {
break;
return chooserTargets;
}
}
} catch (InterruptedException e) {
return chooserTargets;
} catch (final InterruptedException e) {
Log.d(
Config.LOGTAG,
"Thread got interrupted before binding to XmppConnectionService",
e);
} finally {
unbindService(this);
}
unbindService(this);
return chooserTargets;
return Collections.emptyList();
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service;
public void onServiceConnected(final ComponentName name, final IBinder service) {
XmppConnectionService.XmppConnectionBinder binder =
(XmppConnectionService.XmppConnectionBinder) service;
mXmppConnectionService = binder.getService();
synchronized (this.lock) {
lock.notifyAll();

View file

@ -0,0 +1,295 @@
package eu.siacs.conversations.services;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.preference.PreferenceManager;
import android.util.Log;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.io.BaseEncoding;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.parser.AbstractParser;
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
public class UnifiedPushBroker {
private final XmppConnectionService service;
public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) {
this.service = xmppConnectionService;
}
public void renewUnifiedPushEndpointsOnBind(final Account account) {
final Optional<Transport> transport = getTransport();
if (transport.isPresent()) {
final Account transportAccount = transport.get().account;
if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": trigger endpoint renewal on bind");
renewUnifiedEndpoint(transport.get());
}
}
}
public Optional<Transport> renewUnifiedPushEndpoints() {
final Optional<Transport> transportOptional = getTransport();
if (transportOptional.isPresent()) {
final Transport transport = transportOptional.get();
if (transport.account.isEnabled()) {
renewUnifiedEndpoint(transportOptional.get());
} else {
Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled");
}
} else {
Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
}
return transportOptional;
}
private void renewUnifiedEndpoint(final Transport transport) {
final Account account = transport.account;
final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
final List<UnifiedPushDatabase.PushTarget> renewals =
unifiedPushDatabase.getRenewals(
account.getUuid(), transport.transport.toEscapedString());
for (final UnifiedPushDatabase.PushTarget renewal : renewals) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
final String hashedApplication =
UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
final String hashedInstance =
UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
final IqPacket registration = new IqPacket(IqPacket.TYPE.SET);
registration.setTo(transport.transport);
final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
register.setAttribute("application", hashedApplication);
register.setAttribute("instance", hashedInstance);
this.service.sendIqPacket(
account,
registration,
(a, response) -> processRegistration(transport, renewal, response));
}
}
private void processRegistration(
final Transport transport,
final UnifiedPushDatabase.PushTarget renewal,
final IqPacket response) {
if (response.getType() == IqPacket.TYPE.RESULT) {
final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
if (registered == null) {
return;
}
final String endpoint = registered.getAttribute("endpoint");
if (Strings.isNullOrEmpty(endpoint)) {
Log.w(Config.LOGTAG, "endpoint was null in up registration");
return;
}
final long expiration;
try {
expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration"));
} catch (final IllegalArgumentException | ParseException e) {
Log.d(Config.LOGTAG, "could not parse expiration", e);
return;
}
renewUnifiedPushEndpoint(transport, renewal, endpoint, expiration);
}
}
private void renewUnifiedPushEndpoint(
final Transport transport,
final UnifiedPushDatabase.PushTarget renewal,
final String endpoint,
final long expiration) {
Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
final boolean modified =
unifiedPushDatabase.updateEndpoint(
renewal.instance,
transport.account.getUuid(),
transport.transport.toEscapedString(),
endpoint,
expiration);
if (modified) {
Log.d(
Config.LOGTAG,
"endpoint for "
+ renewal.application
+ "/"
+ renewal.instance
+ " was updated to "
+ endpoint);
broadcastEndpoint(
renewal.instance,
new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint));
}
}
public boolean reconfigurePushDistributor() {
final boolean enabled = getTransport().isPresent();
setUnifiedPushDistributorEnabled(enabled);
return enabled;
}
private void setUnifiedPushDistributorEnabled(final boolean enabled) {
final PackageManager packageManager = service.getPackageManager();
final ComponentName componentName =
new ComponentName(service, UnifiedPushDistributor.class);
if (enabled) {
packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
} else {
packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
}
}
public boolean processPushMessage(
final Account account, final Jid transport, final Element push) {
final String instance = push.getAttribute("instance");
final String application = push.getAttribute("application");
if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
return false;
}
final String content = push.getContent();
final byte[] payload;
if (Strings.isNullOrEmpty(content)) {
payload = new byte[0];
} else if (BaseEncoding.base64().canDecode(content)) {
payload = BaseEncoding.base64().decode(content);
} else {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": received invalid unified push payload");
return false;
}
final Optional<UnifiedPushDatabase.PushTarget> pushTarget =
getPushTarget(account, transport, application, instance);
if (pushTarget.isPresent()) {
final UnifiedPushDatabase.PushTarget target = pushTarget.get();
// TODO check if app is still installed?
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": broadcasting a "
+ payload.length
+ " bytes push message to "
+ target.application);
broadcastPushMessage(target, payload);
return true;
} else {
Log.d(Config.LOGTAG, "could not find application for push");
return false;
}
}
public Optional<Transport> getTransport() {
final SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext());
final String accountPreference =
sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none");
final String pushServerPreference =
sharedPreferences.getString(
UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
service.getString(R.string.default_push_server));
if (Strings.isNullOrEmpty(accountPreference)
|| "none".equalsIgnoreCase(accountPreference)
|| Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) {
return Optional.absent();
}
final Jid transport;
final Jid jid;
try {
transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim());
jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim());
} catch (final IllegalArgumentException e) {
return Optional.absent();
}
final Account account = service.findAccountByJid(jid);
if (account == null) {
return Optional.absent();
}
return Optional.of(new Transport(account, transport));
}
private Optional<UnifiedPushDatabase.PushTarget> getPushTarget(
final Account account,
final Jid transport,
final String application,
final String instance) {
final String uuid = account.getUuid();
final List<UnifiedPushDatabase.PushTarget> pushTargets =
UnifiedPushDatabase.getInstance(service)
.getPushTargets(uuid, transport.toEscapedString());
return Iterables.tryFind(
pushTargets,
pt ->
UnifiedPushDistributor.hash(uuid, pt.application).equals(application)
&& UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance));
}
private void broadcastPushMessage(
final UnifiedPushDatabase.PushTarget target, final byte[] payload) {
final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE);
updateIntent.setPackage(target.application);
updateIntent.putExtra("token", target.instance);
updateIntent.putExtra("bytesMessage", payload);
updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
service.sendBroadcast(updateIntent);
}
private void broadcastEndpoint(
final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application);
final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
updateIntent.setPackage(endpoint.application);
updateIntent.putExtra("token", instance);
updateIntent.putExtra("endpoint", endpoint.endpoint);
service.sendBroadcast(updateIntent);
}
public void rebroadcastEndpoint(final String instance, final Transport transport) {
final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
final UnifiedPushDatabase.ApplicationEndpoint endpoint =
unifiedPushDatabase.getEndpoint(
transport.account.getUuid(),
transport.transport.toEscapedString(),
instance);
if (endpoint != null) {
broadcastEndpoint(instance, endpoint);
}
}
public static class Transport {
public final Account account;
public final Jid transport;
public Transport(Account account, Jid transport) {
this.account = account;
this.transport = transport;
}
}
}

View file

@ -0,0 +1,152 @@
package eu.siacs.conversations.services;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.util.Log;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
import eu.siacs.conversations.utils.Compatibility;
public class UnifiedPushDistributor extends BroadcastReceiver {
public static final String ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER";
public static final String ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER";
public static final String ACTION_BYTE_MESSAGE =
"org.unifiedpush.android.distributor.feature.BYTES_MESSAGE";
public static final String ACTION_REGISTRATION_FAILED =
"org.unifiedpush.android.connector.REGISTRATION_FAILED";
public static final String ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE";
public static final String ACTION_NEW_ENDPOINT =
"org.unifiedpush.android.connector.NEW_ENDPOINT";
public static final String PREFERENCE_ACCOUNT = "up_push_account";
public static final String PREFERENCE_PUSH_SERVER = "up_push_server";
public static final List<String> PREFERENCES =
Arrays.asList(PREFERENCE_ACCOUNT, PREFERENCE_PUSH_SERVER);
@Override
public void onReceive(final Context context, final Intent intent) {
if (intent == null) {
return;
}
final String action = intent.getAction();
final String application = intent.getStringExtra("application");
final String instance = intent.getStringExtra("token");
final List<String> features = intent.getStringArrayListExtra("features");
switch (Strings.nullToEmpty(action)) {
case ACTION_REGISTER:
register(context, application, instance, features);
break;
case ACTION_UNREGISTER:
unregister(context, instance);
break;
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
unregisterApplication(context, intent.getData());
break;
default:
Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action);
break;
}
}
private void register(
final Context context,
final String application,
final String instance,
final Collection<String> features) {
if (Strings.isNullOrEmpty(application) || Strings.isNullOrEmpty(instance)) {
Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush registration");
return;
}
final List<String> receivers = getBroadcastReceivers(context, application);
if (receivers.contains(application)) {
final boolean byteMessage = features != null && features.contains(ACTION_BYTE_MESSAGE);
Log.d(
Config.LOGTAG,
"received up registration from "
+ application
+ "/"
+ instance
+ " features: "
+ features);
if (UnifiedPushDatabase.getInstance(context).register(application, instance)) {
Log.d(
Config.LOGTAG,
"successfully created UnifiedPush entry. waking up XmppConnectionService");
final Intent serviceIntent = new Intent(context, XmppConnectionService.class);
serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS);
serviceIntent.putExtra("instance", instance);
Compatibility.startService(context, serviceIntent);
} else {
Log.d(Config.LOGTAG, "not successful. sending error message back to application");
final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED);
registrationFailed.setPackage(application);
registrationFailed.putExtra("token", instance);
context.sendBroadcast(registrationFailed);
}
} else {
Log.d(
Config.LOGTAG,
"ignoring invalid UnifiedPush registration. Unknown application "
+ application);
}
}
private List<String> getBroadcastReceivers(final Context context, final String application) {
final Intent messageIntent = new Intent(ACTION_MESSAGE);
messageIntent.setPackage(application);
final List<ResolveInfo> resolveInfo =
context.getPackageManager().queryBroadcastReceivers(messageIntent, 0);
return Lists.transform(
resolveInfo, ri -> ri.activityInfo == null ? null : ri.activityInfo.packageName);
}
private void unregister(final Context context, final String instance) {
if (Strings.isNullOrEmpty(instance)) {
Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush un-registration");
return;
}
final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context);
if (unifiedPushDatabase.deleteInstance(instance)) {
Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush");
}
}
private void unregisterApplication(final Context context, final Uri uri) {
if (uri != null && "package".equalsIgnoreCase(uri.getScheme())) {
final String application = uri.getSchemeSpecificPart();
if (Strings.isNullOrEmpty(application)) {
return;
}
Log.d(Config.LOGTAG, "app " + application + " has been removed from the system");
final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context);
if (database.deleteApplication(application)) {
Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush");
}
}
}
public static String hash(String... components) {
return BaseEncoding.base64()
.encode(
Hashing.sha256()
.hashString(Joiner.on('\0').join(components), Charsets.UTF_8)
.asBytes());
}
}

View file

@ -86,6 +86,10 @@ import org.conscrypt.Conscrypt;
import org.openintents.openpgp.IOpenPgpService2;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
import com.google.common.base.Optional;
import java.text.ParseException;
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
import eu.siacs.conversations.utils.AccountUtils;
import java.io.File;
import java.security.Security;
@ -189,7 +193,6 @@ import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
import eu.siacs.conversations.xmpp.OnStatusChanged;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.Patches;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.forms.Data;
@ -222,6 +225,7 @@ public class XmppConnectionService extends Service {
public static final String ACTION_END_CALL = "end_call";
public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
public static final String FDroid = "org.fdroid.fdroid";
public static final String PlayStore = "com.android.vending";
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
@ -258,6 +262,7 @@ public class XmppConnectionService extends Service {
public final FileBackend fileBackend = new FileBackend(this);
private MemorizingTrustManager mMemorizingTrustManager;
private final NotificationService mNotificationService = new NotificationService(this);
private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this);
private final ChannelDiscoveryService mChannelDiscoveryService = new ChannelDiscoveryService(this);
private final ShortcutService mShortcutService = new ShortcutService(this);
private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
@ -414,6 +419,7 @@ public class XmppConnectionService extends Service {
connectMultiModeConversations(account);
syncDirtyContacts(account);
unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account);
}
};
private boolean destroyed = false;
@ -850,6 +856,13 @@ public class XmppConnectionService extends Service {
case ACTION_FCM_TOKEN_REFRESH:
refreshAllFcmTokens();
break;
case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
final String instance = intent.getStringExtra("instance");
final Optional<UnifiedPushBroker.Transport> transport = renewUnifiedPushEndpoints();
if (instance != null && transport.isPresent()) {
unifiedPushBroker.rebroadcastEndpoint(instance, transport.get());
}
break;
case ACTION_IDLE_PING:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
scheduleNextIdlePing();
@ -862,7 +875,7 @@ public class XmppConnectionService extends Service {
case Intent.ACTION_SEND:
Uri uri = intent.getData();
if (uri != null) {
Log.d(Config.LOGTAG, "received uri permission for " + uri.toString());
Log.d(Config.LOGTAG, "received uri permission for " + uri);
}
return START_STICKY;
}
@ -1034,6 +1047,9 @@ public class XmppConnectionService extends Service {
editor.putBoolean(ENABLE_MULTI_ACCOUNTS, true);
}
}
public boolean processUnifiedPushMessage(final Account account, final Jid transport, final Element push) {
return unifiedPushBroker.processPushMessage(account, transport, push);
}
public void reinitializeMuclumbusService() {
mChannelDiscoveryService.initializeMuclumbusService();
@ -1451,6 +1467,7 @@ public class XmppConnectionService extends Service {
editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
editor.apply();
toggleSetProfilePictureActivity(hasEnabledAccounts);
reconfigurePushDistributor();
restoreFromDatabase();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
@ -1826,9 +1843,7 @@ public class XmppConnectionService extends Service {
}
}
MessagePacket packet = null;
final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI
|| !Patches.BAD_MUC_REFLECTION.contains(account.getServerIdentity()))
&& !message.edited();
final boolean addToConversation = !message.edited();
boolean saveInDb = addToConversation;
message.setStatus(Message.STATUS_WAITING);
@ -2757,9 +2772,16 @@ public class XmppConnectionService extends Service {
final int targetState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
getPackageManager().setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
} catch (IllegalStateException e) {
Log.d(Config.LOGTAG, "unable to toggle profile picture actvitiy");
Log.d(Config.LOGTAG, "unable to toggle profile picture activity");
}
}
public boolean reconfigurePushDistributor() {
return this.unifiedPushBroker.reconfigurePushDistributor();
}
public Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
return this.unifiedPushBroker.renewUnifiedPushEndpoints();
}
private void provisionAccount(final String address, final String password) {
final Jid jid = Jid.ofEscaped(address);
@ -4156,7 +4178,7 @@ public class XmppConnectionService extends Service {
}
});
} else {
Log.d(Config.LOGTAG, "failed to request vcard " + response.toString());
Log.d(Config.LOGTAG, "failed to request vcard " + response);
callback.onAvatarPublicationFailed(R.string.error_publish_avatar_no_server_support);
}
});
@ -5106,6 +5128,7 @@ public class XmppConnectionService extends Service {
}
}
private void sendOfflinePresence(final Account account) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
@ -5300,7 +5323,7 @@ public class XmppConnectionService extends Service {
mAvatarService.clear(account);
sendIqPacket(account, request, (account1, packet) -> {
if (packet.getType() == IqPacket.TYPE.ERROR) {
Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name " + packet.toString());
Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name " + packet);
}
});
}

View file

@ -953,8 +953,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayShowCustomEnabled(false);
actionBar.setTitle(null);
actionBar.setIcon(R.drawable.logo_actionbar);
actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.header_background)));
actionBar.setIcon(R.drawable.logo_toolbar_white);
//actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.header_background)));
actionBar.setSubtitle(null);
actionBar.setDisplayHomeAsUpEnabled(false);
ActionBarUtil.resetCustomActionBarOnClickListeners(binding.toolbar);

View file

@ -65,6 +65,7 @@ import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.ContentAddition;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
@ -101,9 +102,12 @@ public class RtpSessionActivity extends XmppActivity
Arrays.asList(
RtpEndUserState.CONNECTING,
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING);
RtpEndUserState.RECONNECTING,
RtpEndUserState.INCOMING_CONTENT_ADD);
private static final List<RtpEndUserState> STATES_CONSIDERED_CONNECTED =
Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING);
Arrays.asList(
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING);
private static final List<RtpEndUserState> STATES_SHOWING_PIP_PLACEHOLDER =
Arrays.asList(
RtpEndUserState.ACCEPTING_CALL,
@ -111,6 +115,8 @@ public class RtpSessionActivity extends XmppActivity
RtpEndUserState.RECONNECTING);
private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
private static final int REQUEST_ACCEPT_CALL = 0x1111;
private static final int REQUEST_ACCEPT_CONTENT = 0x1112;
private static final int REQUEST_ADD_CONTENT = 0x1113;
private WeakReference<JingleRtpConnection> rtpConnectionReference;
private ActivityRtpSessionBinding binding;
@ -164,8 +170,10 @@ public class RtpSessionActivity extends XmppActivity
getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
final MenuItem help = menu.findItem(R.id.action_help);
final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat);
final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video);
help.setVisible(Config.HELP != null && isHelpButtonVisible());
gotoChat.setVisible(isSwitchToConversationVisible());
switchToVideo.setVisible(isSwitchToVideoVisible());
return super.onCreateOptionsMenu(menu);
}
@ -203,6 +211,15 @@ public class RtpSessionActivity extends XmppActivity
&& STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState());
}
private boolean isSwitchToVideoVisible() {
final JingleRtpConnection connection =
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
if (connection == null) {
return false;
}
return connection.isSwitchToVideoAvailable();
}
private void switchToConversation() {
final Contact contact = getWith();
final Conversation conversation =
@ -215,10 +232,13 @@ public class RtpSessionActivity extends XmppActivity
switch (item.getItemId()) {
case R.id.action_help:
launchHelpInBrowser();
break;
return true;
case R.id.action_goto_chat:
switchToConversation();
break;
return true;
case R.id.action_switch_to_video:
requestPermissionAndSwitchToVideo();
return true;
}
return super.onOptionsItemSelected(item);
}
@ -272,9 +292,60 @@ public class RtpSessionActivity extends XmppActivity
requestPermissionsAndAcceptCall();
}
private void acceptContentAdd() {
try {
requireRtpConnection()
.acceptContentAdd(requireRtpConnection().getPendingContentAddition().summary);
} catch (final IllegalStateException e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private void requestPermissionAndSwitchToVideo() {
final List<String> permissions = permissions(ImmutableSet.of(Media.VIDEO, Media.AUDIO));
if (PermissionUtils.hasPermission(this, permissions, REQUEST_ADD_CONTENT)) {
switchToVideo();
}
}
private void switchToVideo() {
try {
requireRtpConnection().addMedia(Media.VIDEO);
} catch (final IllegalStateException e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private void acceptContentAdd(final ContentAddition contentAddition) {
if (contentAddition == null || contentAddition.direction != ContentAddition.Direction.INCOMING) {
Log.d(Config.LOGTAG,"ignore press on content-accept button");
return;
}
requestPermissionAndAcceptContentAdd(contentAddition);
}
private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) {
final List<String> permissions = permissions(contentAddition.media());
if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) {
requireRtpConnection().acceptContentAdd(contentAddition.summary);
}
}
private void rejectContentAdd(final View view) {
requireRtpConnection().rejectContentAdd();
}
private void requestPermissionsAndAcceptCall() {
final List<String> permissions = permissions(getMedia());
if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
putScreenInCallMode();
checkRecorderAndAcceptCall();
}
}
private List<String> permissions(final Set<Media> media) {
final ImmutableList.Builder<String> permissions = ImmutableList.builder();
if (getMedia().contains(Media.VIDEO)) {
if (media.contains(Media.VIDEO)) {
permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO);
} else {
permissions.add(Manifest.permission.RECORD_AUDIO);
@ -282,10 +353,7 @@ public class RtpSessionActivity extends XmppActivity
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
}
if (PermissionUtils.hasPermission(this, permissions.build(), REQUEST_ACCEPT_CALL)) {
putScreenInCallMode();
checkRecorderAndAcceptCall();
}
return permissions.build();
}
private void checkRecorderAndAcceptCall() {
@ -516,6 +584,10 @@ public class RtpSessionActivity extends XmppActivity
if (PermissionUtils.allGranted(permissionResult.grantResults)) {
if (requestCode == REQUEST_ACCEPT_CALL) {
checkRecorderAndAcceptCall();
} else if (requestCode == REQUEST_ACCEPT_CONTENT) {
acceptContentAdd();
} else if (requestCode == REQUEST_ADD_CONTENT) {
switchToVideo();
}
} else {
@StringRes int res;
@ -598,8 +670,8 @@ public class RtpSessionActivity extends XmppActivity
private boolean isConnected() {
final JingleRtpConnection connection =
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
return connection != null
&& STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState());
final RtpEndUserState endUserState = connection == null ? null : connection.getEndUserState();
return STATES_CONSIDERED_CONNECTED.contains(endUserState) || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD;
}
private boolean switchToPictureInPicture() {
@ -691,6 +763,7 @@ public class RtpSessionActivity extends XmppActivity
return true;
}
final Set<Media> media = getMedia();
final ContentAddition contentAddition = getPendingContentAddition();
if (currentState == RtpEndUserState.INCOMING_CALL) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@ -700,9 +773,9 @@ public class RtpSessionActivity extends XmppActivity
}
setWidth(currentState);
updateVideoViews(currentState);
updateStateDisplay(currentState, media);
updateStateDisplay(currentState, media, contentAddition);
updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState));
updateButtonConfiguration(currentState, media);
updateButtonConfiguration(currentState, media, contentAddition);
updateIncomingCallScreen(currentState);
invalidateOptionsMenu();
return false;
@ -753,10 +826,10 @@ public class RtpSessionActivity extends XmppActivity
}
private void updateStateDisplay(final RtpEndUserState state) {
updateStateDisplay(state, Collections.emptySet());
updateStateDisplay(state, Collections.emptySet(), null);
}
private void updateStateDisplay(final RtpEndUserState state, final Set<Media> media) {
private void updateStateDisplay(final RtpEndUserState state, final Set<Media> media, final ContentAddition contentAddition) {
switch (state) {
case INCOMING_CALL:
Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
@ -766,6 +839,13 @@ public class RtpSessionActivity extends XmppActivity
setTitle(R.string.rtp_state_incoming_call);
}
break;
case INCOMING_CONTENT_ADD:
if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) {
setTitle(R.string.rtp_state_content_add_video);
} else {
setTitle(R.string.rtp_state_content_add);
}
break;
case CONNECTING:
setTitle(R.string.rtp_state_connecting);
break;
@ -842,13 +922,13 @@ public class RtpSessionActivity extends XmppActivity
binding.contactPhoto.setVisibility(View.GONE);
}
final Account account = contact == null ? getWith().getAccount() : contact.getAccount();
binding.detailsAccount.setVisibility(View.VISIBLE); //TODO: Change detailsAccount to usingAccount
binding.detailsAccount.setText( //TODO: Change detailsAccount to usingAccount
binding.detailsAccount.setVisibility(View.VISIBLE);
binding.detailsAccount.setText(
getString(
R.string.using_account,
account.getJid().asBareJid().toEscapedString()));
} else {
binding.detailsAccount.setVisibility(View.GONE); //TODO: Change detailsAccount to usingAccount
binding.detailsAccount.setVisibility(View.GONE);
binding.contactPhoto.setVisibility(View.GONE);
}
}
@ -857,12 +937,16 @@ public class RtpSessionActivity extends XmppActivity
return requireRtpConnection().getMedia();
}
public ContentAddition getPendingContentAddition() {
return requireRtpConnection().getPendingContentAddition();
}
private void updateButtonConfiguration(final RtpEndUserState state) {
updateButtonConfiguration(state, Collections.emptySet());
updateButtonConfiguration(state, Collections.emptySet(), null);
}
@SuppressLint("RestrictedApi")
private void updateButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
private void updateButtonConfiguration(final RtpEndUserState state, final Set<Media> media, final ContentAddition contentAddition) {
if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
this.binding.rejectCall.setVisibility(View.INVISIBLE);
this.binding.endCall.setVisibility(View.INVISIBLE);
@ -877,6 +961,16 @@ public class RtpSessionActivity extends XmppActivity
this.binding.acceptCall.setOnClickListener(this::acceptCall);
this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
this.binding.acceptCall.setVisibility(View.VISIBLE);
} else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) {
this.binding.rejectCall.setContentDescription(getString(R.string.reject_switch_to_video));
this.binding.rejectCall.setOnClickListener(this::rejectContentAdd);
this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
this.binding.rejectCall.setVisibility(View.VISIBLE);
this.binding.endCall.setVisibility(View.INVISIBLE);
this.binding.acceptCall.setContentDescription(getString(R.string.accept));
this.binding.acceptCall.setOnClickListener((v -> acceptContentAdd(contentAddition)));
this.binding.acceptCall.setImageResource(R.drawable.ic_baseline_check_24);
this.binding.acceptCall.setVisibility(View.VISIBLE);
} else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
this.binding.rejectCall.setContentDescription(getString(R.string.exit));
this.binding.rejectCall.setOnClickListener(this::exit);
@ -1051,6 +1145,12 @@ public class RtpSessionActivity extends XmppActivity
}
private void disableVideo(View view) {
final JingleRtpConnection rtpConnection = requireRtpConnection();
final ContentAddition pending = rtpConnection.getPendingContentAddition();
if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) {
rtpConnection.retractContentAdd();
return;
}
requireRtpConnection().setVideoEnabled(false);
updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable());
}
@ -1279,6 +1379,7 @@ public class RtpSessionActivity extends XmppActivity
final AbstractJingleConnection.Id id = requireRtpConnection().getId();
final boolean verified = requireRtpConnection().isVerified();
final Set<Media> media = getMedia();
final ContentAddition contentAddition = getPendingContentAddition();
final Contact contact = getWith();
if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
if (state == RtpEndUserState.ENDED) {
@ -1287,10 +1388,10 @@ public class RtpSessionActivity extends XmppActivity
}
runOnUiThread(
() -> {
updateStateDisplay(state, media);
updateStateDisplay(state, media, contentAddition);
updateVerifiedShield(
verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state));
updateButtonConfiguration(state, media);
updateButtonConfiguration(state, media, contentAddition);
updateVideoViews(state);
updateIncomingCallScreen(state, contact);
invalidateOptionsMenu();
@ -1308,8 +1409,8 @@ public class RtpSessionActivity extends XmppActivity
@Override
public void onAudioDeviceChanged(
AppRTCAudioManager.AudioDevice selectedAudioDevice,
Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
final AppRTCAudioManager.AudioDevice selectedAudioDevice,
final Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
Log.d(
Config.LOGTAG,
"onAudioDeviceChanged in activity: selected:"
@ -1317,24 +1418,26 @@ public class RtpSessionActivity extends XmppActivity
+ ", available:"
+ availableAudioDevices);
try {
if (getMedia().contains(Media.VIDEO)) {
Log.d(Config.LOGTAG, "nothing to do; in video mode");
return;
}
final RtpEndUserState endUserState = requireRtpConnection().getEndUserState();
if (endUserState == RtpEndUserState.CONNECTED) {
final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
updateInCallButtonConfigurationSpeaker(
audioManager.getSelectedAudioDevice(),
audioManager.getAudioDevices().size());
} else if (END_CARD.contains(endUserState)) {
final Set<Media> media = getMedia();
if (END_CARD.contains(endUserState)) {
Log.d(
Config.LOGTAG,
"onAudioDeviceChanged() nothing to do because end card has been reached");
} else {
if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) {
final AppRTCAudioManager audioManager =
requireRtpConnection().getAudioManager();
updateInCallButtonConfigurationSpeaker(
audioManager.getSelectedAudioDevice(),
audioManager.getAudioDevices().size());
}
Log.d(
Config.LOGTAG,
"put proximity wake lock into proper state after device update");
putProximityWakeLockInProperState(selectedAudioDevice);
}
} catch (IllegalStateException e) {
} catch (final IllegalStateException e) {
Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
}
}

View file

@ -3,6 +3,9 @@ package eu.siacs.conversations.ui;
import static eu.siacs.conversations.persistance.FileBackend.APP_DIRECTORY;
import static eu.siacs.conversations.utils.StorageHelper.getBackupDirectory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import android.app.FragmentManager;
import android.content.DialogInterface;
import android.content.Intent;
@ -49,6 +52,7 @@ import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.xmpp.Jid;
import me.drakeet.support.toast.ToastCompat;
import eu.siacs.conversations.services.UnifiedPushDistributor;
public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener {
@ -130,7 +134,36 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
}
@Override
void onBackendConnected() {}
void onBackendConnected() {
final Preference accountPreference =
mSettingsFragment.findPreference(UnifiedPushDistributor.PREFERENCE_ACCOUNT);
reconfigureUpAccountPreference(accountPreference);
}
private void reconfigureUpAccountPreference(final Preference preference) {
final ListPreference listPreference;
if (preference instanceof ListPreference) {
listPreference = (ListPreference) preference;
} else {
return;
}
final List<CharSequence> accounts =
ImmutableList.copyOf(
Lists.transform(
xmppConnectionService.getAccounts(),
a -> a.getJid().asBareJid().toEscapedString()));
final ImmutableList.Builder<CharSequence> entries = new ImmutableList.Builder<>();
final ImmutableList.Builder<CharSequence> entryValues = new ImmutableList.Builder<>();
entries.add(getString(R.string.no_account_deactivated));
entryValues.add("none");
entries.addAll(accounts);
entryValues.addAll(accounts);
listPreference.setEntries(entries.build().toArray(new CharSequence[0]));
listPreference.setEntryValues(entryValues.build().toArray(new CharSequence[0]));
if (!accounts.contains(listPreference.getValue())) {
listPreference.setValue("none");
}
}
@Override
public void onStart() {
@ -651,6 +684,11 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
} else if (name.equals(USE_UNICOLORED_CHATBG)) {
xmppConnectionService.updateConversationUi();
}
else if (UnifiedPushDistributor.PREFERENCES.contains(name)) {
if (xmppConnectionService.reconfigurePushDistributor()) {
xmppConnectionService.renewUnifiedPushEndpoints();
}
}
}
@Override

View file

@ -771,7 +771,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
if (mRequestedContactsPermission.compareAndSet(false, true)) {
if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
final AtomicBoolean requestPermission = new AtomicBoolean(false);
builder.setTitle(R.string.sync_with_contacts);
@ -793,6 +794,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
});
SharedPreferences pref = this.getSharedPreferences("PACKAGE.NAME",MODE_PRIVATE);
Boolean firstTime = pref.getBoolean("firstTime",true);
if(firstTime){
builder.setCancelable(QuickConversationsService.isQuicksy());
final AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(QuickConversationsService.isQuicksy());
@ -803,9 +807,12 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
});
dialog.show();
pref.edit().putBoolean("firstTime",false).apply();
}
} else {
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
}
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
}
}
}
}
@ -813,6 +820,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);

View file

@ -6,6 +6,9 @@ import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import com.google.common.base.Optional;
import com.google.common.primitives.Ints;
import eu.siacs.conversations.utils.XmlHelper;
import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.Jid;
@ -149,6 +152,13 @@ public class Element {
return null;
}
}
public Optional<Integer> getOptionalIntAttribute(final String name) {
final String value = getAttribute(name);
if (value == null) {
return Optional.absent();
}
return Optional.fromNullable(Ints.tryParse(value));
}
public Jid getAttributeAsJid(String name) {
final String jid = this.getAttribute(name);

View file

@ -65,4 +65,5 @@ public final class Namespace {
public static final String PARS = "urn:xmpp:pars:0";
public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite";
public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push";
}

View file

@ -1,16 +1,11 @@
package eu.siacs.conversations.xmpp;
import java.util.Arrays;
import java.util.List;
public class Patches {
public static final List<String> DISCO_EXCEPTIONS = Arrays.asList(
"nimbuzz.com"
);
public static final List<XmppConnection.Identity> BAD_MUC_REFLECTION = Arrays.asList(
XmppConnection.Identity.SLACK
);
public static final List<String> ENCRYPTION_EXCEPTIONS = Arrays.asList(
"support@monocles.de"
);
}
}

View file

@ -12,6 +12,7 @@ import android.util.Base64;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import com.google.common.base.Optional;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -174,6 +175,7 @@ public class XmppConnection implements Runnable {
private String streamId = null;
private int stanzasReceived = 0;
private int stanzasSent = 0;
private int stanzasSentBeforeAuthentication;
private long lastPacketReceived = 0;
private long lastPingSent = 0;
private long lastConnect = 0;
@ -682,22 +684,21 @@ public class XmppConnection implements Runnable {
}
final Element ack = tagReader.readElement(nextTag);
lastPacketReceived = SystemClock.elapsedRealtime();
try {
final boolean acknowledgedMessages;
synchronized (this.mStanzaQueue) {
final int serverSequence = Integer.parseInt(ack.getAttribute("h"));
acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence);
final boolean acknowledgedMessages;
synchronized (this.mStanzaQueue) {
final Optional<Integer> serverSequence = ack.getOptionalIntAttribute("h");
if (serverSequence.isPresent()) {
acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence.get());
} else {
acknowledgedMessages = false;
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": server send ack without sequence number");
}
if (acknowledgedMessages) {
mXmppConnectionService.updateConversationUi();
}
} catch (NumberFormatException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server send ack without sequence number");
} catch (NullPointerException e) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": server send ack without sequence number");
}
if (acknowledgedMessages) {
mXmppConnectionService.updateConversationUi();
}
} else if (nextTag.isStart("failed")) {
final Element failed = tagReader.readElement(nextTag);
@ -811,10 +812,9 @@ public class XmppConnection implements Runnable {
account.getJid().asBareJid()
+ ": jid changed during SASL 2.0. updating database");
}
final boolean nopStreamFeatures;
final Element bound = success.findChild("bound", Namespace.BIND2);
final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3");
final Element failed = success.findChild("failed", "urn:xmpp:sm:3");
final Element resumed = success.findChild("resumed", Namespace.STREAM_MANAGEMENT);
final Element failed = success.findChild("failed", Namespace.STREAM_MANAGEMENT);
final Element tokenWrapper = success.findChild("token", Namespace.FAST);
final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token");
if (bound != null && resumed != null) {
@ -838,6 +838,7 @@ public class XmppConnection implements Runnable {
final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
final boolean waitForDisco;
if (streamManagementEnabled != null) {
resetOutboundStanzaQueue();
processEnabled(streamManagementEnabled);
waitForDisco = true;
} else {
@ -864,8 +865,16 @@ public class XmppConnection implements Runnable {
tokenMechanism = null;
}
if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) {
this.account.setFastToken(tokenMechanism,token);
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism);
this.account.setFastToken(tokenMechanism, token);
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": storing hashed token " + tokenMechanism);
} else if (this.hashTokenRequest != null) {
Log.w(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": no response to our hashed token request "
+ this.hashTokenRequest);
}
// a successful resume will not send stream features
if (processNopStreamFeatures) {
@ -888,6 +897,37 @@ public class XmppConnection implements Runnable {
return false;
}
}
private void resetOutboundStanzaQueue() {
synchronized (this.mStanzaQueue) {
final List<AbstractAcknowledgeableStanza> intermediateStanzas = new ArrayList<>();
if (Config.EXTENDED_SM_LOGGING) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": stanzas sent before auth: "
+ this.stanzasSentBeforeAuthentication);
}
for (int i = this.stanzasSentBeforeAuthentication + 1; i <= this.stanzasSent; ++i) {
final AbstractAcknowledgeableStanza stanza = this.mStanzaQueue.get(i);
if (stanza != null) {
intermediateStanzas.add(stanza);
}
}
this.mStanzaQueue.clear();
for (int i = 0; i < intermediateStanzas.size(); ++i) {
this.mStanzaQueue.put(i, intermediateStanzas.get(i));
}
this.stanzasSent = intermediateStanzas.size();
if (Config.EXTENDED_SM_LOGGING) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": resetting outbound stanza queue to "
+ this.stanzasSent);
}
}
}
private void processNopStreamFeatures() throws IOException {
final Tag tag = tagReader.readTag();
if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
@ -988,15 +1028,11 @@ public class XmppConnection implements Runnable {
this.isBound = true;
this.tagWriter.writeStanzaAsync(new RequestPacket());
lastPacketReceived = SystemClock.elapsedRealtime();
final String h = resumed.getAttribute("h");
if (h == null) {
resetStreamId();
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
final Optional<Integer> h = resumed.getOptionalIntAttribute("h");
final int serverCount;
try {
serverCount = Integer.parseInt(h);
} catch (final NumberFormatException e) {
if (h.isPresent()) {
serverCount = h.get();
} else {
resetStreamId();
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
@ -1045,28 +1081,22 @@ public class XmppConnection implements Runnable {
}
private void processFailed(final Element failed, final boolean sendBindRequest) {
final int serverCount;
try {
serverCount = Integer.parseInt(failed.getAttribute("h"));
} catch (final NumberFormatException | NullPointerException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed");
resetStreamId();
if (sendBindRequest) {
sendBindRequest();
final Optional<Integer> serverCount = failed.getOptionalIntAttribute("h");
if (serverCount.isPresent()) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": resumption failed but server acknowledged stanza #"
+ serverCount.get());
final boolean acknowledgedMessages;
synchronized (this.mStanzaQueue) {
acknowledgedMessages = acknowledgeStanzaUpTo(serverCount.get());
}
return;
}
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": resumption failed but server acknowledged stanza #"
+ serverCount);
final boolean acknowledgedMessages;
synchronized (this.mStanzaQueue) {
acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
}
if (acknowledgedMessages) {
mXmppConnectionService.updateConversationUi();
if (acknowledgedMessages) {
mXmppConnectionService.updateConversationUi();
}
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed");
}
resetStreamId();
if (sendBindRequest) {
@ -1074,7 +1104,7 @@ public class XmppConnection implements Runnable {
}
}
private boolean acknowledgeStanzaUpTo(int serverCount) {
private boolean acknowledgeStanzaUpTo(final int serverCount) {
if (serverCount > stanzasSent) {
Log.e(
Config.LOGTAG,
@ -1463,7 +1493,7 @@ public class XmppConnection implements Runnable {
quickStartAvailable = false;
} else if (version == SaslMechanism.Version.SASL_2) {
final Element inline = authElement.findChild("inline", Namespace.SASL_2);
final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3");
final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT);
final HashedToken.Mechanism hashTokenRequest;
if (usingFast) {
hashTokenRequest = null;
@ -1507,7 +1537,11 @@ public class XmppConnection implements Runnable {
+ "/"
+ this.saslMechanism.getMechanism());
authenticate.setAttribute("mechanism", this.saslMechanism.getMechanism());
tagWriter.writeElement(authenticate);
synchronized (this.mStanzaQueue) {
this.stanzasSentBeforeAuthentication = this.stanzasSent;
tagWriter.writeElement(authenticate);
}
}
private static boolean isFastTokenAvailable(final Element authentication) {
@ -1948,16 +1982,7 @@ public class XmppConnection implements Runnable {
}
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
mPendingServiceDiscoveries.set(0);
if (!waitForDisco
|| Patches.DISCO_EXCEPTIONS.contains(
account.getJid().getDomain().toEscapedString())) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": do not wait for service discovery");
mWaitForDisco.set(false);
} else {
mWaitForDisco.set(true);
}
mWaitForDisco.set(waitForDisco);
lastDiscoStarted = SystemClock.elapsedRealtime();
mXmppConnectionService.scheduleWakeUpCall(
Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
@ -2321,7 +2346,10 @@ public class XmppConnection implements Runnable {
generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast);
authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism());
sendStartStream(true, false);
tagWriter.writeElement(authenticate);
synchronized (this.mStanzaQueue) {
this.stanzasSentBeforeAuthentication = this.stanzasSent;
tagWriter.writeElement(authenticate);
}
Log.d(
Config.LOGTAG,
account.getJid().toString()
@ -2415,6 +2443,9 @@ public class XmppConnection implements Runnable {
}
}
++stanzasSent;
if (Config.EXTENDED_SM_LOGGING) {
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": counting outbound "+packet.getName()+" as #" + stanzasSent);
}
this.mStanzaQueue.append(stanzasSent, stanza);
if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
if (Config.EXTENDED_SM_LOGGING) {
@ -2649,43 +2680,10 @@ public class XmppConnection implements Runnable {
this.mInteractive = interactive;
}
public Identity getServerIdentity() {
synchronized (this.disco) {
ServiceDiscoveryResult result = disco.get(account.getJid().getDomain());
if (result == null) {
return Identity.UNKNOWN;
}
for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
if (id.getType().equals("im")
&& id.getCategory().equals("server")
&& id.getName() != null) {
switch (id.getName()) {
case "Prosody":
return Identity.PROSODY;
case "ejabberd":
return Identity.EJABBERD;
case "Slack-XMPP":
return Identity.SLACK;
}
}
}
}
return Identity.UNKNOWN;
}
private IqGenerator getIqGenerator() {
return mXmppConnectionService.getIqGenerator();
}
public enum Identity {
FACEBOOK,
SLACK,
EJABBERD,
PROSODY,
NIMBUZZ,
UNKNOWN
}
private class MyKeyManager implements X509KeyManager {
@Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
@ -2813,7 +2811,7 @@ public class XmppConnection implements Runnable {
public boolean sm() {
return streamId != null
|| (connection.streamFeatures != null
&& connection.streamFeatures.hasChild("sm"));
&& connection.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT));
}
public boolean csi() {

View file

@ -45,10 +45,13 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.utils.IP;
import eu.siacs.conversations.xml.Namespace;
@ -1277,7 +1280,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false);
this.responderRtpContentMap = respondingRtpContentMap;
storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip());
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
final ListenableFuture<RtpContentMap> outgoingContentMapFuture =
prepareOutgoingContentMap(respondingRtpContentMap);
Futures.addCallback(
@ -1286,6 +1288,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
@Override
public void onSuccess(final RtpContentMap outgoingContentMap) {
sendSessionAccept(outgoingContentMap);
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
}
@Override
@ -1728,8 +1731,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
SessionDescription.parse(webRTCSessionDescription.description);
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true);
this.initiatorRtpContentMap = rtpContentMap;
//TODO delay ready to receive ice until after session-init
this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
final ListenableFuture<RtpContentMap> outgoingContentMapFuture =
encryptSessionInitiate(rtpContentMap);
Futures.addCallback(
@ -1738,6 +1739,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
@Override
public void onSuccess(final RtpContentMap outgoingContentMap) {
sendSessionInitiate(outgoingContentMap, targetState);
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
}
@Override
@ -2661,6 +2663,12 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ ": skipping invalid combination of udp/tls in external services");
continue;
}
// TODO Starting on milestone 110, Chromium will perform
// stricter validation of TURN and STUN URLs passed to the
// constructor of an RTCPeerConnection. More specifically,
// STUN URLs will not support a query section, and TURN URLs
// will support only a transport parameter in their query
// section.
final PeerConnection.IceServer.Builder iceServerBuilder =
PeerConnection.IceServer.builder(
String.format(
@ -2794,6 +2802,25 @@ public class JingleRtpConnection extends AbstractJingleConnection
id.account, id.with, id.sessionId, endUserState);
}
public boolean isSwitchToVideoAvailable() {
final boolean prerequisite =
Media.audioOnly(getMedia())
&& Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING)
.contains(getEndUserState());
return prerequisite && remoteHasVideoFeature();
}
private boolean remoteHasVideoFeature() {
final Contact contact = id.getContact();
final Presence presence =
contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
final ServiceDiscoveryResult serviceDiscoveryResult =
presence == null ? null : presence.getServiceDiscoveryResult();
final List<String> features =
serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
return features != null && features.contains(Namespace.JINGLE_FEATURE_VIDEO);
}
private interface OnIceServersDiscovered {
void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
}

View file

@ -87,7 +87,15 @@ class VideoSourceWrapper {
public void dispose() {
this.cameraVideoCapturer.dispose();
if (this.videoSource != null) {
this.videoSource.dispose();
dispose(this.videoSource);
}
}
private static void dispose(final VideoSource videoSource) {
try {
videoSource.dispose();
} catch (final IllegalStateException e) {
Log.e(Config.LOGTAG, "unable to dispose video source", e);
}
}

View file

@ -423,19 +423,32 @@ public class WebRTCWrapper {
}
void restartIce() {
executorService.execute(() -> {
final PeerConnection peerConnection = requirePeerConnection();
setIsReadyToReceiveIceCandidates(false);
peerConnection.restartIce();
requirePeerConnection().restartIce();}
);
executorService.execute(
() -> {
final PeerConnection peerConnection;
try {
peerConnection = requirePeerConnection();
} catch (final PeerConnectionNotInitialized e) {
Log.w(
EXTENDED_LOGGING_TAG,
"PeerConnection vanished before we could execute restart");
return;
}
setIsReadyToReceiveIceCandidates(false);
peerConnection.restartIce();
});
}
public void setIsReadyToReceiveIceCandidates(final boolean ready) {
readyToReceivedIceCandidates.set(ready);
final int was = iceCandidates.size();
while (ready && iceCandidates.peek() != null) {
eventCallback.onIceCandidate(iceCandidates.poll());
}
final int is = iceCandidates.size();
Log.d(
EXTENDED_LOGGING_TAG,
"setIsReadyToReceiveCandidates(" + ready + ") was=" + was + " is=" + is);
}
synchronized void close() {
@ -445,8 +458,8 @@ public class WebRTCWrapper {
final AppRTCAudioManager audioManager = this.appRTCAudioManager;
final EglBase eglBase = this.eglBase;
if (peerConnection != null) {
dispose(peerConnection);
this.peerConnection = null;
dispose(peerConnection);
}
if (audioManager != null) {
toneManager.setAppRtcAudioManagerHasControl(false);
@ -455,6 +468,7 @@ public class WebRTCWrapper {
this.localVideoTrack = null;
this.remoteVideoTrack = null;
if (videoSourceWrapper != null) {
this.videoSourceWrapper = null;
try {
videoSourceWrapper.stopCapture();
} catch (final InterruptedException e) {
@ -467,6 +481,7 @@ public class WebRTCWrapper {
this.eglBase = null;
}
if (peerConnectionFactory != null) {
this.peerConnectionFactory = null;
peerConnectionFactory.dispose();
}
}
@ -693,7 +708,11 @@ public class WebRTCWrapper {
}
public PeerConnection.SignalingState getSignalingState() {
return requirePeerConnection().signalingState();
try {
return requirePeerConnection().signalingState();
} catch (final IllegalStateException e) {
return PeerConnection.SignalingState.CLOSED;
}
}
EglBase.Context getEglBaseContext() {

View file

@ -64,7 +64,9 @@ public class Content extends Element {
return null;
}
final String namespace = description.getNamespace();
if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
if (FileTransferDescription.NAMESPACES.contains(namespace)) {
return FileTransferDescription.upgrade(description);
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
return RtpDescription.upgrade(description);
} else {
return GenericDescription.upgrade(description);
@ -84,7 +86,11 @@ public class Content extends Element {
public GenericTransportInfo getTransport() {
final Element transport = this.findChild("transport");
final String namespace = transport == null ? null : transport.getNamespace();
if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) {
return IbbTransportInfo.upgrade(transport);
} else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) {
return S5BTransportInfo.upgrade(transport);
} else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
return IceUdpTransportInfo.upgrade(transport);
} else if (transport != null) {
return GenericTransportInfo.upgrade(transport);
@ -93,6 +99,7 @@ public class Content extends Element {
}
}
public void setTransport(GenericTransportInfo transportInfo) {
this.addChild(transportInfo);
}

View file

@ -0,0 +1,9 @@
<vector android:height="54dp" android:viewportHeight="240.94"
android:viewportWidth="566.93" android:width="127.06159dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M261.94,136.91a27.49,27.49 0,0 1,-10.84 2.21c-14.76,0 -26.77,-11.55 -26.77,-25.74s12,-25.74 26.77,-25.74a27.66,27.66 0,0 1,10.84 2.21V77.15a40.76,40.76 0,0 0,-10.84 -1.47c-21.63,0 -39.22,16.91 -39.22,37.7s17.59,37.7 39.22,37.7a40.76,40.76 0,0 0,10.84 -1.47Z"/>
<path android:fillColor="#FFFFFF" android:pathData="M322.18,80.71c-12.78,-8.1 -22.72,-4.85 -29.95,-1.11a22.8,22.8 0,0 0,-3.23 2V41.4H275.41V151.08h13.83V111.54c0,-7.19 3.27,-16.52 9.34,-19.65 6.42,-3.32 10.36,-3.2 16.19,0.5 5,3.18 8.38,12.42 8.38,19.06v39.63H337V111.45C337,100.84 331.9,86.87 322.18,80.71Z"/>
<path android:fillColor="#FFFFFF" android:pathData="M392.21,81.31c-18.32,-9.18 -34.54,-4.28 -39.55,-3.37V91.33C355.76,91 374,85.91 384.88,93c3.07,2 6,3.94 8.17,8.42 -15.07,0.19 -31.17,1.79 -38.77,12.23 0,0 -9.75,11.83 -2.25,26.62 5.82,11.48 28.15,15.76 44,3v7.83H409.9V112.84C408.91,96.65 402.42,86.44 392.21,81.31ZM361.74,129c0,-10.36 15.55,-17.72 34,-17.93a41.29,41.29 0,0 1,0.32 5.21v16.36C381.42,144.25 361.74,139.37 361.74,129Z"/>
<path android:fillColor="#FFFFFF" android:pathData="M466.33,90.11V76.25H439.44l0,-34.42H425.57v72.24c1,16.19 7.48,26.4 17.69,31.52a51.21,51.21 0,0 0,23 5.49V137.65c-5.43,0 -11.19,-0.82 -15.71,-3.76 -3.07,-2 -6,-3.94 -8.18,-8.42 0,0 -2.91,-5.6 -3,-18.76l0,-16.6Z"/>
<path android:fillColor="#FFFFFF" android:pathData="M164.64,55.85a54.79,54.79 0,0 0,-51.74 72.81l-5,10.27 -7,14.36a2.49,2.49 0,0 0,2.41 3.59l16,-1.09 13.12,-0.91a54.78,54.78 0,1 0,32.3 -99ZM164.64,155.2a44.56,44.56 0,1 1,44.56 -44.56A44.61,44.61 0,0 1,164.64 155.2Z"/>
<path android:fillColor="#FFFFFF" android:pathData="M193.24,97.53a11.31,11.31 0,1 0,-0.83 6v19.21a1.63,1.63 0,0 1,-0.85 1.44,14.75 14.75,0 0,1 -7.32,1.94c-4,-0.09 -7.8,-2.79 -9.26,-3.94a17,17 0,0 0,-2.51 -1.66,10.62 10.62,0 0,0 -3.26,-1.22c-1.41,-0.24 -3,-0.51 -4.61,0.93 -1.62,-1.44 -3.21,-1.17 -4.61,-0.93a10.57,10.57 0,0 0,-3.27 1.22,16.22 16.22,0 0,0 -2.51,1.66c-1.46,1.15 -5.28,3.85 -9.26,3.94a14.91,14.91 0,0 1,-7.44 -2,1.43 1.43,0 0,1 -0.73,-1.26L136.78,103.32a11.28,11.28 0,1 0,-0.74 -5.79,1.52 1.52,0 0,0 -1.12,2.42v23a3.08,3.08 0,0 0,0.44 1.6c1.22,2 5.69,8.56 12.35,8.93 0.61,0 1.19,0.06 1.75,0.06a20.06,20.06 0,0 0,9.35 -2.28,12.15 12.15,0 0,0 5.79,-5.81 12.08,12.08 0,0 0,5.78 5.81,20.25 20.25,0 0,0 11.11,2.22c6.7,-0.37 11.18,-7 12.39,-9a2.81,2.81 0,0 0,0.39 -1.44v-23a1.48,1.48 0,0 0,0.39 -1A1.53,1.53 0,0 0,193.24 97.53ZM190.11,99.05a8.05,8.05 0,1 1,-8.05 -8A8.07,8.07 0,0 1,190.11 99.05ZM147.22,91.05a8.06,8.06 0,1 1,-8.05 8A8.07,8.07 0,0 1,147.22 91Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector android:height="53.973824dp" android:viewportHeight="240.94"
android:viewportWidth="566.93" android:width="127dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="m261.94,152.91a27.49,27.49 0,0 1,-10.84 2.21c-14.76,0 -26.77,-11.55 -26.77,-25.74 0,-14.19 12,-25.74 26.77,-25.74a27.66,27.66 0,0 1,10.84 2.21L261.94,93.15A40.76,40.76 0,0 0,251.1 91.68c-21.63,0 -39.22,16.91 -39.22,37.7 0,20.79 17.59,37.7 39.22,37.7a40.76,40.76 0,0 0,10.84 -1.47z"/>
<path android:fillColor="#FFFFFF" android:pathData="M322.18,96.71C309.4,88.61 299.46,91.86 292.23,95.6a22.8,22.8 0,0 0,-3.23 2L289,57.4h-13.59v109.68h13.83v-39.54c0,-7.19 3.27,-16.52 9.34,-19.65 6.42,-3.32 10.36,-3.2 16.19,0.5 5,3.18 8.38,12.42 8.38,19.06v39.63L337,167.08v-39.63c0,-10.61 -5.1,-24.58 -14.82,-30.74z"/>
<path android:fillColor="#FFFFFF" android:pathData="M392.21,97.31C373.89,88.13 357.67,93.03 352.66,93.94L352.66,107.33C355.76,107 374,101.91 384.88,109c3.07,2 6,3.94 8.17,8.42 -15.07,0.19 -31.17,1.79 -38.77,12.23 0,0 -9.75,11.83 -2.25,26.62 5.82,11.48 28.15,15.76 44,3v7.83L409.9,167.1L409.9,128.84C408.91,112.65 402.42,102.44 392.21,97.31ZM361.74,145c0,-10.36 15.55,-17.72 34,-17.93a41.29,41.29 0,0 1,0.32 5.21v16.36c-14.64,11.61 -34.32,6.73 -34.32,-3.64z"/>
<path android:fillColor="#FFFFFF" android:pathData="M466.33,106.11L466.33,92.25L439.44,92.25L439.44,57.83h-13.87v72.24c1,16.19 7.48,26.4 17.69,31.52a51.21,51.21 0,0 0,23 5.49v-13.43c-5.43,0 -11.19,-0.82 -15.71,-3.76 -3.07,-2 -6,-3.94 -8.18,-8.42 0,0 -2.91,-5.6 -3,-18.76v-16.6z"/>
<path android:fillColor="#FFFFFF" android:pathData="m164.64,71.85a54.79,54.79 0,0 0,-51.74 72.81l-5,10.27 -7,14.36a2.49,2.49 0,0 0,2.41 3.59l16,-1.09 13.12,-0.91a54.78,54.78 0,1 0,32.3 -99zM164.64,171.2a44.56,44.56 0,1 1,44.56 -44.56,44.61 44.61,0 0,1 -44.56,44.56z"/>
<path android:fillColor="#FFFFFF" android:pathData="m193.24,113.53a11.31,11.31 0,1 0,-0.83 6v19.21a1.63,1.63 0,0 1,-0.85 1.44,14.75 14.75,0 0,1 -7.32,1.94c-4,-0.09 -7.8,-2.79 -9.26,-3.94a17,17 0,0 0,-2.51 -1.66,10.62 10.62,0 0,0 -3.26,-1.22c-1.41,-0.24 -3,-0.51 -4.61,0.93 -1.62,-1.44 -3.21,-1.17 -4.61,-0.93a10.57,10.57 0,0 0,-3.27 1.22,16.22 16.22,0 0,0 -2.51,1.66c-1.46,1.15 -5.28,3.85 -9.26,3.94a14.91,14.91 0,0 1,-7.44 -2,1.43 1.43,0 0,1 -0.73,-1.26v-19.54a11.28,11.28 0,1 0,-0.74 -5.79,1.52 1.52,0 0,0 -1.12,2.42v23a3.08,3.08 0,0 0,0.44 1.6c1.22,2 5.69,8.56 12.35,8.93 0.61,0 1.19,0.06 1.75,0.06a20.06,20.06 0,0 0,9.35 -2.28,12.15 12.15,0 0,0 5.79,-5.81 12.08,12.08 0,0 0,5.78 5.81,20.25 20.25,0 0,0 11.11,2.22c6.7,-0.37 11.18,-7 12.39,-9a2.81,2.81 0,0 0,0.39 -1.44v-23a1.48,1.48 0,0 0,0.39 -1,1.53 1.53,0 0,0 -1.42,-1.51zM190.11,115.05a8.05,8.05 0,1 1,-8.05 -8,8.07 8.07,0 0,1 8.05,8zM147.22,107.05a8.06,8.06 0,1 1,-8.05 8,8.07 8.07,0 0,1 8.05,-8.05z"/>
</vector>

View file

@ -18,7 +18,7 @@
android:background="?attr/colorPrimary"
android:elevation="@dimen/toolbar_elevation"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
app:tabGravity="fill"
app:tabMode="fixed"
app:tabSelectedTextColor="@color/white"

View file

@ -1068,6 +1068,8 @@
<string name="rtp_state_declined_or_busy">Ocupado</string>
<string name="rtp_state_connectivity_error">No se puede conectar</string>
<string name="rtp_state_connectivity_lost_error">Conexión perdida</string>
<string name="rtp_state_content_add_video">¿Cambiar a vídeo llamada?</string>
<string name="rtp_state_content_add">¿Añadir pistas adicionales?</string>
<string name="rtp_state_retracted">Llamada retirada</string>
<string name="rtp_state_application_failure">Fallo en la aplicación</string>
<string name="rtp_state_security_error">Problema con la verificación</string>
@ -1231,4 +1233,6 @@
<string name="audio_video_disabled_tor">Las llamadas están deshabilitadas cuando se usa Tor</string>
<string name="pref_enable_persistent_rooms_summary">Hacer los grupos y canales públicos persistentes y no eliminarlos del servidor después de que el último usuario lo abandone.</string>
<string name="pref_enable_persistent_rooms_title">Grupos persistentes</string>
<string name="switch_to_video">Cambiar a vídeo</string>
<string name="reject_switch_to_video">Rechazar peticiones de cambio a vídeo</string>
</resources>

View file

@ -102,6 +102,8 @@
<bool name="show_date_in_quotes">false</bool>
<integer name="individual_notification">0</integer>
<bool name="enable_persistent_rooms">true</bool>
<string name="default_push_server">up.conversations.im</string>
<string name="default_push_account">none</string>
<!--
<string-array name="domains">

View file

@ -1245,4 +1245,10 @@
<string name="pref_enable_persistent_rooms_title">Persistent group chats</string>
<string name="switch_to_video">Switch to video</string>
<string name="reject_switch_to_video">Reject switch to video request</string>
<string name="unified_push_distributor">UnifiedPush Distributor</string>
<string name="pref_up_push_account_title">XMPP Account</string>
<string name="pref_up_push_account_summary">The account through which push messages will be received.</string>
<string name="pref_up_push_server_title">Push Server</string>
<string name="pref_up_push_server_summary">A user-chosen push server to relay push messages via XMPP to your device.</string>
<string name="no_account_deactivated">None (deactivated)</string>
</resources>