Detailed changes
@@ -10,6 +10,8 @@ import androidx.preference.PreferenceManager;
import com.google.common.base.Strings;
+import java.security.SecureRandom;
+
public class AppSettings {
public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service";
@@ -45,6 +47,7 @@ public class AppSettings {
public static final String LARGE_FONT = "large_font";
private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers";
+ private static final String INSTALLATION_ID = "im.conversations.android.install_id";
private final Context context;
@@ -140,4 +143,25 @@ public class AppSettings {
public boolean isRequireChannelBinding() {
return getBooleanPreference(REQUIRE_CHANNEL_BINDING, R.bool.require_channel_binding);
}
+
+ public synchronized long getInstallationId() {
+ final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ final long existing = sharedPreferences.getLong(INSTALLATION_ID, 0);
+ if (existing != 0) {
+ return existing;
+ }
+ final var secureRandom = new SecureRandom();
+ final var installationId = secureRandom.nextLong();
+ sharedPreferences.edit().putLong(INSTALLATION_ID, installationId).apply();
+ return installationId;
+ }
+
+ public synchronized void resetInstallationId() {
+ final var secureRandom = new SecureRandom();
+ final var installationId = secureRandom.nextLong();
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putLong(INSTALLATION_ID, installationId)
+ .apply();
+ }
}
@@ -229,6 +229,8 @@ public class XmppConnectionService extends Service {
public DatabaseBackend databaseBackend;
private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
private long mLastActivity = 0;
+
+ private final AppSettings appSettings = new AppSettings(this);
private final FileBackend fileBackend = new FileBackend(this);
private MemorizingTrustManager mMemorizingTrustManager;
private final NotificationService mNotificationService = new NotificationService(this);
@@ -483,6 +485,10 @@ public class XmppConnectionService extends Service {
}
}
+ public AppSettings getAppSettings() {
+ return this.appSettings;
+ }
+
public FileBackend getFileBackend() {
return this.fileBackend;
}
@@ -4633,7 +4639,6 @@ public class XmppConnectionService extends Service {
public void updateMemorizingTrustManager() {
final MemorizingTrustManager trustManager;
- final var appSettings = new AppSettings(this);
if (appSettings.isTrustSystemCAStore()) {
trustManager = new MemorizingTrustManager(getApplicationContext());
} else {
@@ -38,23 +38,27 @@ public class AccountUtils {
return false;
}
- public static String publicDeviceId(final Account account) {
+ public static String publicDeviceId(final Account account, final long installationId) {
final UUID uuid;
try {
uuid = UUID.fromString(account.getUuid());
} catch (final IllegalArgumentException e) {
return account.getUuid();
}
+ return createUuid4(uuid.getMostSignificantBits(), installationId).toString();
+ }
+
+ public static UUID createUuid4(long mostSigBits, long leastSigBits) {
final byte[] bytes =
Bytes.concat(
- Longs.toByteArray(uuid.getLeastSignificantBits()),
- Longs.toByteArray(uuid.getLeastSignificantBits()));
+ Longs.toByteArray(mostSigBits),
+ Longs.toByteArray(leastSigBits));
bytes[6] &= 0x0f; /* clear version */
bytes[6] |= 0x40; /* set to version 4 */
bytes[8] &= 0x3f; /* clear variant */
bytes[8] |= 0x80; /* set to IETF variant */
final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
- return new UUID(byteBuffer.getLong(), byteBuffer.getLong()).toString();
+ return new UUID(byteBuffer.getLong(), byteBuffer.getLong());
}
public static List<String> getEnabledAccounts(final XmppConnectionService service) {
@@ -23,6 +23,7 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.XmppDomainVerifier;
@@ -78,6 +79,7 @@ import im.conversations.android.xmpp.model.sasl.Response;
import im.conversations.android.xmpp.model.sasl.Success;
import im.conversations.android.xmpp.model.sasl2.Authenticate;
import im.conversations.android.xmpp.model.sasl2.Authentication;
+import im.conversations.android.xmpp.model.sasl2.UserAgent;
import im.conversations.android.xmpp.model.sm.Ack;
import im.conversations.android.xmpp.model.sm.Enable;
import im.conversations.android.xmpp.model.sm.Enabled;
@@ -195,7 +197,7 @@ public class XmppConnection implements Runnable {
public XmppConnection(final Account account, final XmppConnectionService service) {
this.account = account;
this.mXmppConnectionService = service;
- this.appSettings = new AppSettings(mXmppConnectionService.getApplicationContext());
+ this.appSettings = mXmppConnectionService.getAppSettings();
this.presenceListener = new PresenceParser(service, account);
this.unregisteredIqListener = new IqParser(service, account);
this.messageListener = new MessageParser(service, account);
@@ -1664,15 +1666,15 @@ public class XmppConnection implements Runnable {
if (!Strings.isNullOrEmpty(firstMessage)) {
authenticate.addChild("initial-response").setContent(firstMessage);
}
- final Element userAgent = authenticate.addChild("user-agent");
- userAgent.setAttribute("id", AccountUtils.publicDeviceId(account));
- userAgent
- .addChild("software")
- .setContent(mXmppConnectionService.getString(R.string.app_name));
+ final var userAgent =
+ authenticate.addExtension(
+ new UserAgent(
+ AccountUtils.publicDeviceId(
+ account, appSettings.getInstallationId())));
+ userAgent.setSoftware(
+ String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
if (!PhoneHelper.isEmulator()) {
- userAgent
- .addChild("device")
- .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
+ userAgent.setDevice(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
}
// do not include bind if 'inlineStreamManagement' is missing and we have a streamId
// (because we would rather just do a normal SM/resume)
@@ -1698,7 +1700,7 @@ public class XmppConnection implements Runnable {
private Bind generateBindRequest(final Collection<String> bindFeatures) {
Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
final var bind = new Bind();
- bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name));
+ bind.setTag(BuildConfig.APP_NAME);
if (bindFeatures.contains(Namespace.CARBONS)) {
bind.addExtension(new im.conversations.android.xmpp.model.carbons.Enable());
}
@@ -2292,6 +2294,10 @@ public class XmppConnection implements Runnable {
return;
}
if (streamError.hasChild("conflict")) {
+ final var loginInfo = this.loginInfo;
+ if (loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2) {
+ this.appSettings.resetInstallationId();
+ }
account.setResource(createNewResource());
Log.d(
Config.LOGTAG,
@@ -2299,7 +2305,7 @@ public class XmppConnection implements Runnable {
+ ": switching resource due to conflict ("
+ account.getResource()
+ ")");
- throw new IOException();
+ throw new IOException("Closed stream due to resource conflict");
} else if (streamError.hasChild("host-unknown")) {
throw new StateChangingException(Account.State.HOST_UNKNOWN);
} else if (streamError.hasChild("policy-violation")) {
@@ -21,4 +21,8 @@ public class Bind extends Extension {
final var inline = getInline();
return inline == null ? Collections.emptyList() : inline.getExtensions(Feature.class);
}
+
+ public void setTag(final String tag) {
+ this.addExtension(new Tag(tag));
+ }
}
@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.bind2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Tag extends Extension {
+
+ public Tag() {
+ super(Tag.class);
+ }
+
+ public Tag(final String tag) {
+ this();
+ setContent(tag);
+ }
+}
@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Device extends Extension {
+
+ public Device() {
+ super(Device.class);
+ }
+
+ public Device(final String device) {
+ this();
+ this.setContent(device);
+ }
+}
@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Software extends Extension {
+
+ public Software() {
+ super(Software.class);
+ }
+
+ public Software(final String software) {
+ this();
+ this.setContent(software);
+ }
+}
@@ -0,0 +1,25 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class UserAgent extends Extension {
+
+ public UserAgent() {
+ super(UserAgent.class);
+ }
+
+ public UserAgent(final String userAgentId) {
+ this();
+ this.setAttribute("id", userAgentId);
+ }
+
+ public void setSoftware(final String software) {
+ this.addExtension(new Software(software));
+ }
+
+ public void setDevice(final String device) {
+ this.addExtension(new Device(device));
+ }
+}
@@ -5,16 +5,36 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
-import android.preference.PreferenceManager;
import android.util.Log;
import com.google.common.collect.ImmutableMap;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.PhoneNumberContact;
+import eu.siacs.conversations.crypto.TrustManagers;
+import eu.siacs.conversations.crypto.sasl.Plain;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Entry;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.utils.AccountUtils;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
+import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import eu.siacs.conversations.utils.SmsRetrieverWrapper;
+import eu.siacs.conversations.utils.TLSSocketFactory;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+
+import im.conversations.android.xmpp.model.stanza.Iq;
+
+import io.michaelrocks.libphonenumber.android.Phonenumber;
+
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
@@ -38,7 +58,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
-import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -52,26 +71,6 @@ import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.android.PhoneNumberContact;
-import eu.siacs.conversations.crypto.TrustManagers;
-import eu.siacs.conversations.crypto.sasl.Plain;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
-import eu.siacs.conversations.entities.Entry;
-import eu.siacs.conversations.http.HttpConnectionManager;
-import eu.siacs.conversations.utils.AccountUtils;
-import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
-import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
-import eu.siacs.conversations.utils.SmsRetrieverWrapper;
-import eu.siacs.conversations.utils.TLSSocketFactory;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.Jid;
-import im.conversations.android.xmpp.model.stanza.Iq;
-import io.michaelrocks.libphonenumber.android.Phonenumber;
-
public class QuickConversationsService extends AbstractQuickConversationsService {
@@ -88,8 +87,6 @@ public class QuickConversationsService extends AbstractQuickConversationsService
private static final String BASE_URL = "https://" + API_DOMAIN;
- private static final String INSTALLATION_ID = "eu.siacs.conversations.installation-id";
-
private final Set<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
private final Set<OnVerification> mOnVerification = Collections.newSetFromMap(new WeakHashMap<>());
@@ -308,16 +305,9 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
private String getInstallationId() {
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(service);
- String id = preferences.getString(INSTALLATION_ID, null);
- if (id != null) {
- return id;
- } else {
- id = UUID.randomUUID().toString();
- preferences.edit().putString(INSTALLATION_ID, id).apply();
- return id;
- }
-
+ final var appSettings = service.getAppSettings();
+ final long installationId = appSettings.getInstallationId();
+ return AccountUtils.createUuid4(installationId, installationId).toString();
}
private int getApiErrorCode(final Exception e) {