forked from mirror/monocles_chat
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:
commit
342ddbee6e
42 changed files with 1314 additions and 208 deletions
10
build.gradle
10
build.gradle
|
@ -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"
|
||||
|
|
16
fastlane/metadata/android/de/changelogs/125.txt
Normal file
16
fastlane/metadata/android/de/changelogs/125.txt
Normal 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
|
2
fastlane/metadata/android/de/changelogs/126.txt
Normal file
2
fastlane/metadata/android/de/changelogs/126.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
* Ändere Farbe der Topbar zu Themefarbe
|
||||
* Kritische Datenbank Fehler repariert für den Updateprozess
|
16
fastlane/metadata/android/en-US/changelogs/125.txt
Normal file
16
fastlane/metadata/android/en-US/changelogs/125.txt
Normal 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
|
2
fastlane/metadata/android/en-US/changelogs/126.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/126.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
* change color to theme of topbar
|
||||
* critical database bugfix for updateprocess
|
BIN
src/debug/logo_actionbar_white-playstore.png
Normal file
BIN
src/debug/logo_actionbar_white-playstore.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
4
src/debug/res/values/logo_actionbar_white_background.xml
Normal file
4
src/debug/res/values/logo_actionbar_white_background.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="logo_actionbar_white_background">#3DDC84</color>
|
||||
</resources>
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
9
src/main/res/drawable/chat_graphic_white.xml
Normal file
9
src/main/res/drawable/chat_graphic_white.xml
Normal 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>
|
9
src/main/res/drawable/logo_toolbar_white.xml
Normal file
9
src/main/res/drawable/logo_toolbar_white.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue