Merge tag '2.6.4' into develop
|
@ -1,5 +1,9 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
### Version 2.6.4
|
||||||
|
|
||||||
|
* Support automatic theme switching on Android 10
|
||||||
|
|
||||||
### Version 2.6.3
|
### Version 2.6.3
|
||||||
|
|
||||||
* Support for ?register and ?register;preauth XMPP uri parameters
|
* Support for ?register and ?register;preauth XMPP uri parameters
|
||||||
|
|
|
@ -85,13 +85,13 @@ ext {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 25
|
||||||
versionCode 360
|
versionCode 362
|
||||||
versionName "2.6.3"
|
versionName "2.6.4"
|
||||||
archivesBaseName += "-$versionName"
|
archivesBaseName += "-$versionName"
|
||||||
applicationId "eu.sum7.conversations"
|
applicationId "eu.sum7.conversations"
|
||||||
resValue "string", "applicationId", applicationId
|
resValue "string", "applicationId", applicationId
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
* Introduce expert setting to perform channel discovery on local server instead of search.jabber.network
|
||||||
|
* Enable delivery check marks by default and remove setting
|
||||||
|
* Enable ‘Send button indicates status’ by default and remove setting
|
||||||
|
* Move Backup and Foreground Service settings to main screen
|
|
@ -0,0 +1,3 @@
|
||||||
|
* fixes for Jingle IBB file transfer
|
||||||
|
* fixes for repeated corrections filling up the database
|
||||||
|
* switched to Last Message Correction v1.1
|
|
@ -0,0 +1,4 @@
|
||||||
|
* let users set their own nick name
|
||||||
|
* resume download of OMEMO encrypted files
|
||||||
|
* Channels now use '#' as symbol in avatar
|
||||||
|
* Quicksy uses 'always' as OMEMO encryption default (hides lock icon)
|
|
@ -0,0 +1 @@
|
||||||
|
* Support for ?register and ?register;preauth XMPP uri parameters
|
|
@ -0,0 +1,47 @@
|
||||||
|
Easy to use, reliable, battery friendly. With built-in support for images, group chats and e2e encryption.
|
||||||
|
|
||||||
|
Design principles:
|
||||||
|
|
||||||
|
* Be as beautiful and easy to use as possible without sacrificing security or privacy
|
||||||
|
* Rely on existing, well established protocols
|
||||||
|
* Do not require a Google Account or specifically Google Cloud Messaging (GCM)
|
||||||
|
* Require as few permissions as possible
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
* End-to-end encryption with either <a href="http://conversations.im/omemo/">OMEMO</a> or <a href="http://openpgp.org/about/">OpenPGP</a>
|
||||||
|
* Sending and receiving images
|
||||||
|
* Intuitive UI that follows Android Design guidelines
|
||||||
|
* Pictures / Avatars for your Contacts
|
||||||
|
* Syncs with desktop client
|
||||||
|
* Conferences (with support for bookmarks)
|
||||||
|
* Address book integration
|
||||||
|
* Multiple accounts / unified inbox
|
||||||
|
* Very low impact on battery life
|
||||||
|
|
||||||
|
Conversations makes it very easy to create an account on the conversations.im
|
||||||
|
server. Using that server comes with an annual fee of 8 Euro after a 6 month
|
||||||
|
trial period. However Conversations will work with any other XMPP server as
|
||||||
|
well. A lot of XMPP servers are run by volunteers and are free of charge.
|
||||||
|
|
||||||
|
XMPP Features:
|
||||||
|
|
||||||
|
Conversations works with every XMPP server out there. However XMPP is an
|
||||||
|
extensible protocol. These extensions are standardized as well in so called
|
||||||
|
XEP’s. Conversations supports a couple of those to make the overall user
|
||||||
|
experience better. There is a chance that your current XMPP server does not
|
||||||
|
support these extensions. Therefore to get the most out of Conversations you
|
||||||
|
should consider either switching to an XMPP server that does or - even better -
|
||||||
|
run your own XMPP server for you and your friends.
|
||||||
|
|
||||||
|
These XEPs are - as of now:
|
||||||
|
|
||||||
|
* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Will be used to transfer files if both parties are behind a firewall (NAT).
|
||||||
|
* XEP-0163: Personal Eventing Protocol for avatars
|
||||||
|
* XEP-0191: Blocking command lets you blacklist spammers or block contacts without removing them from your roster.
|
||||||
|
* XEP-0198: Stream Management allows XMPP to survive small network outages and changes of the underlying TCP connection.
|
||||||
|
* XEP-0280: Message Carbons which automatically syncs the messages you send to your desktop client and thus allows you to switch seamlessly from your mobile client to your desktop client and back within one conversation.
|
||||||
|
* XEP-0237: Roster Versioning mainly to save bandwidth on poor mobile connections
|
||||||
|
* XEP-0313: Message Archive Management synchronize message history with the server. Catch up with messages that were sent while Conversations was offline.
|
||||||
|
* XEP-0352: Client State Indication lets the server know whether or not Conversations is in the background. Allows the server to save bandwidth by withholding unimportant packages.
|
||||||
|
* XEP-0363: HTTP File Upload allows you to share files in conferences and with offline contacts. Requires an additional component on your server.
|
After Width: | Height: | Size: 414 KiB |
After Width: | Height: | Size: 449 KiB |
After Width: | Height: | Size: 475 KiB |
After Width: | Height: | Size: 339 KiB |
After Width: | Height: | Size: 284 KiB |
After Width: | Height: | Size: 168 KiB |
After Width: | Height: | Size: 418 KiB |
After Width: | Height: | Size: 391 KiB |
After Width: | Height: | Size: 382 KiB |
After Width: | Height: | Size: 86 KiB |
After Width: | Height: | Size: 372 KiB |
|
@ -0,0 +1 @@
|
||||||
|
An encrypted, user friendly XMPP instant messaging client optimized for mobile
|
|
@ -52,12 +52,12 @@ public class WelcomeActivity extends XmppActivity {
|
||||||
|
|
||||||
private boolean processXmppUri(final XmppUri xmppUri) {
|
private boolean processXmppUri(final XmppUri xmppUri) {
|
||||||
if (xmppUri.isValidJid()) {
|
if (xmppUri.isValidJid()) {
|
||||||
final String preauth = xmppUri.getParamater("preauth");
|
final String preauth = xmppUri.getParameter("preauth");
|
||||||
final Jid jid = xmppUri.getJid();
|
final Jid jid = xmppUri.getJid();
|
||||||
final Intent intent;
|
final Intent intent;
|
||||||
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
|
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
|
||||||
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preauth);
|
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preauth);
|
||||||
} else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParamater("ibr"))) {
|
} else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter("ibr"))) {
|
||||||
intent = SignupUtils.getTokenRegistrationIntent(this, Jid.ofDomain(jid.getDomain()), preauth);
|
intent = SignupUtils.getTokenRegistrationIntent(this, Jid.ofDomain(jid.getDomain()), preauth);
|
||||||
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
|
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,4 +5,7 @@
|
||||||
<string name="create_new_account">Új fiók létrehozása</string>
|
<string name="create_new_account">Új fiók létrehozása</string>
|
||||||
<string name="do_you_have_an_account">Már rendelkezik XMPP-fiókkal? Ez az eset állhat fenn, ha már egy másik XMPP-klienst használ, vagy ha már korábban használta a Conversations alkalmazást. Ha nem, akkor most létrehozhat egy új XMPP-fiókot.\nTipp: egyes e-mail szolgáltatók is biztosítanak XMPP-fiókokat.</string>
|
<string name="do_you_have_an_account">Már rendelkezik XMPP-fiókkal? Ez az eset állhat fenn, ha már egy másik XMPP-klienst használ, vagy ha már korábban használta a Conversations alkalmazást. Ha nem, akkor most létrehozhat egy új XMPP-fiókot.\nTipp: egyes e-mail szolgáltatók is biztosítanak XMPP-fiókokat.</string>
|
||||||
<string name="server_select_text">Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja.\nAzonban a kényelem érdekében megkönnyítettük a chat.sum7.eu szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve.</string>
|
<string name="server_select_text">Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja.\nAzonban a kényelem érdekében megkönnyítettük a chat.sum7.eu szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve.</string>
|
||||||
|
<string name="magic_create_text_on_x">Meghívást kapott a(z) %1$s kiszolgálóra. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nHa a(z) %1$s kiszolgálót választja szolgáltatóként, akkor képes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét.</string>
|
||||||
|
<string name="magic_create_text_fixed">Meghívást kapott a(z) %1$s kiszolgálóra. Már kiválasztottak Önnek egy felhasználónevet. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nKépes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét.</string>
|
||||||
|
<string name="your_server_invitation">Az Ön kiszolgálómeghívása</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -5,4 +5,7 @@
|
||||||
<string name="create_new_account">Stwórz nowe konto</string>
|
<string name="create_new_account">Stwórz nowe konto</string>
|
||||||
<string name="do_you_have_an_account">Czy masz już konto XMPP? Tak może być jeśli używasz już innego klienta XMPP lub używałeś już Conversations. Jeśli nie możesz stworzyć nowe konto XMPP teraz.\nPodpowiedź: Niektórzy dostawcy poczty oferują również konta XMPP.</string>
|
<string name="do_you_have_an_account">Czy masz już konto XMPP? Tak może być jeśli używasz już innego klienta XMPP lub używałeś już Conversations. Jeśli nie możesz stworzyć nowe konto XMPP teraz.\nPodpowiedź: Niektórzy dostawcy poczty oferują również konta XMPP.</string>
|
||||||
<string name="server_select_text">XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP.\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na chat.sum7.eu; dostawcy specjalnie dostosowanego do pracy z Conversations.</string>
|
<string name="server_select_text">XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP.\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na chat.sum7.eu; dostawcy specjalnie dostosowanego do pracy z Conversations.</string>
|
||||||
|
<string name="magic_create_text_on_x">Zostałeś zaproszony do %1$s. Poprowadzimy ciebie przez proces tworzenia konta.\nWybierając %1$s jako dostawcę będziesz mógł komunikować się z innymi użytkownikami podając swój pełny adres XMPP.</string>
|
||||||
|
<string name="magic_create_text_fixed">Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP.</string>
|
||||||
|
<string name="your_server_invitation">Zaproszenie twojego serwera</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -125,7 +125,7 @@ public final class Config {
|
||||||
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY * 5;
|
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY * 5;
|
||||||
public static final int MAM_MAX_MESSAGES = 750;
|
public static final int MAM_MAX_MESSAGES = 750;
|
||||||
|
|
||||||
public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE;
|
public static final ChatState DEFAULT_CHAT_STATE = ChatState.ACTIVE;
|
||||||
public static final int TYPING_TIMEOUT = 8;
|
public static final int TYPING_TIMEOUT = 8;
|
||||||
|
|
||||||
public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes
|
public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
|
@ -79,16 +79,37 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
private final Map<Jid, Boolean> fetchDeviceListStatus = new HashMap<>();
|
private final Map<Jid, Boolean> fetchDeviceListStatus = new HashMap<>();
|
||||||
private final HashMap<Jid, List<OnDeviceIdsFetched>> fetchDeviceIdsMap = new HashMap<>();
|
private final HashMap<Jid, List<OnDeviceIdsFetched>> fetchDeviceIdsMap = new HashMap<>();
|
||||||
private final SerialSingleThreadExecutor executor;
|
private final SerialSingleThreadExecutor executor;
|
||||||
|
private final Set<SignalProtocolAddress> healingAttempts = new HashSet<>();
|
||||||
|
private final HashSet<Integer> cleanedOwnDeviceIds = new HashSet<>();
|
||||||
|
private final Set<Integer> PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>();
|
||||||
private int numPublishTriesOnEmptyPep = 0;
|
private int numPublishTriesOnEmptyPep = 0;
|
||||||
private boolean pepBroken = false;
|
private boolean pepBroken = false;
|
||||||
private final Set<SignalProtocolAddress> healingAttempts = new HashSet<>();
|
|
||||||
private int lastDeviceListNotificationHash = 0;
|
private int lastDeviceListNotificationHash = 0;
|
||||||
private final HashSet<Integer> cleanedOwnDeviceIds = new HashSet<>();
|
|
||||||
private Set<XmppAxolotlSession> postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment
|
private Set<XmppAxolotlSession> postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment
|
||||||
private Set<SignalProtocolAddress> postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup
|
private Set<SignalProtocolAddress> postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup
|
||||||
|
|
||||||
private AtomicBoolean changeAccessMode = new AtomicBoolean(false);
|
private AtomicBoolean changeAccessMode = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
public AxolotlService(Account account, XmppConnectionService connectionService) {
|
||||||
|
if (account == null || connectionService == null) {
|
||||||
|
throw new IllegalArgumentException("account and service cannot be null");
|
||||||
|
}
|
||||||
|
if (Security.getProvider("BC") == null) {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
this.mXmppConnectionService = connectionService;
|
||||||
|
this.account = account;
|
||||||
|
this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
|
||||||
|
this.deviceIds = new HashMap<>();
|
||||||
|
this.messageCache = new HashMap<>();
|
||||||
|
this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
|
||||||
|
this.fetchStatusMap = new FetchStatusMap();
|
||||||
|
this.executor = new SerialSingleThreadExecutor("Axolotl");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getLogprefix(Account account) {
|
||||||
|
return LOGPREFIX + " (" + account.getJid().asBareJid().toString() + "): ";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAdvancedStreamFeaturesAvailable(Account account) {
|
public void onAdvancedStreamFeaturesAvailable(Account account) {
|
||||||
if (Config.supportOmemo()
|
if (Config.supportOmemo()
|
||||||
|
@ -145,172 +166,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class AxolotlAddressMap<T> {
|
|
||||||
protected Map<String, Map<Integer, T>> map;
|
|
||||||
protected final Object MAP_LOCK = new Object();
|
|
||||||
|
|
||||||
public AxolotlAddressMap() {
|
|
||||||
this.map = new HashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void put(SignalProtocolAddress address, T value) {
|
|
||||||
synchronized (MAP_LOCK) {
|
|
||||||
Map<Integer, T> devices = map.get(address.getName());
|
|
||||||
if (devices == null) {
|
|
||||||
devices = new HashMap<>();
|
|
||||||
map.put(address.getName(), devices);
|
|
||||||
}
|
|
||||||
devices.put(address.getDeviceId(), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public T get(SignalProtocolAddress address) {
|
|
||||||
synchronized (MAP_LOCK) {
|
|
||||||
Map<Integer, T> devices = map.get(address.getName());
|
|
||||||
if (devices == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return devices.get(address.getDeviceId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<Integer, T> getAll(String name) {
|
|
||||||
synchronized (MAP_LOCK) {
|
|
||||||
Map<Integer, T> devices = map.get(name);
|
|
||||||
if (devices == null) {
|
|
||||||
return new HashMap<>();
|
|
||||||
}
|
|
||||||
return devices;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasAny(SignalProtocolAddress address) {
|
|
||||||
synchronized (MAP_LOCK) {
|
|
||||||
Map<Integer, T> devices = map.get(address.getName());
|
|
||||||
return devices != null && !devices.isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
map.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
|
|
||||||
private final XmppConnectionService xmppConnectionService;
|
|
||||||
private final Account account;
|
|
||||||
|
|
||||||
public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
|
|
||||||
super();
|
|
||||||
this.xmppConnectionService = service;
|
|
||||||
this.account = account;
|
|
||||||
this.fillMap(store);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Jid> findCounterpartsForSourceId(Integer sid) {
|
|
||||||
Set<Jid> candidates = new HashSet<>();
|
|
||||||
synchronized (MAP_LOCK) {
|
|
||||||
for(Map.Entry<String,Map<Integer,XmppAxolotlSession>> entry : map.entrySet()) {
|
|
||||||
String key = entry.getKey();
|
|
||||||
if (entry.getValue().containsKey(sid)) {
|
|
||||||
candidates.add(Jid.of(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
|
|
||||||
for (Integer deviceId : deviceIds) {
|
|
||||||
SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId);
|
|
||||||
IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
|
|
||||||
if (Config.X509_VERIFICATION) {
|
|
||||||
X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()));
|
|
||||||
if (certificate != null) {
|
|
||||||
Bundle information = CryptoHelper.extractCertificateInformation(certificate);
|
|
||||||
try {
|
|
||||||
final String cn = information.getString("subject_cn");
|
|
||||||
final Jid jid = Jid.of(bareJid);
|
|
||||||
Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn);
|
|
||||||
account.getRoster().getContact(jid).setCommonName(cn);
|
|
||||||
} catch (final IllegalArgumentException ignored) {
|
|
||||||
//ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fillMap(SQLiteAxolotlStore store) {
|
|
||||||
List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().asBareJid().toString());
|
|
||||||
putDevicesForJid(account.getJid().asBareJid().toString(), deviceIds, store);
|
|
||||||
for (String address : store.getKnownAddresses()) {
|
|
||||||
deviceIds = store.getSubDeviceSessions(address);
|
|
||||||
putDevicesForJid(address, deviceIds, store);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void put(SignalProtocolAddress address, XmppAxolotlSession value) {
|
|
||||||
super.put(address, value);
|
|
||||||
value.setNotFresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void put(XmppAxolotlSession session) {
|
|
||||||
this.put(session.getRemoteAddress(), session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum FetchStatus {
|
|
||||||
PENDING,
|
|
||||||
SUCCESS,
|
|
||||||
SUCCESS_VERIFIED,
|
|
||||||
TIMEOUT,
|
|
||||||
SUCCESS_TRUSTED,
|
|
||||||
ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
|
|
||||||
|
|
||||||
public void clearErrorFor(Jid jid) {
|
|
||||||
synchronized (MAP_LOCK) {
|
|
||||||
Map<Integer, FetchStatus> devices = this.map.get(jid.asBareJid().toString());
|
|
||||||
if (devices == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (Map.Entry<Integer, FetchStatus> entry : devices.entrySet()) {
|
|
||||||
if (entry.getValue() == FetchStatus.ERROR) {
|
|
||||||
Log.d(Config.LOGTAG, "resetting error for " + jid.asBareJid() + "(" + entry.getKey() + ")");
|
|
||||||
entry.setValue(FetchStatus.TIMEOUT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getLogprefix(Account account) {
|
|
||||||
return LOGPREFIX + " (" + account.getJid().asBareJid().toString() + "): ";
|
|
||||||
}
|
|
||||||
|
|
||||||
public AxolotlService(Account account, XmppConnectionService connectionService) {
|
|
||||||
if (account == null || connectionService == null) {
|
|
||||||
throw new IllegalArgumentException("account and service cannot be null");
|
|
||||||
}
|
|
||||||
if (Security.getProvider("BC") == null) {
|
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
|
||||||
}
|
|
||||||
this.mXmppConnectionService = connectionService;
|
|
||||||
this.account = account;
|
|
||||||
this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
|
|
||||||
this.deviceIds = new HashMap<>();
|
|
||||||
this.messageCache = new HashMap<>();
|
|
||||||
this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
|
|
||||||
this.fetchStatusMap = new FetchStatusMap();
|
|
||||||
this.executor = new SerialSingleThreadExecutor("Axolotl");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getOwnFingerprint() {
|
public String getOwnFingerprint() {
|
||||||
return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize());
|
return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize());
|
||||||
}
|
}
|
||||||
|
@ -359,7 +214,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
|
public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
|
||||||
SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid());
|
SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid());
|
||||||
ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values());
|
ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values());
|
||||||
|
@ -924,8 +778,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Set<Integer> PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>();
|
|
||||||
|
|
||||||
private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) {
|
private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) {
|
||||||
SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0);
|
SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0);
|
||||||
Map<Integer, FetchStatus> own = fetchStatusMap.getAll(ownAddress.getName());
|
Map<Integer, FetchStatus> own = fetchStatusMap.getAll(ownAddress.getName());
|
||||||
|
@ -963,14 +815,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
return !hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty());
|
return !hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface OnDeviceIdsFetched {
|
|
||||||
void fetched(Jid jid, Set<Integer> deviceIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnMultipleDeviceIdFetched {
|
|
||||||
void fetched();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void fetchDeviceIds(final Jid jid) {
|
public void fetchDeviceIds(final Jid jid) {
|
||||||
fetchDeviceIds(jid, null);
|
fetchDeviceIds(jid, null);
|
||||||
}
|
}
|
||||||
|
@ -1047,11 +891,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OnSessionBuildFromPep {
|
|
||||||
void onSessionBuildSuccessful();
|
|
||||||
void onSessionBuildFailed();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildSessionFromPEP(final SignalProtocolAddress address) {
|
private void buildSessionFromPEP(final SignalProtocolAddress address) {
|
||||||
buildSessionFromPEP(address, null);
|
buildSessionFromPEP(address, null);
|
||||||
}
|
}
|
||||||
|
@ -1399,7 +1238,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
private XmppAxolotlSession getReceivingSession(SignalProtocolAddress senderAddress) {
|
private XmppAxolotlSession getReceivingSession(SignalProtocolAddress senderAddress) {
|
||||||
XmppAxolotlSession session = sessions.get(senderAddress);
|
XmppAxolotlSession session = sessions.get(senderAddress);
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
//Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message);
|
|
||||||
session = recreateUncachedSession(senderAddress);
|
session = recreateUncachedSession(senderAddress);
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
|
session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
|
||||||
|
@ -1408,7 +1246,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) throws NotEncryptedForThisDeviceException, BrokenSessionException {
|
public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) throws NotEncryptedForThisDeviceException, BrokenSessionException, OutdatedSenderException {
|
||||||
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
|
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
|
||||||
|
|
||||||
XmppAxolotlSession session = getReceivingSession(message);
|
XmppAxolotlSession session = getReceivingSession(message);
|
||||||
|
@ -1427,6 +1265,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
} catch (final BrokenSessionException e) {
|
} catch (final BrokenSessionException e) {
|
||||||
throw e;
|
throw e;
|
||||||
|
} catch (final OutdatedSenderException e) {
|
||||||
|
Log.e(Config.LOGTAG,account.getJid().asBareJid()+": "+e.getMessage());
|
||||||
|
throw e;
|
||||||
} catch (CryptoFailedException e) {
|
} catch (CryptoFailedException e) {
|
||||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), e);
|
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), e);
|
||||||
}
|
}
|
||||||
|
@ -1503,7 +1344,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean trustedOrPreviouslyResponded(XmppAxolotlSession session) {
|
private boolean trustedOrPreviouslyResponded(XmppAxolotlSession session) {
|
||||||
try {
|
try {
|
||||||
return trustedOrPreviouslyResponded(Jid.of(session.getRemoteAddress().getName()));
|
return trustedOrPreviouslyResponded(Jid.of(session.getRemoteAddress().getName()));
|
||||||
|
@ -1533,7 +1373,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) {
|
public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) {
|
||||||
final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
|
final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
|
||||||
final XmppAxolotlSession session = getReceivingSession(message);
|
final XmppAxolotlSession session = getReceivingSession(message);
|
||||||
|
@ -1565,4 +1404,164 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum FetchStatus {
|
||||||
|
PENDING,
|
||||||
|
SUCCESS,
|
||||||
|
SUCCESS_VERIFIED,
|
||||||
|
TIMEOUT,
|
||||||
|
SUCCESS_TRUSTED,
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnDeviceIdsFetched {
|
||||||
|
void fetched(Jid jid, Set<Integer> deviceIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public interface OnMultipleDeviceIdFetched {
|
||||||
|
void fetched();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnSessionBuildFromPep {
|
||||||
|
void onSessionBuildSuccessful();
|
||||||
|
|
||||||
|
void onSessionBuildFailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class AxolotlAddressMap<T> {
|
||||||
|
protected final Object MAP_LOCK = new Object();
|
||||||
|
protected Map<String, Map<Integer, T>> map;
|
||||||
|
|
||||||
|
public AxolotlAddressMap() {
|
||||||
|
this.map = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void put(SignalProtocolAddress address, T value) {
|
||||||
|
synchronized (MAP_LOCK) {
|
||||||
|
Map<Integer, T> devices = map.get(address.getName());
|
||||||
|
if (devices == null) {
|
||||||
|
devices = new HashMap<>();
|
||||||
|
map.put(address.getName(), devices);
|
||||||
|
}
|
||||||
|
devices.put(address.getDeviceId(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T get(SignalProtocolAddress address) {
|
||||||
|
synchronized (MAP_LOCK) {
|
||||||
|
Map<Integer, T> devices = map.get(address.getName());
|
||||||
|
if (devices == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return devices.get(address.getDeviceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Integer, T> getAll(String name) {
|
||||||
|
synchronized (MAP_LOCK) {
|
||||||
|
Map<Integer, T> devices = map.get(name);
|
||||||
|
if (devices == null) {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAny(SignalProtocolAddress address) {
|
||||||
|
synchronized (MAP_LOCK) {
|
||||||
|
Map<Integer, T> devices = map.get(address.getName());
|
||||||
|
return devices != null && !devices.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
map.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
|
||||||
|
private final XmppConnectionService xmppConnectionService;
|
||||||
|
private final Account account;
|
||||||
|
|
||||||
|
public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
|
||||||
|
super();
|
||||||
|
this.xmppConnectionService = service;
|
||||||
|
this.account = account;
|
||||||
|
this.fillMap(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Jid> findCounterpartsForSourceId(Integer sid) {
|
||||||
|
Set<Jid> candidates = new HashSet<>();
|
||||||
|
synchronized (MAP_LOCK) {
|
||||||
|
for (Map.Entry<String, Map<Integer, XmppAxolotlSession>> entry : map.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
if (entry.getValue().containsKey(sid)) {
|
||||||
|
candidates.add(Jid.of(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
|
||||||
|
for (Integer deviceId : deviceIds) {
|
||||||
|
SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId);
|
||||||
|
IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
|
||||||
|
if (Config.X509_VERIFICATION) {
|
||||||
|
X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()));
|
||||||
|
if (certificate != null) {
|
||||||
|
Bundle information = CryptoHelper.extractCertificateInformation(certificate);
|
||||||
|
try {
|
||||||
|
final String cn = information.getString("subject_cn");
|
||||||
|
final Jid jid = Jid.of(bareJid);
|
||||||
|
Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn);
|
||||||
|
account.getRoster().getContact(jid).setCommonName(cn);
|
||||||
|
} catch (final IllegalArgumentException ignored) {
|
||||||
|
//ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillMap(SQLiteAxolotlStore store) {
|
||||||
|
List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().asBareJid().toString());
|
||||||
|
putDevicesForJid(account.getJid().asBareJid().toString(), deviceIds, store);
|
||||||
|
for (String address : store.getKnownAddresses()) {
|
||||||
|
deviceIds = store.getSubDeviceSessions(address);
|
||||||
|
putDevicesForJid(address, deviceIds, store);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void put(SignalProtocolAddress address, XmppAxolotlSession value) {
|
||||||
|
super.put(address, value);
|
||||||
|
value.setNotFresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void put(XmppAxolotlSession session) {
|
||||||
|
this.put(session.getRemoteAddress(), session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
|
||||||
|
|
||||||
|
public void clearErrorFor(Jid jid) {
|
||||||
|
synchronized (MAP_LOCK) {
|
||||||
|
Map<Integer, FetchStatus> devices = this.map.get(jid.asBareJid().toString());
|
||||||
|
if (devices == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Map.Entry<Integer, FetchStatus> entry : devices.entrySet()) {
|
||||||
|
if (entry.getValue() == FetchStatus.ERROR) {
|
||||||
|
Log.d(Config.LOGTAG, "resetting error for " + jid.asBareJid() + "(" + entry.getKey() + ")");
|
||||||
|
entry.setValue(FetchStatus.TIMEOUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package eu.siacs.conversations.crypto.axolotl;
|
||||||
|
|
||||||
|
public class OutdatedSenderException extends CryptoFailedException {
|
||||||
|
|
||||||
|
public OutdatedSenderException(final String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,69 +38,13 @@ public class XmppAxolotlMessage {
|
||||||
private static final String KEYTYPE = "AES";
|
private static final String KEYTYPE = "AES";
|
||||||
private static final String CIPHERMODE = "AES/GCM/NoPadding";
|
private static final String CIPHERMODE = "AES/GCM/NoPadding";
|
||||||
private static final String PROVIDER = "BC";
|
private static final String PROVIDER = "BC";
|
||||||
|
private final List<XmppAxolotlSession.AxolotlKey> keys;
|
||||||
|
private final Jid from;
|
||||||
|
private final int sourceDeviceId;
|
||||||
private byte[] innerKey;
|
private byte[] innerKey;
|
||||||
private byte[] ciphertext = null;
|
private byte[] ciphertext = null;
|
||||||
private byte[] authtagPlusInnerKey = null;
|
private byte[] authtagPlusInnerKey = null;
|
||||||
private byte[] iv = null;
|
private byte[] iv = null;
|
||||||
private final List<XmppAxolotlSession.AxolotlKey> keys;
|
|
||||||
private final Jid from;
|
|
||||||
private final int sourceDeviceId;
|
|
||||||
|
|
||||||
public static class XmppAxolotlPlaintextMessage {
|
|
||||||
private final String plaintext;
|
|
||||||
private final String fingerprint;
|
|
||||||
|
|
||||||
XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) {
|
|
||||||
this.plaintext = plaintext;
|
|
||||||
this.fingerprint = fingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPlaintext() {
|
|
||||||
return plaintext;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public String getFingerprint() {
|
|
||||||
return fingerprint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class XmppAxolotlKeyTransportMessage {
|
|
||||||
private final String fingerprint;
|
|
||||||
private final byte[] key;
|
|
||||||
private final byte[] iv;
|
|
||||||
|
|
||||||
XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) {
|
|
||||||
this.fingerprint = fingerprint;
|
|
||||||
this.key = key;
|
|
||||||
this.iv = iv;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFingerprint() {
|
|
||||||
return fingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getKey() {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getIv() {
|
|
||||||
return iv;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int parseSourceId(final Element axolotlMessage) throws IllegalArgumentException {
|
|
||||||
final Element header = axolotlMessage.findChild(HEADER);
|
|
||||||
if (header == null) {
|
|
||||||
throw new IllegalArgumentException("No header found");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Integer.parseInt(header.getAttribute(SOURCEID));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new IllegalArgumentException("invalid source id");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
|
private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
|
||||||
this.from = from;
|
this.from = from;
|
||||||
|
@ -149,6 +93,18 @@ public class XmppAxolotlMessage {
|
||||||
this.innerKey = generateKey();
|
this.innerKey = generateKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int parseSourceId(final Element axolotlMessage) throws IllegalArgumentException {
|
||||||
|
final Element header = axolotlMessage.findChild(HEADER);
|
||||||
|
if (header == null) {
|
||||||
|
throw new IllegalArgumentException("No header found");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(header.getAttribute(SOURCEID));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException("invalid source id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static XmppAxolotlMessage fromElement(Element element, Jid from) {
|
public static XmppAxolotlMessage fromElement(Element element, Jid from) {
|
||||||
return new XmppAxolotlMessage(element, from);
|
return new XmppAxolotlMessage(element, from);
|
||||||
}
|
}
|
||||||
|
@ -171,6 +127,22 @@ public class XmppAxolotlMessage {
|
||||||
return iv;
|
return iv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte[] getPaddedBytes(String plaintext) {
|
||||||
|
int plainLength = plaintext.getBytes().length;
|
||||||
|
int pad = Math.max(64, (plainLength / 32 + 1) * 32) - plainLength;
|
||||||
|
SecureRandom random = new SecureRandom();
|
||||||
|
int left = random.nextInt(pad);
|
||||||
|
int right = pad - left;
|
||||||
|
StringBuilder builder = new StringBuilder(plaintext);
|
||||||
|
for (int i = 0; i < left; ++i) {
|
||||||
|
builder.insert(0, random.nextBoolean() ? "\t" : " ");
|
||||||
|
}
|
||||||
|
for (int i = 0; i < right; ++i) {
|
||||||
|
builder.append(random.nextBoolean() ? "\t" : " ");
|
||||||
|
}
|
||||||
|
return builder.toString().getBytes();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasPayload() {
|
public boolean hasPayload() {
|
||||||
return ciphertext != null;
|
return ciphertext != null;
|
||||||
}
|
}
|
||||||
|
@ -197,22 +169,6 @@ public class XmppAxolotlMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] getPaddedBytes(String plaintext) {
|
|
||||||
int plainLength = plaintext.getBytes().length;
|
|
||||||
int pad = Math.max(64,(plainLength / 32 + 1) * 32) - plainLength;
|
|
||||||
SecureRandom random = new SecureRandom();
|
|
||||||
int left = random.nextInt(pad);
|
|
||||||
int right = pad - left;
|
|
||||||
StringBuilder builder = new StringBuilder(plaintext);
|
|
||||||
for(int i = 0; i < left; ++i) {
|
|
||||||
builder.insert(0,random.nextBoolean() ? "\t" : " ");
|
|
||||||
}
|
|
||||||
for(int i = 0; i < right; ++i) {
|
|
||||||
builder.append(random.nextBoolean() ? "\t" : " ");
|
|
||||||
}
|
|
||||||
return builder.toString().getBytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Jid getFrom() {
|
public Jid getFrom() {
|
||||||
return this.from;
|
return this.from;
|
||||||
}
|
}
|
||||||
|
@ -288,19 +244,19 @@ public class XmppAxolotlMessage {
|
||||||
byte[] key = unpackKey(session, sourceDeviceId);
|
byte[] key = unpackKey(session, sourceDeviceId);
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
try {
|
try {
|
||||||
if (key.length >= 32) {
|
if (key.length < 32) {
|
||||||
int authtaglength = key.length - 16;
|
throw new OutdatedSenderException("Key did not contain auth tag. Sender needs to update their OMEMO client");
|
||||||
Log.d(Config.LOGTAG,"found auth tag as part of omemo key");
|
}
|
||||||
|
final int authTagLength = key.length - 16;
|
||||||
byte[] newCipherText = new byte[key.length - 16 + ciphertext.length];
|
byte[] newCipherText = new byte[key.length - 16 + ciphertext.length];
|
||||||
byte[] newKey = new byte[16];
|
byte[] newKey = new byte[16];
|
||||||
System.arraycopy(ciphertext, 0, newCipherText, 0, ciphertext.length);
|
System.arraycopy(ciphertext, 0, newCipherText, 0, ciphertext.length);
|
||||||
System.arraycopy(key, 16, newCipherText, ciphertext.length, authtaglength);
|
System.arraycopy(key, 16, newCipherText, ciphertext.length, authTagLength);
|
||||||
System.arraycopy(key, 0, newKey, 0, newKey.length);
|
System.arraycopy(key, 0, newKey, 0, newKey.length);
|
||||||
ciphertext = newCipherText;
|
ciphertext = newCipherText;
|
||||||
key = newKey;
|
key = newKey;
|
||||||
}
|
|
||||||
|
|
||||||
Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
||||||
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
||||||
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||||
|
|
||||||
|
@ -317,4 +273,47 @@ public class XmppAxolotlMessage {
|
||||||
}
|
}
|
||||||
return plaintextMessage;
|
return plaintextMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class XmppAxolotlPlaintextMessage {
|
||||||
|
private final String plaintext;
|
||||||
|
private final String fingerprint;
|
||||||
|
|
||||||
|
XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) {
|
||||||
|
this.plaintext = plaintext;
|
||||||
|
this.fingerprint = fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlaintext() {
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getFingerprint() {
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class XmppAxolotlKeyTransportMessage {
|
||||||
|
private final String fingerprint;
|
||||||
|
private final byte[] key;
|
||||||
|
private final byte[] iv;
|
||||||
|
|
||||||
|
XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) {
|
||||||
|
this.fingerprint = fingerprint;
|
||||||
|
this.key = key;
|
||||||
|
this.iv = iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFingerprint() {
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getIv() {
|
||||||
|
return iv;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,8 +76,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
private Jid nextCounterpart;
|
private Jid nextCounterpart;
|
||||||
private transient MucOptions mucOptions = null;
|
private transient MucOptions mucOptions = null;
|
||||||
private boolean messagesLeftOnServer = true;
|
private boolean messagesLeftOnServer = true;
|
||||||
private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
|
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
|
||||||
private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
|
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
|
||||||
private String mFirstMamReference = null;
|
private String mFirstMamReference = null;
|
||||||
|
|
||||||
public Conversation(final String name, final Account account, final Jid contactJid,
|
public Conversation(final String name, final Account account, final Jid contactJid,
|
||||||
|
|
|
@ -94,7 +94,7 @@ public class MucOptions {
|
||||||
public void resetChatState() {
|
public void resetChatState() {
|
||||||
synchronized (users) {
|
synchronized (users) {
|
||||||
for (User user : users) {
|
for (User user : users) {
|
||||||
user.chatState = Config.DEFAULT_CHATSTATE;
|
user.chatState = Config.DEFAULT_CHAT_STATE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -746,7 +746,7 @@ public class MucOptions {
|
||||||
private long pgpKeyId = 0;
|
private long pgpKeyId = 0;
|
||||||
private Avatar avatar;
|
private Avatar avatar;
|
||||||
private MucOptions options;
|
private MucOptions options;
|
||||||
private ChatState chatState = Config.DEFAULT_CHATSTATE;
|
private ChatState chatState = Config.DEFAULT_CHAT_STATE;
|
||||||
|
|
||||||
public User(MucOptions options, Jid fullJid) {
|
public User(MucOptions options, Jid fullJid) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|
|
@ -31,22 +31,20 @@ import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
import eu.siacs.conversations.utils.FileWriterException;
|
import eu.siacs.conversations.utils.FileWriterException;
|
||||||
import eu.siacs.conversations.utils.WakeLockHelper;
|
import eu.siacs.conversations.utils.WakeLockHelper;
|
||||||
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
|
|
||||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||||
import rocks.xmpp.addr.Jid;
|
import rocks.xmpp.addr.Jid;
|
||||||
|
|
||||||
public class HttpDownloadConnection implements Transferable {
|
public class HttpDownloadConnection implements Transferable {
|
||||||
|
|
||||||
|
private final Message message;
|
||||||
|
private final boolean mUseTor;
|
||||||
private HttpConnectionManager mHttpConnectionManager;
|
private HttpConnectionManager mHttpConnectionManager;
|
||||||
private XmppConnectionService mXmppConnectionService;
|
private XmppConnectionService mXmppConnectionService;
|
||||||
|
|
||||||
private URL mUrl;
|
private URL mUrl;
|
||||||
private final Message message;
|
|
||||||
private DownloadableFile file;
|
private DownloadableFile file;
|
||||||
private int mStatus = Transferable.STATUS_UNKNOWN;
|
private int mStatus = Transferable.STATUS_UNKNOWN;
|
||||||
private boolean acceptedAutomatically = false;
|
private boolean acceptedAutomatically = false;
|
||||||
private int mProgress = 0;
|
private int mProgress = 0;
|
||||||
private final boolean mUseTor;
|
|
||||||
private boolean canceled = false;
|
private boolean canceled = false;
|
||||||
private Method method = Method.HTTP_UPLOAD;
|
private Method method = Method.HTTP_UPLOAD;
|
||||||
|
|
||||||
|
@ -78,6 +76,7 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
} else if (message.isFileOrImage()) {
|
} else if (message.isFileOrImage()) {
|
||||||
message.setType(Message.TYPE_TEXT);
|
message.setType(Message.TYPE_TEXT);
|
||||||
}
|
}
|
||||||
|
message.setOob(true);
|
||||||
message.setDeleted(false);
|
message.setDeleted(false);
|
||||||
mXmppConnectionService.updateMessage(message);
|
mXmppConnectionService.updateMessage(message);
|
||||||
}
|
}
|
||||||
|
@ -105,16 +104,15 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
ext = extension.main;
|
ext = extension.main;
|
||||||
}
|
}
|
||||||
message.setRelativeFilePath(message.getUuid() + (ext != null ? ("." + ext) : ""));
|
message.setRelativeFilePath(message.getUuid() + (ext != null ? ("." + ext) : ""));
|
||||||
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
|
final String reference = mUrl.getRef();
|
||||||
|
if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) {
|
||||||
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
|
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
|
||||||
|
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
|
||||||
Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
|
Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
|
||||||
} else {
|
} else {
|
||||||
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
|
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
|
||||||
}
|
}
|
||||||
final String reference = mUrl.getRef();
|
|
||||||
if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) {
|
|
||||||
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
|
if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
|
||||||
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
||||||
|
@ -151,15 +149,17 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
mHttpConnectionManager.updateConversationUi(true);
|
mHttpConnectionManager.updateConversationUi(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void decryptOmemoFile() {
|
private void decryptFile() throws IOException {
|
||||||
final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
|
final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
|
||||||
|
|
||||||
if (outputFile.getParentFile().mkdirs()) {
|
if (outputFile.getParentFile().mkdirs()) {
|
||||||
Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath());
|
Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!outputFile.createNewFile()) {
|
||||||
outputFile.createNewFile();
|
Log.w(Config.LOGTAG, "unable to create output file " + outputFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
final InputStream is = new FileInputStream(this.file);
|
final InputStream is = new FileInputStream(this.file);
|
||||||
|
|
||||||
outputFile.setKey(this.file.getKey());
|
outputFile.setKey(this.file.getKey());
|
||||||
|
@ -174,12 +174,6 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
if (!file.delete()) {
|
if (!file.delete()) {
|
||||||
Log.w(Config.LOGTAG, "unable to delete temporary OMEMO encrypted file " + file.getAbsolutePath());
|
Log.w(Config.LOGTAG, "unable to delete temporary OMEMO encrypted file " + file.getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
message.setRelativeFilePath(outputFile.getPath());
|
|
||||||
} catch (IOException e) {
|
|
||||||
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
|
|
||||||
mXmppConnectionService.updateMessage(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finish() {
|
private void finish() {
|
||||||
|
@ -199,9 +193,9 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void decryptIfNeeded() {
|
private void decryptIfNeeded() throws IOException {
|
||||||
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
|
if (file.getKey() != null && file.getIv() != null) {
|
||||||
decryptOmemoFile();
|
decryptFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -491,7 +485,7 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
throw new FileWriterException();
|
throw new FileWriterException();
|
||||||
}
|
}
|
||||||
} catch (CancellationException | IOException e) {
|
} catch (CancellationException | IOException e) {
|
||||||
Log.d(Config.LOGTAG, "http download failed " + e.getMessage());
|
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": http download failed", e);
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
FileBackend.close(os);
|
FileBackend.close(os);
|
||||||
|
|
|
@ -19,6 +19,7 @@ import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||||
import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
|
import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
|
||||||
import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
|
import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
|
||||||
|
import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException;
|
||||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
|
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Bookmark;
|
import eu.siacs.conversations.entities.Bookmark;
|
||||||
|
@ -140,6 +141,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
}
|
}
|
||||||
} catch (NotEncryptedForThisDeviceException e) {
|
} catch (NotEncryptedForThisDeviceException e) {
|
||||||
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
|
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
|
||||||
|
} catch (OutdatedSenderException e) {
|
||||||
|
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
|
||||||
}
|
}
|
||||||
if (plaintextMessage != null) {
|
if (plaintextMessage != null) {
|
||||||
Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
|
Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
|
||||||
|
|
|
@ -508,6 +508,8 @@ public class FileBackend {
|
||||||
return getFile(message, true);
|
return getFile(message, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public DownloadableFile getFileForPath(String path) {
|
public DownloadableFile getFileForPath(String path) {
|
||||||
return getFileForPath(path, MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(path)));
|
return getFileForPath(path, MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(path)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,6 @@ import android.provider.ContactsContract;
|
||||||
import android.security.KeyChain;
|
import android.security.KeyChain;
|
||||||
import android.support.annotation.BoolRes;
|
import android.support.annotation.BoolRes;
|
||||||
import android.support.annotation.IntegerRes;
|
import android.support.annotation.IntegerRes;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.v4.app.RemoteInput;
|
import android.support.v4.app.RemoteInput;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -1515,7 +1514,7 @@ public class XmppConnectionService extends Service {
|
||||||
if (delay) {
|
if (delay) {
|
||||||
mMessageGenerator.addDelay(packet, message.getTimeSent());
|
mMessageGenerator.addDelay(packet, message.getTimeSent());
|
||||||
}
|
}
|
||||||
if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
|
if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
|
||||||
if (this.sendChatStates()) {
|
if (this.sendChatStates()) {
|
||||||
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
|
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
|
||||||
}
|
}
|
||||||
|
@ -1855,6 +1854,9 @@ public class XmppConnectionService extends Service {
|
||||||
for (Conversation conversation : getConversations()) {
|
for (Conversation conversation : getConversations()) {
|
||||||
deleted |= conversation.markAsDeleted(uuids);
|
deleted |= conversation.markAsDeleted(uuids);
|
||||||
}
|
}
|
||||||
|
for(final String uuid : uuids) {
|
||||||
|
evictPreview(uuid);
|
||||||
|
}
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
updateConversationUi();
|
updateConversationUi();
|
||||||
}
|
}
|
||||||
|
@ -2514,7 +2516,7 @@ public class XmppConnectionService extends Service {
|
||||||
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||||
conversation.getMucOptions().resetChatState();
|
conversation.getMucOptions().resetChatState();
|
||||||
} else {
|
} else {
|
||||||
conversation.setIncomingChatState(Config.DEFAULT_CHATSTATE);
|
conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Account account : getAccounts()) {
|
for (Account account : getAccounts()) {
|
||||||
|
@ -4576,6 +4578,12 @@ public class XmppConnectionService extends Service {
|
||||||
sendIqPacket(account, set, null);
|
sendIqPacket(account, set, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void evictPreview(String uuid) {
|
||||||
|
if (mBitmapCache.remove(uuid) != null) {
|
||||||
|
Log.d(Config.LOGTAG,"deleted cached preview");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public interface OnMamPreferencesFetched {
|
public interface OnMamPreferencesFetched {
|
||||||
void onPreferencesFetched(Element prefs);
|
void onPreferencesFetched(Element prefs);
|
||||||
|
|
||||||
|
|
|
@ -1653,6 +1653,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
|
builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
|
||||||
if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
|
if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
|
||||||
message.setDeleted(true);
|
message.setDeleted(true);
|
||||||
|
activity.xmppConnectionService.evictPreview(message.getUuid());
|
||||||
activity.xmppConnectionService.updateMessage(message, false);
|
activity.xmppConnectionService.updateMessage(message, false);
|
||||||
activity.onConversationsListItemUpdated();
|
activity.onConversationsListItemUpdated();
|
||||||
refresh();
|
refresh();
|
||||||
|
@ -1721,7 +1722,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
|
|
||||||
public void privateMessageWith(final Jid counterpart) {
|
public void privateMessageWith(final Jid counterpart) {
|
||||||
if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
|
if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
|
||||||
activity.xmppConnectionService.sendChatState(conversation);
|
activity.xmppConnectionService.sendChatState(conversation);
|
||||||
}
|
}
|
||||||
this.binding.textinput.setText("");
|
this.binding.textinput.setText("");
|
||||||
|
@ -1859,7 +1860,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateChatState(final Conversation conversation, final String msg) {
|
private void updateChatState(final Conversation conversation, final String msg) {
|
||||||
ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED;
|
ChatState state = msg.length() == 0 ? Config.DEFAULT_CHAT_STATE : ChatState.PAUSED;
|
||||||
Account.State status = conversation.getAccount().getStatus();
|
Account.State status = conversation.getAccount().getStatus();
|
||||||
if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) {
|
if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) {
|
||||||
activity.xmppConnectionService.sendChatState(conversation);
|
activity.xmppConnectionService.sendChatState(conversation);
|
||||||
|
@ -2619,7 +2620,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Account.State status = conversation.getAccount().getStatus();
|
Account.State status = conversation.getAccount().getStatus();
|
||||||
if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
|
if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
|
||||||
service.sendChatState(conversation);
|
service.sendChatState(conversation);
|
||||||
}
|
}
|
||||||
if (storeNextMessage()) {
|
if (storeNextMessage()) {
|
||||||
|
|
|
@ -88,7 +88,7 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(true);
|
final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(true);
|
||||||
|
|
||||||
if (SignupUtils.isSupportTokenRegistry() && xmppUri.isValidJid()) {
|
if (SignupUtils.isSupportTokenRegistry() && xmppUri.isValidJid()) {
|
||||||
final String preauth = xmppUri.getParamater("preauth");
|
final String preauth = xmppUri.getParameter("preauth");
|
||||||
final Jid jid = xmppUri.getJid();
|
final Jid jid = xmppUri.getJid();
|
||||||
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
|
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
|
||||||
if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) {
|
if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) {
|
||||||
|
@ -99,7 +99,7 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParamater("ibr"))) {
|
if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter("ibr"))) {
|
||||||
intent = SignupUtils.getTokenRegistrationIntent(this, Jid.ofDomain(jid.getDomain()), preauth);
|
intent = SignupUtils.getTokenRegistrationIntent(this, Jid.ofDomain(jid.getDomain()), preauth);
|
||||||
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
|
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
|
|
|
@ -31,8 +31,10 @@ package eu.siacs.conversations.utils;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.StyleRes;
|
import android.support.annotation.StyleRes;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
|
@ -45,10 +47,10 @@ import eu.siacs.conversations.ui.SettingsActivity;
|
||||||
|
|
||||||
public class ThemeHelper {
|
public class ThemeHelper {
|
||||||
|
|
||||||
public static int find(Context context) {
|
public static int find(final Context context) {
|
||||||
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final Resources resources = context.getResources();
|
final Resources resources = context.getResources();
|
||||||
final boolean dark = sharedPreferences.getString(SettingsActivity.THEME, resources.getString(R.string.theme)).equals("dark");
|
final boolean dark = isDark(sharedPreferences, resources);
|
||||||
final String fontSize = sharedPreferences.getString("font_size", resources.getString(R.string.default_font_size));
|
final String fontSize = sharedPreferences.getString("font_size", resources.getString(R.string.default_font_size));
|
||||||
switch (fontSize) {
|
switch (fontSize) {
|
||||||
case "medium":
|
case "medium":
|
||||||
|
@ -63,7 +65,7 @@ public class ThemeHelper {
|
||||||
public static int findDialog(Context context) {
|
public static int findDialog(Context context) {
|
||||||
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final Resources resources = context.getResources();
|
final Resources resources = context.getResources();
|
||||||
final boolean dark = sharedPreferences.getString(SettingsActivity.THEME, resources.getString(R.string.theme)).equals("dark");
|
final boolean dark = isDark(sharedPreferences, resources);
|
||||||
final String fontSize = sharedPreferences.getString("font_size", resources.getString(R.string.default_font_size));
|
final String fontSize = sharedPreferences.getString("font_size", resources.getString(R.string.default_font_size));
|
||||||
switch (fontSize) {
|
switch (fontSize) {
|
||||||
case "medium":
|
case "medium":
|
||||||
|
@ -75,6 +77,15 @@ public class ThemeHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isDark(final SharedPreferences sharedPreferences, final Resources resources) {
|
||||||
|
final String setting = sharedPreferences.getString(SettingsActivity.THEME, resources.getString(R.string.theme));
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && "automatic".equals(setting)) {
|
||||||
|
return (resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
|
||||||
|
} else {
|
||||||
|
return "dark".equals(setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isDark(@StyleRes int id) {
|
public static boolean isDark(@StyleRes int id) {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case R.style.ConversationsTheme_Dark:
|
case R.style.ConversationsTheme_Dark:
|
||||||
|
|
|
@ -18,19 +18,17 @@ import rocks.xmpp.addr.Jid;
|
||||||
|
|
||||||
public class XmppUri {
|
public class XmppUri {
|
||||||
|
|
||||||
|
public static final String ACTION_JOIN = "join";
|
||||||
|
public static final String ACTION_MESSAGE = "message";
|
||||||
|
public static final String ACTION_REGISTER = "register";
|
||||||
|
public static final String ACTION_ROSTER = "roster";
|
||||||
|
private static final String OMEMO_URI_PARAM = "omemo-sid-";
|
||||||
protected Uri uri;
|
protected Uri uri;
|
||||||
protected String jid;
|
protected String jid;
|
||||||
private List<Fingerprint> fingerprints = new ArrayList<>();
|
private List<Fingerprint> fingerprints = new ArrayList<>();
|
||||||
private Map<String, String> parameters = Collections.emptyMap();
|
private Map<String, String> parameters = Collections.emptyMap();
|
||||||
private boolean safeSource = true;
|
private boolean safeSource = true;
|
||||||
|
|
||||||
private static final String OMEMO_URI_PARAM = "omemo-sid-";
|
|
||||||
|
|
||||||
public static final String ACTION_JOIN = "join";
|
|
||||||
public static final String ACTION_MESSAGE = "message";
|
|
||||||
public static final String ACTION_REGISTER = "register";
|
|
||||||
public static final String ACTION_ROSTER = "roster";
|
|
||||||
|
|
||||||
public XmppUri(String uri) {
|
public XmppUri(String uri) {
|
||||||
try {
|
try {
|
||||||
parse(Uri.parse(uri));
|
parse(Uri.parse(uri));
|
||||||
|
@ -52,6 +50,77 @@ public class XmppUri {
|
||||||
parse(uri);
|
parse(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> parseParameters(final String query, final char seperator) {
|
||||||
|
final ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
|
||||||
|
final String[] pairs = query == null ? new String[0] : query.split(String.valueOf(seperator));
|
||||||
|
for (String pair : pairs) {
|
||||||
|
final String[] parts = pair.split("=", 2);
|
||||||
|
if (parts.length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String key = parts[0].toLowerCase(Locale.US);
|
||||||
|
final String value;
|
||||||
|
if (parts.length == 2) {
|
||||||
|
String decoded;
|
||||||
|
try {
|
||||||
|
decoded = URLDecoder.decode(parts[1], "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
decoded = "";
|
||||||
|
}
|
||||||
|
value = decoded;
|
||||||
|
} else {
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
builder.put(key, value);
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Fingerprint> parseFingerprints(Map<String, String> parameters) {
|
||||||
|
ImmutableList.Builder<Fingerprint> builder = new ImmutableList.Builder<>();
|
||||||
|
for (Map.Entry<String, String> parameter : parameters.entrySet()) {
|
||||||
|
final String key = parameter.getKey();
|
||||||
|
final String value = parameter.getValue().toLowerCase(Locale.US);
|
||||||
|
if (key.startsWith(OMEMO_URI_PARAM)) {
|
||||||
|
try {
|
||||||
|
final int id = Integer.parseInt(key.substring(OMEMO_URI_PARAM.length()));
|
||||||
|
builder.add(new Fingerprint(FingerprintType.OMEMO, value, id));
|
||||||
|
} catch (Exception e) {
|
||||||
|
//ignoring invalid device id
|
||||||
|
}
|
||||||
|
} else if ("omemo".equals(key)) {
|
||||||
|
builder.add(new Fingerprint(FingerprintType.OMEMO, value, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getFingerprintUri(final String base, final List<XmppUri.Fingerprint> fingerprints, char separator) {
|
||||||
|
final StringBuilder builder = new StringBuilder(base);
|
||||||
|
builder.append('?');
|
||||||
|
for (int i = 0; i < fingerprints.size(); ++i) {
|
||||||
|
XmppUri.FingerprintType type = fingerprints.get(i).type;
|
||||||
|
if (type == XmppUri.FingerprintType.OMEMO) {
|
||||||
|
builder.append(XmppUri.OMEMO_URI_PARAM);
|
||||||
|
builder.append(fingerprints.get(i).deviceId);
|
||||||
|
}
|
||||||
|
builder.append('=');
|
||||||
|
builder.append(fingerprints.get(i).fingerprint);
|
||||||
|
if (i != fingerprints.size() - 1) {
|
||||||
|
builder.append(separator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String lameUrlDecode(String url) {
|
||||||
|
return url.replace("%23", "#").replace("%25", "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String lameUrlEncode(String url) {
|
||||||
|
return url.replace("%", "%25").replace("#", "%23");
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSafeSource() {
|
public boolean isSafeSource() {
|
||||||
return safeSource;
|
return safeSource;
|
||||||
}
|
}
|
||||||
|
@ -111,33 +180,6 @@ public class XmppUri {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static Map<String,String> parseParameters(final String query, final char seperator) {
|
|
||||||
final ImmutableMap.Builder<String,String> builder = new ImmutableMap.Builder<>();
|
|
||||||
final String[] pairs = query == null ? new String[0] : query.split(String.valueOf(seperator));
|
|
||||||
for (String pair : pairs) {
|
|
||||||
final String[] parts = pair.split("=", 2);
|
|
||||||
if (parts.length == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final String key = parts[0].toLowerCase(Locale.US);
|
|
||||||
final String value;
|
|
||||||
if (parts.length == 2) {
|
|
||||||
String decoded;
|
|
||||||
try {
|
|
||||||
decoded = URLDecoder.decode(parts[1],"UTF-8");
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
decoded = "";
|
|
||||||
}
|
|
||||||
value = decoded;
|
|
||||||
} else {
|
|
||||||
value = "";
|
|
||||||
}
|
|
||||||
builder.put(key, value);
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
@ -147,23 +189,6 @@ public class XmppUri {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Fingerprint> parseFingerprints(Map<String,String> parameters) {
|
|
||||||
ImmutableList.Builder<Fingerprint> builder = new ImmutableList.Builder<>();
|
|
||||||
for (Map.Entry<String, String> parameter : parameters.entrySet()) {
|
|
||||||
final String key = parameter.getKey();
|
|
||||||
final String value = parameter.getValue().toLowerCase(Locale.US);
|
|
||||||
if (key.startsWith(OMEMO_URI_PARAM)) {
|
|
||||||
try {
|
|
||||||
final int id = Integer.parseInt(key.substring(OMEMO_URI_PARAM.length()));
|
|
||||||
builder.add(new Fingerprint(FingerprintType.OMEMO, value, id));
|
|
||||||
} catch (Exception e) {
|
|
||||||
//ignoring invalid device id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAction(final String action) {
|
public boolean isAction(final String action) {
|
||||||
return parameters.containsKey(action);
|
return parameters.containsKey(action);
|
||||||
}
|
}
|
||||||
|
@ -196,7 +221,7 @@ public class XmppUri {
|
||||||
return parameters.get("name");
|
return parameters.get("name");
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getParamater(String key) {
|
public String getParameter(String key) {
|
||||||
return this.parameters.get(key);
|
return this.parameters.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,24 +237,6 @@ public class XmppUri {
|
||||||
OMEMO
|
OMEMO
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFingerprintUri(String base, List<XmppUri.Fingerprint> fingerprints, char separator) {
|
|
||||||
StringBuilder builder = new StringBuilder(base);
|
|
||||||
builder.append('?');
|
|
||||||
for (int i = 0; i < fingerprints.size(); ++i) {
|
|
||||||
XmppUri.FingerprintType type = fingerprints.get(i).type;
|
|
||||||
if (type == XmppUri.FingerprintType.OMEMO) {
|
|
||||||
builder.append(XmppUri.OMEMO_URI_PARAM);
|
|
||||||
builder.append(fingerprints.get(i).deviceId);
|
|
||||||
}
|
|
||||||
builder.append('=');
|
|
||||||
builder.append(fingerprints.get(i).fingerprint);
|
|
||||||
if (i != fingerprints.size() - 1) {
|
|
||||||
builder.append(separator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Fingerprint {
|
public static class Fingerprint {
|
||||||
public final FingerprintType type;
|
public final FingerprintType type;
|
||||||
public final String fingerprint;
|
public final String fingerprint;
|
||||||
|
@ -247,12 +254,4 @@ public class XmppUri {
|
||||||
return type.toString() + ": " + fingerprint + (deviceId != 0 ? " " + deviceId : "");
|
return type.toString() + ": " + fingerprint + (deviceId != 0 ? " " + deviceId : "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String lameUrlDecode(String url) {
|
|
||||||
return url.replace("%23", "#").replace("%25", "%");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String lameUrlEncode(String url) {
|
|
||||||
return url.replace("%", "%25").replace("#", "%23");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,6 +155,7 @@
|
||||||
<string name="account_status_regis_conflict">A felhasználónév már használatban van</string>
|
<string name="account_status_regis_conflict">A felhasználónév már használatban van</string>
|
||||||
<string name="account_status_regis_success">Regisztráció befejezve</string>
|
<string name="account_status_regis_success">Regisztráció befejezve</string>
|
||||||
<string name="account_status_regis_not_sup">A kiszolgáló nem támogatja a regisztrációt</string>
|
<string name="account_status_regis_not_sup">A kiszolgáló nem támogatja a regisztrációt</string>
|
||||||
|
<string name="account_status_regis_invalid_token">Érvénytelen regisztrációs token</string>
|
||||||
<string name="account_status_tls_error">A TLS-egyeztetés sikertelen</string>
|
<string name="account_status_tls_error">A TLS-egyeztetés sikertelen</string>
|
||||||
<string name="account_status_policy_violation">Irányelv megsértése</string>
|
<string name="account_status_policy_violation">Irányelv megsértése</string>
|
||||||
<string name="account_status_incompatible_server">Nem kompatibilis kiszolgáló</string>
|
<string name="account_status_incompatible_server">Nem kompatibilis kiszolgáló</string>
|
||||||
|
@ -879,4 +880,8 @@
|
||||||
<string name="pref_channel_discovery">Csatornafelderítés módszere</string>
|
<string name="pref_channel_discovery">Csatornafelderítés módszere</string>
|
||||||
<string name="backup">Biztonsági mentés</string>
|
<string name="backup">Biztonsági mentés</string>
|
||||||
<string name="category_about">Névjegy</string>
|
<string name="category_about">Névjegy</string>
|
||||||
|
<plurals name="view_users">
|
||||||
|
<item quantity="one">%1$d résztvevő megtekintése</item>
|
||||||
|
<item quantity="other">%1$d résztvevő megtekintése</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -155,6 +155,7 @@
|
||||||
<string name="account_status_regis_conflict">Nazwa jest już w użyciu</string>
|
<string name="account_status_regis_conflict">Nazwa jest już w użyciu</string>
|
||||||
<string name="account_status_regis_success">Zarejestrowano pomyślnie</string>
|
<string name="account_status_regis_success">Zarejestrowano pomyślnie</string>
|
||||||
<string name="account_status_regis_not_sup">Serwer nie umożliwia rejestracji</string>
|
<string name="account_status_regis_not_sup">Serwer nie umożliwia rejestracji</string>
|
||||||
|
<string name="account_status_regis_invalid_token">Nieprawidłowy żeton rejestracji</string>
|
||||||
<string name="account_status_tls_error">Nie powiodła się negocjacja TLS</string>
|
<string name="account_status_tls_error">Nie powiodła się negocjacja TLS</string>
|
||||||
<string name="account_status_policy_violation">Naruszenie zasad</string>
|
<string name="account_status_policy_violation">Naruszenie zasad</string>
|
||||||
<string name="account_status_incompatible_server">Serwer niekompatybilny</string>
|
<string name="account_status_incompatible_server">Serwer niekompatybilny</string>
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<string name="theme">automatic</string>
|
||||||
|
<string-array name="themes" tools:ignore="InconsistentArrays">
|
||||||
|
<item>@string/pref_theme_automatic</item>
|
||||||
|
<item>@string/pref_theme_light</item>
|
||||||
|
<item>@string/pref_theme_dark</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="themes_values" tools:ignore="InconsistentArrays">
|
||||||
|
<item>automatic</item>
|
||||||
|
<item>light</item>
|
||||||
|
<item>dark</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -31,7 +31,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="pref_about_message" translatable="false">
|
<string name="pref_about_message" translatable="false">
|
||||||
Conversations • the very last word in instant messaging.
|
Conversations • the very last word in instant messaging.
|
||||||
\n\nCopyright © 2014-2019 Daniel Gultsch
|
\n\nCopyright © 2014-2020 Daniel Gultsch
|
||||||
\n\nThis program is free software: you can redistribute it and/or modify
|
\n\nThis program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string-array name="themes">
|
|
||||||
<item>@string/pref_theme_light</item>
|
|
||||||
<item>@string/pref_theme_dark</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="themes_values">
|
|
||||||
<item>light</item>
|
|
||||||
<item>dark</item>
|
|
||||||
</string-array>
|
|
||||||
<string-array name="filesizes">
|
<string-array name="filesizes">
|
||||||
<item>@string/never</item>
|
<item>@string/never</item>
|
||||||
<item>256 KiB</item>
|
<item>256 KiB</item>
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
<integer name="grace_period">144</integer>
|
<integer name="grace_period">144</integer>
|
||||||
<integer name="auto_accept_filesize">524288</integer>
|
<integer name="auto_accept_filesize">524288</integer>
|
||||||
<string name="picture_compression">auto</string>
|
<string name="picture_compression">auto</string>
|
||||||
<string name="theme">light</string>
|
|
||||||
<string name="quick_action">recent</string>
|
<string name="quick_action">recent</string>
|
||||||
<bool name="show_dynamic_tags">true</bool>
|
<bool name="show_dynamic_tags">true</bool>
|
||||||
<bool name="btbv">true</bool>
|
<bool name="btbv">true</bool>
|
||||||
|
|
|
@ -559,6 +559,7 @@
|
||||||
<string name="pref_privacy">Privacy</string>
|
<string name="pref_privacy">Privacy</string>
|
||||||
<string name="pref_theme_options">Theme</string>
|
<string name="pref_theme_options">Theme</string>
|
||||||
<string name="pref_theme_options_summary">Select the color palette</string>
|
<string name="pref_theme_options_summary">Select the color palette</string>
|
||||||
|
<string name="pref_theme_automatic">Automatic</string>
|
||||||
<string name="pref_theme_light">Light theme</string>
|
<string name="pref_theme_light">Light theme</string>
|
||||||
<string name="pref_theme_dark">Dark theme</string>
|
<string name="pref_theme_dark">Dark theme</string>
|
||||||
<string name="unable_to_connect_to_keychain">Unable to connect to OpenKeychain</string>
|
<string name="unable_to_connect_to_keychain">Unable to connect to OpenKeychain</string>
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="theme">light</string>
|
||||||
|
<string-array name="themes">
|
||||||
|
<item>@string/pref_theme_light</item>
|
||||||
|
<item>@string/pref_theme_dark</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="themes_values">
|
||||||
|
<item>light</item>
|
||||||
|
<item>dark</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
</resources>
|