diff --git a/.travis.yml b/.travis.yml index ac5f1ff91..d2392afab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ android: - '.+' before_script: - mkdir libs - - wget -O libs/libwebrtc-m81.aar http://gultsch.de/files/libwebrtc-m81.aar + - wget -O libs/libwebrtc-m83.aar http://gultsch.de/files/libwebrtc-m83.aar script: - ./gradlew assembleConversationsFreeSystemRelease - ./gradlew assembleQuicksyFreeCompatRelease diff --git a/CHANGELOG.md b/CHANGELOG.md index a363926b2..ce7f4430e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### Version 2.8.7 + +* Show help button if A/V call fails +* Fixed some annoying crashes +* Fixed Jingle connections (file transfer + calls) with bare JIDs + ### Version 2.8.6 * Offer to record voice message when callee is busy diff --git a/build.gradle b/build.gradle index 40ba513ae..a6c770309 100644 --- a/build.gradle +++ b/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:3.12.12' implementation 'com.google.guava:guava:27.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1' - //implementation fileTree(include: ['libwebrtc-m81.aar'], dir: 'libs') + //implementation fileTree(include: ['libwebrtc-m83.aar'], dir: 'libs') implementation 'org.webrtc:google-webrtc:1.0.+' } @@ -96,8 +96,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 25 - versionCode 390 - versionName "2.8.6" + versionCode 393 + versionName "2.8.7" archivesBaseName += "-$versionName" applicationId "eu.sum7.conversations" resValue "string", "applicationId", applicationId diff --git a/metadata/en-US/changelogs/394.txt b/metadata/en-US/changelogs/394.txt new file mode 100644 index 000000000..18107a020 --- /dev/null +++ b/metadata/en-US/changelogs/394.txt @@ -0,0 +1,3 @@ +• Show help button if A/V call fails +• Fixed some annoying crashes +• Fixed Jingle connections (file transfer + calls) with bare JIDs diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index d8a2b8a54..6a5d90690 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -1,6 +1,7 @@ package eu.siacs.conversations; import android.graphics.Bitmap; +import android.net.Uri; import java.util.Collections; import java.util.List; @@ -35,11 +36,11 @@ public final class Config { public static final String LOGTAG = BuildConfig.LOGTAG; public static final Jid BUG_REPORTS = Jid.of("bugs@chat.sum7.eu"); - + public static final Uri HELP = Uri.parse("https://sum7.eu/chat"); public static final String DOMAIN_LOCK = null; //only allow account creation for this domain public static final String MAGIC_CREATE_DOMAIN = "chat.sum7.eu"; - public static final String QUICKSY_DOMAIN = "quicksy.im"; + public static final Jid QUICKSY_DOMAIN = Jid.of("quicksy.im"); public static final String CHANNEL_DISCOVERY = "https://search.jabber.network"; diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index d37646f78..7789a6686 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -134,7 +134,7 @@ public class Contact implements ListItem, Blockable { return this.systemName; } else if (!TextUtils.isEmpty(this.serverName)) { return this.serverName; - } else if (!TextUtils.isEmpty(this.presenceName) && ((QuickConversationsService.isQuicksy() && Config.QUICKSY_DOMAIN.equals(jid.getDomain().toEscapedString())) ||mutualPresenceSubscription())) { + } else if (!TextUtils.isEmpty(this.presenceName) && ((QuickConversationsService.isQuicksy() && JidHelper.isQuicksyDomain(jid.getDomain())) ||mutualPresenceSubscription())) { return this.presenceName; } else if (jid.getLocal() != null) { return JidHelper.localPartOrFallback(jid); diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 1e88f8d82..83e49b9a5 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -1006,7 +1006,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl && !contact.isOwnServer() && !contact.showInContactList() && !contact.isSelf() - && !Config.QUICKSY_DOMAIN.equals(contact.getJid().toEscapedString()) + && !JidHelper.isQuicksyDomain(contact.getJid()) && sentMessagesCount() == 0; } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 4263a6ef8..44f7588f8 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -6,6 +6,9 @@ import android.graphics.Color; import android.text.SpannableStringBuilder; import android.util.Log; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; + import org.json.JSONException; import java.lang.ref.WeakReference; @@ -21,6 +24,7 @@ import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.services.AvatarService; +import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.GeoHelper; @@ -531,7 +535,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public Set getReadByMarkers() { - return Collections.unmodifiableSet(this.readByMarkers); + return ImmutableSet.copyOf(this.readByMarkers); } boolean similar(Message message) { @@ -745,19 +749,12 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public boolean fixCounterpart() { - Presences presences = conversation.getContact().getPresences(); - if (counterpart != null && presences.has(counterpart.getResource())) { + final Presences presences = conversation.getContact().getPresences(); + if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) { return true; } else if (presences.size() >= 1) { - try { - counterpart = Jid.of(conversation.getJid().getLocal(), - conversation.getJid().getDomain(), - presences.toResourceArray()[0]); - return true; - } catch (IllegalArgumentException e) { - counterpart = null; - return false; - } + counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]); + return true; } else { counterpart = null; return false; diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 37cf7ed33..991fea547 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -836,6 +836,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece for (Element child : packet.getChildren()) { if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) { final String action = child.getName(); + final String sessionId = child.getAttribute("id"); + if (sessionId == null) { + break; + } if (query == null) { if (serverMsgId == null) { serverMsgId = extractStanzaId(account, packet); @@ -845,10 +849,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece processMessageReceipts(account, packet, query); } } else if (query.isCatchup()) { - final String sessionId = child.getAttribute("id"); - if (sessionId == null) { - break; - } if ("propose".equals(action)) { final Element description = child.findChild("description"); final String namespace = description == null ? null : description.getNamespace(); @@ -872,7 +872,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece c.add(message); mXmppConnectionService.databaseBackend.createMessage(message); } - } else if ("proceed".equals(action)) { //status needs to be flipped to find the original propose final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false); @@ -890,6 +889,37 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } + } else { + //MAM reloads (non catchups + if ("propose".equals(action)) { + final Element description = child.findChild("description"); + final String namespace = description == null ? null : description.getNamespace(); + if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false); + final Message preExistingMessage = c.findRtpSession(sessionId, status); + if (preExistingMessage != null) { + preExistingMessage.setServerMsgId(serverMsgId); + mXmppConnectionService.updateMessage(preExistingMessage); + break; + } + final Message message = new Message( + c, + status, + Message.TYPE_RTP_SESSION, + sessionId + ); + message.setServerMsgId(serverMsgId); + message.setTime(timestamp); + message.setBody(new RtpSessionStatus(true, 0).toString()); + if (query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { + c.prepend(query.getActualInThisQuery(), message); + } else { + c.add(message); + } + query.incrementActualMessageCount(); + mXmppConnectionService.databaseBackend.createMessage(message); + } + } } break; } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 1ff94c7c0..98d0f3de8 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -704,7 +704,7 @@ public class FileBackend { return pos > 0 ? filename.substring(pos + 1) : null; } - private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException { + private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, NotAnImageFileException { file.getParentFile().mkdirs(); InputStream is = null; OutputStream os = null; @@ -724,7 +724,7 @@ public class FileBackend { originalBitmap = BitmapFactory.decodeStream(is, null, options); is.close(); if (originalBitmap == null) { - throw new FileCopyException(R.string.error_not_an_image_file); + throw new NotAnImageFileException(); } Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE); int rotation = getRotation(image); @@ -763,12 +763,12 @@ public class FileBackend { } } - public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException { + public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException, NotAnImageFileException { Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath()); copyImageToPrivateStorage(file, image, 0); } - public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException { + public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException, NotAnImageFileException { switch (Config.IMAGE_FORMAT) { case JPEG: message.setRelativeFilePath(message.getUuid() + ".jpg"); @@ -1420,11 +1420,14 @@ public class FileBackend { } } - public class FileCopyException extends Exception { - private static final long serialVersionUID = -1010013599132881427L; + public static class NotAnImageFileException extends Exception { + + } + + public static class FileCopyException extends Exception { private int resId; - public FileCopyException(int resId) { + private FileCopyException(int resId) { this.resId = resId; } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 6f1ff65cc..48030e7f3 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -567,19 +567,23 @@ public class XmppConnectionService extends Service { mFileAddingExecutor.execute(() -> { try { getFileBackend().copyImageToPrivateStorage(message, uri); - if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - final PgpEngine pgpEngine = getPgpEngine(); - if (pgpEngine != null) { - pgpEngine.encrypt(message, callback); - } else if (callback != null) { - callback.error(R.string.unable_to_connect_to_keychain, null); - } - } else { - sendMessage(message); - callback.success(message); - } + } catch (FileBackend.NotAnImageFileException e) { + attachFileToConversation(conversation, uri, mimeType, callback); + return; } catch (final FileBackend.FileCopyException e) { callback.error(e.getResId(), message); + return; + } + if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { + final PgpEngine pgpEngine = getPgpEngine(); + if (pgpEngine != null) { + pgpEngine.encrypt(message, callback); + } else if (callback != null) { + callback.error(R.string.unable_to_connect_to_keychain, null); + } + } else { + sendMessage(message); + callback.success(message); } }); } @@ -2774,6 +2778,7 @@ public class XmppConnectionService extends Service { updateConversationUi(); } } + private void fetchConferenceMembers(final Conversation conversation) { final Account account = conversation.getAccount(); final AxolotlService axolotlService = account.getAxolotlService(); @@ -3299,6 +3304,10 @@ public class XmppConnectionService extends Service { updateConversationUi(); } + public void createMessageAsync(final Message message) { + mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message)); + } + public void updateMessage(Message message, String uuid) { if (!databaseBackend.updateMessage(message, uuid)) { Log.e(Config.LOGTAG, "error updated message in DB after edit"); @@ -4137,7 +4146,7 @@ public class XmppConnectionService extends Service { } } if (Config.QUICKSY_DOMAIN != null) { - hosts.remove(Config.QUICKSY_DOMAIN); //we only want to show this when we type a e164 number + hosts.remove(Config.QUICKSY_DOMAIN.toEscapedString()); //we only want to show this when we type a e164 number } if (Config.DOMAIN_LOCK != null) { hosts.add(Config.DOMAIN_LOCK); @@ -4436,34 +4445,38 @@ public class XmppConnectionService extends Service { public void fetchCaps(Account account, final Jid jid, final Presence presence) { final Pair key = new Pair<>(presence.getHash(), presence.getVer()); - ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key); + final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key); if (disco != null) { presence.setServiceDiscoveryResult(disco); } else { - if (!account.inProgressDiscoFetches.contains(key)) { - account.inProgressDiscoFetches.add(key); - IqPacket request = new IqPacket(IqPacket.TYPE.GET); - request.setTo(jid); - final String node = presence.getNode(); - final String ver = presence.getVer(); - final Element query = request.query(Namespace.DISCO_INFO); - if (node != null && ver != null) { - query.setAttribute("node", node + "#" + ver); - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid); - sendIqPacket(account, request, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response); - if (presence.getVer().equals(discoveryResult.getVer())) { - databaseBackend.insertDiscoveryResult(discoveryResult); - injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult); - } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer()); - } - } - a.inProgressDiscoFetches.remove(key); - }); + if (account.inProgressDiscoFetches.contains(key)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping duplicate disco request for " + key.second + " to " + jid); + return; } + account.inProgressDiscoFetches.add(key); + final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(jid); + final String node = presence.getNode(); + final String ver = presence.getVer(); + final Element query = request.query(Namespace.DISCO_INFO); + if (node != null && ver != null) { + query.setAttribute("node", node + "#" + ver); + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid); + sendIqPacket(account, request, (a, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response); + if (presence.getVer().equals(discoveryResult.getVer())) { + databaseBackend.insertDiscoveryResult(discoveryResult); + injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult); + } else { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer()); + } + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to fetch caps from " + jid); + } + a.inProgressDiscoFetches.remove(key); + }); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index c0b97759d..398be7f68 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -263,7 +263,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O } public void joinChannelSearchResult(String selectedAccount, Room result) { - final Jid jid = Config.DOMAIN_LOCK == null ? Jid.ofEscaped(selectedAccount) : Jid.ofEscaped(selectedAccount, Config.DOMAIN_LOCK, null); + final Jid jid = Config.DOMAIN_LOCK == null ? Jid.ofEscaped(selectedAccount) : Jid.ofLocalAndDomainEscaped(selectedAccount, Config.DOMAIN_LOCK); final boolean syncAutoJoin = getBooleanPreference("autojoin", R.bool.autojoin); final Account account = xmppConnectionService.findAccountByJid(jid); final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, result.getRoom(), true, true, true); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index b7e018abf..23a743eda 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.ui; import android.Manifest; import android.annotation.SuppressLint; import android.app.PictureInPictureParams; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -17,6 +18,8 @@ import android.support.annotation.RequiresApi; import android.support.annotation.StringRes; import android.util.Log; import android.util.Rational; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.WindowManager; import android.widget.Toast; @@ -53,6 +56,7 @@ import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.utils.TimeFrameUtils; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; @@ -77,8 +81,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONNECTIVITY_ERROR, + RtpEndUserState.CONNECTIVITY_LOST_ERROR, RtpEndUserState.RETRACTED ); + private static final List STATES_SHOWING_HELP_BUTTON = Arrays.asList( + RtpEndUserState.APPLICATION_ERROR, + RtpEndUserState.CONNECTIVITY_ERROR + ); private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; private WeakReference rtpConnectionReference; @@ -122,6 +131,45 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe setSupportActionBar(binding.toolbar); } + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.activity_rtp_session, menu); + final MenuItem help = menu.findItem(R.id.action_help); + help.setVisible(isHelpButtonVisible()); + return super.onCreateOptionsMenu(menu); + } + + private boolean isHelpButtonVisible() { + try { + return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState()); + } catch (IllegalStateException e) { + final Intent intent = getIntent(); + final String state = intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null; + if (state != null) { + return STATES_SHOWING_HELP_BUTTON.contains(RtpEndUserState.valueOf(state)); + } else { + return false; + } + } + } + + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == R.id.action_help) { + launchHelpInBrowser(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void launchHelpInBrowser() { + final Intent intent = new Intent(Intent.ACTION_VIEW, Config.HELP); + try { + startActivity(intent); + } catch (final ActivityNotFoundException e) { + Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG).show(); + } + } + private void endCall(View view) { endCall(); } @@ -232,9 +280,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.mProximityWakeLock = null; } } - - private void putProximityWakeLockInProperState() { - if (requireRtpConnection().getAudioManager().getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) { + + private void putProximityWakeLockInProperState(final AppRTCAudioManager.AudioDevice audioDevice) { + if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) { acquireProximityWakeLock(); } else { releaseProximityWakeLock(); @@ -300,6 +348,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateButtonConfiguration(state); updateStateDisplay(state); updateProfilePicture(state); + invalidateOptionsMenu(); } binding.with.setText(account.getRoster().getContact(with).getDisplayName()); } @@ -427,7 +476,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final WeakReference reference = xmppConnectionService.getJingleConnectionManager() .findJingleRtpConnection(account, with, sessionId); if (reference == null || reference.get() == null) { - throw new IllegalStateException("failed to initialize activity with running rtp session. session not found"); + final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = xmppConnectionService + .getJingleConnectionManager().getTerminalSessionState(with, sessionId); + if (terminatedRtpSession == null) { + throw new IllegalStateException("failed to initialize activity with running rtp session. session not found"); + } + initializeWithTerminatedSessionState(account, with, terminatedRtpSession); + return true; } this.rtpConnectionReference = reference; final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); @@ -448,9 +503,26 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateStateDisplay(currentState, media); updateButtonConfiguration(currentState, media); updateProfilePicture(currentState); + invalidateOptionsMenu(); return false; } + private void initializeWithTerminatedSessionState(final Account account, final Jid with, final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) { + Log.d(Config.LOGTAG, "initializeWithTerminatedSessionState()"); + if (terminatedRtpSession.state == RtpEndUserState.ENDED) { + finish(); + return; + } + RtpEndUserState state = terminatedRtpSession.state; + resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media); + updateButtonConfiguration(state); + updateStateDisplay(state); + updateProfilePicture(state); + updateCallDuration(); + invalidateOptionsMenu(); + binding.with.setText(account.getRoster().getContact(with).getDisplayName()); + } + private void reInitializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) { runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId)); resetIntent(account, with, sessionId); @@ -512,6 +584,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe case CONNECTIVITY_ERROR: setTitle(R.string.rtp_state_connectivity_error); break; + case CONNECTIVITY_LOST_ERROR: + setTitle(R.string.rtp_state_connectivity_lost_error); + break; case RETRACTED: setTitle(R.string.rtp_state_retracted); break; @@ -577,7 +652,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.acceptCall.setOnClickListener(this::recordVoiceMail); this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_white_24dp); this.binding.acceptCall.setVisibility(View.VISIBLE); - } else if (asList(RtpEndUserState.CONNECTIVITY_ERROR, RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.RETRACTED).contains(state)) { + } else if (asList( + RtpEndUserState.CONNECTIVITY_ERROR, + RtpEndUserState.CONNECTIVITY_LOST_ERROR, + RtpEndUserState.APPLICATION_ERROR, + RtpEndUserState.RETRACTED + ).contains(state)) { this.binding.rejectCall.setOnClickListener(this::exit); this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); this.binding.rejectCall.setVisibility(View.VISIBLE); @@ -926,6 +1006,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateButtonConfiguration(state, media); updateVideoViews(state); updateProfilePicture(state, contact); + invalidateOptionsMenu(); }); if (END_CARD.contains(state)) { final JingleRtpConnection rtpConnection = requireRtpConnection(); @@ -956,7 +1037,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } else if (END_CARD.contains(endUserState)) { Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached"); } else { - putProximityWakeLockInProperState(); + putProximityWakeLockInProperState(selectedAudioDevice); } } catch (IllegalStateException e) { Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed"); @@ -974,6 +1055,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateStateDisplay(state); updateButtonConfiguration(state); updateProfilePicture(state); + invalidateOptionsMenu(); }); resetIntent(account, with, state, actionToMedia(currentIntent.getAction())); } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 93196a8b0..fcdccb9c4 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -83,931 +83,927 @@ import eu.siacs.conversations.xmpp.Jid; public abstract class XmppActivity extends ActionBarActivity { - public static final String EXTRA_ACCOUNT = "account"; - protected static final int REQUEST_ANNOUNCE_PGP = 0x0101; - protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102; - protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103; - protected static final int REQUEST_BATTERY_OP = 0x49ff; - public XmppConnectionService xmppConnectionService; - public boolean xmppConnectionServiceBound = false; - - protected static final String FRAGMENT_TAG_DIALOG = "dialog"; - - private boolean isCameraFeatureAvailable = false; - - protected int mTheme; - protected boolean mUsingEnterKey = false; - protected boolean mUseTor = false; - protected Toast mToast; - public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show(); - protected ConferenceInvite mPendingConferenceInvite = null; - protected ServiceConnection mConnection = new ServiceConnection() { - - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - XmppConnectionBinder binder = (XmppConnectionBinder) service; - xmppConnectionService = binder.getService(); - xmppConnectionServiceBound = true; - registerListeners(); - onBackendConnected(); - } - - @Override - public void onServiceDisconnected(ComponentName arg0) { - xmppConnectionServiceBound = false; - } - }; - private DisplayMetrics metrics; - private long mLastUiRefresh = 0; - private Handler mRefreshUiHandler = new Handler(); - private Runnable mRefreshUiRunnable = () -> { - mLastUiRefresh = SystemClock.elapsedRealtime(); - refreshUiReal(); - }; - private UiCallback adhocCallback = new UiCallback() { - @Override - public void success(final Conversation conversation) { - runOnUiThread(() -> { - switchToConversation(conversation); - hideToast(); - }); - } - - @Override - public void error(final int errorCode, Conversation object) { - runOnUiThread(() -> replaceToast(getString(errorCode))); - } - - @Override - public void userInputRequired(PendingIntent pi, Conversation object) { - - } - }; - public boolean mSkipBackgroundBinding = false; - - public static boolean cancelPotentialWork(Message message, ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final Message oldMessage = bitmapWorkerTask.message; - if (oldMessage == null || message != oldMessage) { - bitmapWorkerTask.cancel(true); - } else { - return false; - } - } - return true; - } - - private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncDrawable) { - final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; - return asyncDrawable.getBitmapWorkerTask(); - } - } - return null; - } - - protected void hideToast() { - if (mToast != null) { - mToast.cancel(); - } - } - - protected void replaceToast(String msg) { - replaceToast(msg, true); - } - - protected void replaceToast(String msg, boolean showlong) { - hideToast(); - mToast = Toast.makeText(this, msg, showlong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT); - mToast.show(); - } - - protected final void refreshUi() { - final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh; - if (diff > Config.REFRESH_UI_INTERVAL) { - mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); - runOnUiThread(mRefreshUiRunnable); - } else { - final long next = Config.REFRESH_UI_INTERVAL - diff; - mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); - mRefreshUiHandler.postDelayed(mRefreshUiRunnable, next); - } - } - - abstract protected void refreshUiReal(); - - @Override - protected void onStart() { - super.onStart(); - if (!xmppConnectionServiceBound) { - if (this.mSkipBackgroundBinding) { - Log.d(Config.LOGTAG,"skipping background binding"); - } else { - connectToBackend(); - } - } else { - this.registerListeners(); - this.onBackendConnected(); - } - this.mUsingEnterKey = usingEnterKey(); - this.mUseTor = useTor(); - } - - public void connectToBackend() { - Intent intent = new Intent(this, XmppConnectionService.class); - intent.setAction("ui"); - try { - startService(intent); - } catch (IllegalStateException e) { - Log.w(Config.LOGTAG,"unable to start service from "+getClass().getSimpleName()); - } - bindService(intent, mConnection, Context.BIND_AUTO_CREATE); - } - - @Override - protected void onStop() { - super.onStop(); - if (xmppConnectionServiceBound) { - this.unregisterListeners(); - unbindService(mConnection); - xmppConnectionServiceBound = false; - } - } - - - public boolean hasPgp() { - return xmppConnectionService.getPgpEngine() != null; - } - - public void showInstallPgpDialog() { - Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.openkeychain_required)); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getText(R.string.openkeychain_required_long)); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setNeutralButton(getString(R.string.restart), - (dialog, which) -> { - if (xmppConnectionServiceBound) { - unbindService(mConnection); - xmppConnectionServiceBound = false; - } - stopService(new Intent(XmppActivity.this, - XmppConnectionService.class)); - finish(); - }); - builder.setPositiveButton(getString(R.string.install), - (dialog, which) -> { - Uri uri = Uri - .parse("market://details?id=org.sufficientlysecure.keychain"); - Intent marketIntent = new Intent(Intent.ACTION_VIEW, - uri); - PackageManager manager = getApplicationContext() - .getPackageManager(); - List infos = manager - .queryIntentActivities(marketIntent, 0); - if (infos.size() > 0) { - startActivity(marketIntent); - } else { - uri = Uri.parse("http://www.openkeychain.org/"); - Intent browserIntent = new Intent( - Intent.ACTION_VIEW, uri); - startActivity(browserIntent); - } - finish(); - }); - builder.create().show(); - } - - abstract void onBackendConnected(); - - protected void registerListeners() { - if (this instanceof XmppConnectionService.OnConversationUpdate) { - this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); - } - if (this instanceof XmppConnectionService.OnAccountUpdate) { - this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); - } - if (this instanceof XmppConnectionService.OnCaptchaRequested) { - this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this); - } - if (this instanceof XmppConnectionService.OnRosterUpdate) { - this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); - } - if (this instanceof XmppConnectionService.OnMucRosterUpdate) { - this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); - } - if (this instanceof OnUpdateBlocklist) { - this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this); - } - if (this instanceof XmppConnectionService.OnShowErrorToast) { - this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); - } - if (this instanceof OnKeyStatusUpdated) { - this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this); - } - if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) { - this.xmppConnectionService.setOnRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this); - } - } - - protected void unregisterListeners() { - if (this instanceof XmppConnectionService.OnConversationUpdate) { - this.xmppConnectionService.removeOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); - } - if (this instanceof XmppConnectionService.OnAccountUpdate) { - this.xmppConnectionService.removeOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); - } - if (this instanceof XmppConnectionService.OnCaptchaRequested) { - this.xmppConnectionService.removeOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this); - } - if (this instanceof XmppConnectionService.OnRosterUpdate) { - this.xmppConnectionService.removeOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); - } - if (this instanceof XmppConnectionService.OnMucRosterUpdate) { - this.xmppConnectionService.removeOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); - } - if (this instanceof OnUpdateBlocklist) { - this.xmppConnectionService.removeOnUpdateBlocklistListener((OnUpdateBlocklist) this); - } - if (this instanceof XmppConnectionService.OnShowErrorToast) { - this.xmppConnectionService.removeOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); - } - if (this instanceof OnKeyStatusUpdated) { - this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this); - } - if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) { - this.xmppConnectionService.removeRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this); - } - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_settings: - startActivity(new Intent(this, SettingsActivity.class)); - break; - case R.id.action_accounts: - AccountUtils.launchManageAccounts(this); - break; - case R.id.action_account: - AccountUtils.launchManageAccount(this); - break; - case android.R.id.home: - finish(); - break; - case R.id.action_show_qr_code: - showQrCode(); - break; - } - return super.onOptionsItemSelected(item); - } - - public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) { - final Contact contact = conversation.getContact(); - if (!contact.showInRoster()) { - showAddToRosterDialog(conversation.getContact()); - } else { - final Presences presences = contact.getPresences(); - if (presences.size() == 0) { - if (!contact.getOption(Contact.Options.TO) - && !contact.getOption(Contact.Options.ASKING) - && contact.getAccount().getStatus() == Account.State.ONLINE) { - showAskForPresenceDialog(contact); - } else if (!contact.getOption(Contact.Options.TO) - || !contact.getOption(Contact.Options.FROM)) { - PresenceSelector.warnMutualPresenceSubscription(this, conversation, listener); - } else { - conversation.setNextCounterpart(null); - listener.onPresenceSelected(); - } - } else if (presences.size() == 1) { - String presence = presences.toResourceArray()[0]; - try { - conversation.setNextCounterpart(Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), presence)); - } catch (IllegalArgumentException e) { - conversation.setNextCounterpart(null); - } - listener.onPresenceSelected(); - } else { - PresenceSelector.showPresenceSelectionDialog(this, conversation, listener); - } - } - } - - @SuppressLint("UnsupportedChromeOsCameraSystemFeature") - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - metrics = getResources().getDisplayMetrics(); - ExceptionHelper.init(getApplicationContext()); - new EmojiService(this).init(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); - } else { - this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); - } - this.mTheme = findTheme(); - setTheme(this.mTheme); - } - - protected boolean isCameraFeatureAvailable() { - return this.isCameraFeatureAvailable; - } - - public boolean isDarkTheme() { - return ThemeHelper.isDark(mTheme); - } - - public int getThemeResource(int r_attr_name, int r_drawable_def) { - int[] attrs = {r_attr_name}; - TypedArray ta = this.getTheme().obtainStyledAttributes(attrs); - - int res = ta.getResourceId(0, r_drawable_def); - ta.recycle(); - - return res; - } - - protected boolean isOptimizingBattery() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); - return pm != null - && !pm.isIgnoringBatteryOptimizations(getPackageName()); - } else { - return false; - } - } - - protected boolean isAffectedByDataSaver() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - return cm != null - && cm.isActiveNetworkMetered() - && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; - } else { - return false; - } - } - - private boolean usingEnterKey() { - return getBooleanPreference("display_enter_key", R.bool.display_enter_key); - } - - private boolean useTor() { - return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor); - } - - protected SharedPreferences getPreferences() { - return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - } - - protected boolean getBooleanPreference(String name, @BoolRes int res) { - return getPreferences().getBoolean(name, getResources().getBoolean(res)); - } - - public void switchToConversation(Conversation conversation) { - switchToConversation(conversation, null); - } - - public void switchToConversationAndQuote(Conversation conversation, String text) { - switchToConversation(conversation, text, true, null, false, false); - } - - public void switchToConversation(Conversation conversation, String text) { - switchToConversation(conversation, text, false, null, false, false); - } - - public void switchToConversationDoNotAppend(Conversation conversation, String text) { - switchToConversation(conversation, text, false, null, false, true); - } - - public void highlightInMuc(Conversation conversation, String nick) { - switchToConversation(conversation, null, false, nick, false, false); - } - - public void privateMsgInMuc(Conversation conversation, String nick) { - switchToConversation(conversation, null, false, nick, true, false); - } - - private void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) { - Intent intent = new Intent(this, ConversationsActivity.class); - intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); - intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); - if (text != null) { - intent.putExtra(Intent.EXTRA_TEXT, text); - if (asQuote) { - intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true); - } - } - if (nick != null) { - intent.putExtra(ConversationsActivity.EXTRA_NICK, nick); - intent.putExtra(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, pm); - } - if (doNotAppend) { - intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true); - } - intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - finish(); - } - - public void switchToContactDetails(Contact contact) { - switchToContactDetails(contact, null); - } - - public void switchToContactDetails(Contact contact, String messageFingerprint) { - Intent intent = new Intent(this, ContactDetailsActivity.class); - intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); - intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toEscapedString()); - intent.putExtra("contact", contact.getJid().toEscapedString()); - intent.putExtra("fingerprint", messageFingerprint); - startActivity(intent); - } - - public void switchToAccount(Account account, String fingerprint) { - switchToAccount(account, false, fingerprint); - } - - public void switchToAccount(Account account) { - switchToAccount(account, false, null); - } - - public void switchToAccount(Account account, boolean init, String fingerprint) { - Intent intent = new Intent(this, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().asBareJid().toEscapedString()); - intent.putExtra("init", init); - if (init) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION); - } - if (fingerprint != null) { - intent.putExtra("fingerprint", fingerprint); - } - startActivity(intent); - if (init) { - overridePendingTransition(0, 0); - } - } - - protected void delegateUriPermissionsToService(Uri uri) { - Intent intent = new Intent(this, XmppConnectionService.class); - intent.setAction(Intent.ACTION_SEND); - intent.setData(uri); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - try { - startService(intent); - } catch (Exception e) { - Log.e(Config.LOGTAG,"unable to delegate uri permission",e); - } - } - - protected void inviteToConversation(Conversation conversation) { - startActivityForResult(ChooseContactActivity.create(this,conversation), REQUEST_INVITE_TO_CONVERSATION); - } - - protected void announcePgp(final Account account, final Conversation conversation, Intent intent, final Runnable onSuccess) { - if (account.getPgpId() == 0) { - choosePgpSignId(account); - } else { - String status = null; - if (manuallyChangePresence()) { - status = account.getPresenceStatusMessage(); - } - if (status == null) { - status = ""; - } - xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback() { - - @Override - public void userInputRequired(PendingIntent pi, String signature) { - try { - startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); - } catch (final SendIntentException ignored) { - } - } - - @Override - public void success(String signature) { - account.setPgpSignature(signature); - xmppConnectionService.databaseBackend.updateAccount(account); - xmppConnectionService.sendPresence(account); - if (conversation != null) { - conversation.setNextEncryption(Message.ENCRYPTION_PGP); - xmppConnectionService.updateConversation(conversation); - refreshUi(); - } - if (onSuccess != null) { - runOnUiThread(onSuccess); - } - } - - @Override - public void error(int error, String signature) { - if (error == 0) { - account.setPgpSignId(0); - account.unsetPgpSignature(); - xmppConnectionService.databaseBackend.updateAccount(account); - choosePgpSignId(account); - } else { - displayErrorDialog(error); - } - } - }); - } - } - - @SuppressWarnings("deprecation") - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - protected void setListItemBackgroundOnView(View view) { - int sdk = android.os.Build.VERSION.SDK_INT; - if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) { - view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground)); - } else { - view.setBackground(getResources().getDrawable(R.drawable.greybackground)); - } - } - - protected void choosePgpSignId(Account account) { - xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback() { - @Override - public void success(Account account1) { - } - - @Override - public void error(int errorCode, Account object) { - - } - - @Override - public void userInputRequired(PendingIntent pi, Account object) { - try { - startIntentSenderForResult(pi.getIntentSender(), - REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0); - } catch (final SendIntentException ignored) { - } - } - }); - } - - protected void displayErrorDialog(final int errorCode) { - runOnUiThread(() -> { - Builder builder = new Builder(XmppActivity.this); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setTitle(getString(R.string.error)); - builder.setMessage(errorCode); - builder.setNeutralButton(R.string.accept, null); - builder.create().show(); - }); - - } - - protected void showAddToRosterDialog(final Contact contact) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(contact.getJid().toString()); - builder.setMessage(getString(R.string.not_in_roster)); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> xmppConnectionService.createContact(contact,true)); - builder.create().show(); - } - - private void showAskForPresenceDialog(final Contact contact) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(contact.getJid().toString()); - builder.setMessage(R.string.request_presence_updates); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.request_now, - (dialog, which) -> { - if (xmppConnectionServiceBound) { - xmppConnectionService.sendPresencePacket(contact - .getAccount(), xmppConnectionService - .getPresenceGenerator() - .requestPresenceUpdatesFrom(contact)); - } - }); - builder.create().show(); - } - - protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback) { - quickEdit(previousValue, callback, hint, false, false); - } - - protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback, boolean permitEmpty) { - quickEdit(previousValue, callback, hint, false, permitEmpty); - } - - protected void quickPasswordEdit(String previousValue, OnValueEdited callback) { - quickEdit(previousValue, callback, R.string.password, true, false); - } - - @SuppressLint("InflateParams") - private void quickEdit(final String previousValue, - final OnValueEdited callback, - final @StringRes int hint, - boolean password, - boolean permitEmpty) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(),R.layout.dialog_quickedit, null, false); - if (password) { - binding.inputEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } - builder.setPositiveButton(R.string.accept, null); - if (hint != 0) { - binding.inputLayout.setHint(getString(hint)); - } - binding.inputEditText.requestFocus(); - if (previousValue != null) { - binding.inputEditText.getText().append(previousValue); - } - builder.setView(binding.getRoot()); - builder.setNegativeButton(R.string.cancel, null); - final AlertDialog dialog = builder.create(); - dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText)); - dialog.show(); - View.OnClickListener clickListener = v -> { - String value = binding.inputEditText.getText().toString(); - if (!value.equals(previousValue) && (!value.trim().isEmpty() || permitEmpty)) { - String error = callback.onValueEdited(value); - if (error != null) { - binding.inputLayout.setError(error); - return; - } - } - SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); - dialog.dismiss(); - }; - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener); - dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> { - SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); - dialog.dismiss(); - })); - dialog.setCanceledOnTouchOutside(false); - dialog.setOnDismissListener(dialog1 -> { - SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); + public static final String EXTRA_ACCOUNT = "account"; + protected static final int REQUEST_ANNOUNCE_PGP = 0x0101; + protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102; + protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103; + protected static final int REQUEST_BATTERY_OP = 0x49ff; + public XmppConnectionService xmppConnectionService; + public boolean xmppConnectionServiceBound = false; + + protected static final String FRAGMENT_TAG_DIALOG = "dialog"; + + private boolean isCameraFeatureAvailable = false; + + protected int mTheme; + protected boolean mUsingEnterKey = false; + protected boolean mUseTor = false; + protected Toast mToast; + public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show(); + protected ConferenceInvite mPendingConferenceInvite = null; + protected ServiceConnection mConnection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + XmppConnectionBinder binder = (XmppConnectionBinder) service; + xmppConnectionService = binder.getService(); + xmppConnectionServiceBound = true; + registerListeners(); + onBackendConnected(); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + xmppConnectionServiceBound = false; + } + }; + private DisplayMetrics metrics; + private long mLastUiRefresh = 0; + private Handler mRefreshUiHandler = new Handler(); + private Runnable mRefreshUiRunnable = () -> { + mLastUiRefresh = SystemClock.elapsedRealtime(); + refreshUiReal(); + }; + private UiCallback adhocCallback = new UiCallback() { + @Override + public void success(final Conversation conversation) { + runOnUiThread(() -> { + switchToConversation(conversation); + hideToast(); + }); + } + + @Override + public void error(final int errorCode, Conversation object) { + runOnUiThread(() -> replaceToast(getString(errorCode))); + } + + @Override + public void userInputRequired(PendingIntent pi, Conversation object) { + + } + }; + public boolean mSkipBackgroundBinding = false; + + public static boolean cancelPotentialWork(Message message, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Message oldMessage = bitmapWorkerTask.message; + if (oldMessage == null || message != oldMessage) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + protected void hideToast() { + if (mToast != null) { + mToast.cancel(); + } + } + + protected void replaceToast(String msg) { + replaceToast(msg, true); + } + + protected void replaceToast(String msg, boolean showlong) { + hideToast(); + mToast = Toast.makeText(this, msg, showlong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT); + mToast.show(); + } + + protected final void refreshUi() { + final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh; + if (diff > Config.REFRESH_UI_INTERVAL) { + mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); + runOnUiThread(mRefreshUiRunnable); + } else { + final long next = Config.REFRESH_UI_INTERVAL - diff; + mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); + mRefreshUiHandler.postDelayed(mRefreshUiRunnable, next); + } + } + + abstract protected void refreshUiReal(); + + @Override + protected void onStart() { + super.onStart(); + if (!xmppConnectionServiceBound) { + if (this.mSkipBackgroundBinding) { + Log.d(Config.LOGTAG, "skipping background binding"); + } else { + connectToBackend(); + } + } else { + this.registerListeners(); + this.onBackendConnected(); + } + this.mUsingEnterKey = usingEnterKey(); + this.mUseTor = useTor(); + } + + public void connectToBackend() { + Intent intent = new Intent(this, XmppConnectionService.class); + intent.setAction("ui"); + try { + startService(intent); + } catch (IllegalStateException e) { + Log.w(Config.LOGTAG, "unable to start service from " + getClass().getSimpleName()); + } + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onStop() { + super.onStop(); + if (xmppConnectionServiceBound) { + this.unregisterListeners(); + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + } + + + public boolean hasPgp() { + return xmppConnectionService.getPgpEngine() != null; + } + + public void showInstallPgpDialog() { + Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.openkeychain_required)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getText(R.string.openkeychain_required_long)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setNeutralButton(getString(R.string.restart), + (dialog, which) -> { + if (xmppConnectionServiceBound) { + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + stopService(new Intent(XmppActivity.this, + XmppConnectionService.class)); + finish(); + }); + builder.setPositiveButton(getString(R.string.install), + (dialog, which) -> { + Uri uri = Uri + .parse("market://details?id=org.sufficientlysecure.keychain"); + Intent marketIntent = new Intent(Intent.ACTION_VIEW, + uri); + PackageManager manager = getApplicationContext() + .getPackageManager(); + List infos = manager + .queryIntentActivities(marketIntent, 0); + if (infos.size() > 0) { + startActivity(marketIntent); + } else { + uri = Uri.parse("http://www.openkeychain.org/"); + Intent browserIntent = new Intent( + Intent.ACTION_VIEW, uri); + startActivity(browserIntent); + } + finish(); + }); + builder.create().show(); + } + + abstract void onBackendConnected(); + + protected void registerListeners() { + if (this instanceof XmppConnectionService.OnConversationUpdate) { + this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); + } + if (this instanceof XmppConnectionService.OnAccountUpdate) { + this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); + } + if (this instanceof XmppConnectionService.OnCaptchaRequested) { + this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this); + } + if (this instanceof XmppConnectionService.OnRosterUpdate) { + this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); + } + if (this instanceof XmppConnectionService.OnMucRosterUpdate) { + this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); + } + if (this instanceof OnUpdateBlocklist) { + this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this); + } + if (this instanceof XmppConnectionService.OnShowErrorToast) { + this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); + } + if (this instanceof OnKeyStatusUpdated) { + this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this); + } + if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) { + this.xmppConnectionService.setOnRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this); + } + } + + protected void unregisterListeners() { + if (this instanceof XmppConnectionService.OnConversationUpdate) { + this.xmppConnectionService.removeOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); + } + if (this instanceof XmppConnectionService.OnAccountUpdate) { + this.xmppConnectionService.removeOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); + } + if (this instanceof XmppConnectionService.OnCaptchaRequested) { + this.xmppConnectionService.removeOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this); + } + if (this instanceof XmppConnectionService.OnRosterUpdate) { + this.xmppConnectionService.removeOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); + } + if (this instanceof XmppConnectionService.OnMucRosterUpdate) { + this.xmppConnectionService.removeOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); + } + if (this instanceof OnUpdateBlocklist) { + this.xmppConnectionService.removeOnUpdateBlocklistListener((OnUpdateBlocklist) this); + } + if (this instanceof XmppConnectionService.OnShowErrorToast) { + this.xmppConnectionService.removeOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); + } + if (this instanceof OnKeyStatusUpdated) { + this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this); + } + if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) { + this.xmppConnectionService.removeRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this); + } + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case R.id.action_settings: + startActivity(new Intent(this, SettingsActivity.class)); + break; + case R.id.action_accounts: + AccountUtils.launchManageAccounts(this); + break; + case R.id.action_account: + AccountUtils.launchManageAccount(this); + break; + case android.R.id.home: + finish(); + break; + case R.id.action_show_qr_code: + showQrCode(); + break; + } + return super.onOptionsItemSelected(item); + } + + public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) { + final Contact contact = conversation.getContact(); + if (!contact.showInRoster()) { + showAddToRosterDialog(conversation.getContact()); + } else { + final Presences presences = contact.getPresences(); + if (presences.size() == 0) { + if (!contact.getOption(Contact.Options.TO) + && !contact.getOption(Contact.Options.ASKING) + && contact.getAccount().getStatus() == Account.State.ONLINE) { + showAskForPresenceDialog(contact); + } else if (!contact.getOption(Contact.Options.TO) + || !contact.getOption(Contact.Options.FROM)) { + PresenceSelector.warnMutualPresenceSubscription(this, conversation, listener); + } else { + conversation.setNextCounterpart(null); + listener.onPresenceSelected(); + } + } else if (presences.size() == 1) { + final String presence = presences.toResourceArray()[0]; + conversation.setNextCounterpart(PresenceSelector.getNextCounterpart(contact, presence)); + listener.onPresenceSelected(); + } else { + PresenceSelector.showPresenceSelectionDialog(this, conversation, listener); + } + } + } + + @SuppressLint("UnsupportedChromeOsCameraSystemFeature") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + metrics = getResources().getDisplayMetrics(); + ExceptionHelper.init(getApplicationContext()); + new EmojiService(this).init(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + } else { + this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); + } + this.mTheme = findTheme(); + setTheme(this.mTheme); + } + + protected boolean isCameraFeatureAvailable() { + return this.isCameraFeatureAvailable; + } + + public boolean isDarkTheme() { + return ThemeHelper.isDark(mTheme); + } + + public int getThemeResource(int r_attr_name, int r_drawable_def) { + int[] attrs = {r_attr_name}; + TypedArray ta = this.getTheme().obtainStyledAttributes(attrs); + + int res = ta.getResourceId(0, r_drawable_def); + ta.recycle(); + + return res; + } + + protected boolean isOptimizingBattery() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + return pm != null + && !pm.isIgnoringBatteryOptimizations(getPackageName()); + } else { + return false; + } + } + + protected boolean isAffectedByDataSaver() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + return cm != null + && cm.isActiveNetworkMetered() + && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; + } else { + return false; + } + } + + private boolean usingEnterKey() { + return getBooleanPreference("display_enter_key", R.bool.display_enter_key); + } + + private boolean useTor() { + return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor); + } + + protected SharedPreferences getPreferences() { + return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + } + + protected boolean getBooleanPreference(String name, @BoolRes int res) { + return getPreferences().getBoolean(name, getResources().getBoolean(res)); + } + + public void switchToConversation(Conversation conversation) { + switchToConversation(conversation, null); + } + + public void switchToConversationAndQuote(Conversation conversation, String text) { + switchToConversation(conversation, text, true, null, false, false); + } + + public void switchToConversation(Conversation conversation, String text) { + switchToConversation(conversation, text, false, null, false, false); + } + + public void switchToConversationDoNotAppend(Conversation conversation, String text) { + switchToConversation(conversation, text, false, null, false, true); + } + + public void highlightInMuc(Conversation conversation, String nick) { + switchToConversation(conversation, null, false, nick, false, false); + } + + public void privateMsgInMuc(Conversation conversation, String nick) { + switchToConversation(conversation, null, false, nick, true, false); + } + + private void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) { + Intent intent = new Intent(this, ConversationsActivity.class); + intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); + intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); + if (text != null) { + intent.putExtra(Intent.EXTRA_TEXT, text); + if (asQuote) { + intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true); + } + } + if (nick != null) { + intent.putExtra(ConversationsActivity.EXTRA_NICK, nick); + intent.putExtra(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, pm); + } + if (doNotAppend) { + intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true); + } + intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + finish(); + } + + public void switchToContactDetails(Contact contact) { + switchToContactDetails(contact, null); + } + + public void switchToContactDetails(Contact contact, String messageFingerprint) { + Intent intent = new Intent(this, ContactDetailsActivity.class); + intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); + intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toEscapedString()); + intent.putExtra("contact", contact.getJid().toEscapedString()); + intent.putExtra("fingerprint", messageFingerprint); + startActivity(intent); + } + + public void switchToAccount(Account account, String fingerprint) { + switchToAccount(account, false, fingerprint); + } + + public void switchToAccount(Account account) { + switchToAccount(account, false, null); + } + + public void switchToAccount(Account account, boolean init, String fingerprint) { + Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().asBareJid().toEscapedString()); + intent.putExtra("init", init); + if (init) { + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION); + } + if (fingerprint != null) { + intent.putExtra("fingerprint", fingerprint); + } + startActivity(intent); + if (init) { + overridePendingTransition(0, 0); + } + } + + protected void delegateUriPermissionsToService(Uri uri) { + Intent intent = new Intent(this, XmppConnectionService.class); + intent.setAction(Intent.ACTION_SEND); + intent.setData(uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + try { + startService(intent); + } catch (Exception e) { + Log.e(Config.LOGTAG, "unable to delegate uri permission", e); + } + } + + protected void inviteToConversation(Conversation conversation) { + startActivityForResult(ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION); + } + + protected void announcePgp(final Account account, final Conversation conversation, Intent intent, final Runnable onSuccess) { + if (account.getPgpId() == 0) { + choosePgpSignId(account); + } else { + String status = null; + if (manuallyChangePresence()) { + status = account.getPresenceStatusMessage(); + } + if (status == null) { + status = ""; + } + xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback() { + + @Override + public void userInputRequired(PendingIntent pi, String signature) { + try { + startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); + } catch (final SendIntentException ignored) { + } + } + + @Override + public void success(String signature) { + account.setPgpSignature(signature); + xmppConnectionService.databaseBackend.updateAccount(account); + xmppConnectionService.sendPresence(account); + if (conversation != null) { + conversation.setNextEncryption(Message.ENCRYPTION_PGP); + xmppConnectionService.updateConversation(conversation); + refreshUi(); + } + if (onSuccess != null) { + runOnUiThread(onSuccess); + } + } + + @Override + public void error(int error, String signature) { + if (error == 0) { + account.setPgpSignId(0); + account.unsetPgpSignature(); + xmppConnectionService.databaseBackend.updateAccount(account); + choosePgpSignId(account); + } else { + displayErrorDialog(error); + } + } + }); + } + } + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + protected void setListItemBackgroundOnView(View view) { + int sdk = android.os.Build.VERSION.SDK_INT; + if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) { + view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground)); + } else { + view.setBackground(getResources().getDrawable(R.drawable.greybackground)); + } + } + + protected void choosePgpSignId(Account account) { + xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback() { + @Override + public void success(Account account1) { + } + + @Override + public void error(int errorCode, Account object) { + + } + + @Override + public void userInputRequired(PendingIntent pi, Account object) { + try { + startIntentSenderForResult(pi.getIntentSender(), + REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0); + } catch (final SendIntentException ignored) { + } + } }); - } + } - protected boolean hasStoragePermission(int requestCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode); - return false; - } else { - return true; - } - } else { - return true; - } - } + protected void displayErrorDialog(final int errorCode) { + runOnUiThread(() -> { + Builder builder = new Builder(XmppActivity.this); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setTitle(getString(R.string.error)); + builder.setMessage(errorCode); + builder.setNeutralButton(R.string.accept, null); + builder.create().show(); + }); - protected void onActivityResult(int requestCode, int resultCode, final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) { - mPendingConferenceInvite = ConferenceInvite.parse(data); - if (xmppConnectionServiceBound && mPendingConferenceInvite != null) { - if (mPendingConferenceInvite.execute(this)) { - mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG); - mToast.show(); - } - mPendingConferenceInvite = null; - } - } - } + } - public boolean copyTextToClipboard(String text, int labelResId) { - ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - String label = getResources().getString(labelResId); - if (mClipBoardManager != null) { - ClipData mClipData = ClipData.newPlainText(label, text); - mClipBoardManager.setPrimaryClip(mClipData); - return true; - } - return false; - } + protected void showAddToRosterDialog(final Contact contact) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(contact.getJid().toString()); + builder.setMessage(getString(R.string.not_in_roster)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> xmppConnectionService.createContact(contact, true)); + builder.create().show(); + } - protected boolean manuallyChangePresence() { - return getBooleanPreference(SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence); - } + private void showAskForPresenceDialog(final Contact contact) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(contact.getJid().toString()); + builder.setMessage(R.string.request_presence_updates); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.request_now, + (dialog, which) -> { + if (xmppConnectionServiceBound) { + xmppConnectionService.sendPresencePacket(contact + .getAccount(), xmppConnectionService + .getPresenceGenerator() + .requestPresenceUpdatesFrom(contact)); + } + }); + builder.create().show(); + } - protected String getShareableUri() { - return getShareableUri(false); - } + protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback) { + quickEdit(previousValue, callback, hint, false, false); + } - protected String getShareableUri(boolean http) { - return null; - } + protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback, boolean permitEmpty) { + quickEdit(previousValue, callback, hint, false, permitEmpty); + } - protected void shareLink(boolean http) { - String uri = getShareableUri(http); - if (uri == null || uri.isEmpty()) { - return; - } - Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http)); - try { - startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with))); - } catch (ActivityNotFoundException e) { - Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show(); - } - } + protected void quickPasswordEdit(String previousValue, OnValueEdited callback) { + quickEdit(previousValue, callback, R.string.password, true, false); + } - protected void launchOpenKeyChain(long keyId) { - PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine(); - try { - startIntentSenderForResult( - pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0, - 0, 0); - } catch (Throwable e) { - Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show(); - } - } + @SuppressLint("InflateParams") + private void quickEdit(final String previousValue, + final OnValueEdited callback, + final @StringRes int hint, + boolean password, + boolean permitEmpty) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_quickedit, null, false); + if (password) { + binding.inputEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + } + builder.setPositiveButton(R.string.accept, null); + if (hint != 0) { + binding.inputLayout.setHint(getString(hint)); + } + binding.inputEditText.requestFocus(); + if (previousValue != null) { + binding.inputEditText.getText().append(previousValue); + } + builder.setView(binding.getRoot()); + builder.setNegativeButton(R.string.cancel, null); + final AlertDialog dialog = builder.create(); + dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText)); + dialog.show(); + View.OnClickListener clickListener = v -> { + String value = binding.inputEditText.getText().toString(); + if (!value.equals(previousValue) && (!value.trim().isEmpty() || permitEmpty)) { + String error = callback.onValueEdited(value); + if (error != null) { + binding.inputLayout.setError(error); + return; + } + } + SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); + dialog.dismiss(); + }; + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener); + dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> { + SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); + dialog.dismiss(); + })); + dialog.setCanceledOnTouchOutside(false); + dialog.setOnDismissListener(dialog1 -> { + SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); + }); + } - @Override - public void onResume() { - super.onResume(); - } + protected boolean hasStoragePermission(int requestCode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode); + return false; + } else { + return true; + } + } else { + return true; + } + } - protected int findTheme() { - return ThemeHelper.find(this); - } + protected void onActivityResult(int requestCode, int resultCode, final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) { + mPendingConferenceInvite = ConferenceInvite.parse(data); + if (xmppConnectionServiceBound && mPendingConferenceInvite != null) { + if (mPendingConferenceInvite.execute(this)) { + mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG); + mToast.show(); + } + mPendingConferenceInvite = null; + } + } + } - @Override - public void onPause() { - super.onPause(); - } + public boolean copyTextToClipboard(String text, int labelResId) { + ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + String label = getResources().getString(labelResId); + if (mClipBoardManager != null) { + ClipData mClipData = ClipData.newPlainText(label, text); + mClipBoardManager.setPrimaryClip(mClipData); + return true; + } + return false; + } - @Override - public boolean onMenuOpened(int id, Menu menu) { - if(id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) { - MenuDoubleTabUtil.recordMenuOpen(); - } - return super.onMenuOpened(id, menu); - } + protected boolean manuallyChangePresence() { + return getBooleanPreference(SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence); + } - protected void showQrCode() { - showQrCode(getShareableUri()); - } + protected String getShareableUri() { + return getShareableUri(false); + } - protected void showQrCode(final String uri) { - if (uri == null || uri.isEmpty()) { - return; - } - Point size = new Point(); - getWindowManager().getDefaultDisplay().getSize(size); - final int width = (size.x < size.y ? size.x : size.y); - Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width); - ImageView view = new ImageView(this); - view.setBackgroundColor(Color.WHITE); - view.setImageBitmap(bitmap); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setView(view); - builder.create().show(); - } + protected String getShareableUri(boolean http) { + return null; + } - protected Account extractAccount(Intent intent) { - final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null; - try { - return jid != null ? xmppConnectionService.findAccountByJid(Jid.ofEscaped(jid)) : null; - } catch (IllegalArgumentException e) { - return null; - } - } + protected void shareLink(boolean http) { + String uri = getShareableUri(http); + if (uri == null || uri.isEmpty()) { + return; + } + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http)); + try { + startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with))); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show(); + } + } - public AvatarService avatarService() { - return xmppConnectionService.getAvatarService(); - } + protected void launchOpenKeyChain(long keyId) { + PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine(); + try { + startIntentSenderForResult( + pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0, + 0, 0); + } catch (Throwable e) { + Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show(); + } + } - public void loadBitmap(Message message, ImageView imageView) { - Bitmap bm; - try { - bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true); - } catch (IOException e) { - bm = null; - } - if (bm != null) { - cancelPotentialWork(message, imageView); - imageView.setImageBitmap(bm); - imageView.setBackgroundColor(0x00000000); - } else { - if (cancelPotentialWork(message, imageView)) { - imageView.setBackgroundColor(0xff333333); - imageView.setImageDrawable(null); - final BitmapWorkerTask task = new BitmapWorkerTask(imageView); - final AsyncDrawable asyncDrawable = new AsyncDrawable( - getResources(), null, task); - imageView.setImageDrawable(asyncDrawable); - try { - task.execute(message); - } catch (final RejectedExecutionException ignored) { - ignored.printStackTrace(); - } - } - } - } + @Override + public void onResume() { + super.onResume(); + } - protected interface OnValueEdited { - String onValueEdited(String value); - } + protected int findTheme() { + return ThemeHelper.find(this); + } - public static class ConferenceInvite { - private String uuid; - private List jids = new ArrayList<>(); + @Override + public void onPause() { + super.onPause(); + } - public static ConferenceInvite parse(Intent data) { - ConferenceInvite invite = new ConferenceInvite(); - invite.uuid = data.getStringExtra(ChooseContactActivity.EXTRA_CONVERSATION); - if (invite.uuid == null) { - return null; - } - invite.jids.addAll(ChooseContactActivity.extractJabberIds(data)); - return invite; - } + @Override + public boolean onMenuOpened(int id, Menu menu) { + if (id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) { + MenuDoubleTabUtil.recordMenuOpen(); + } + return super.onMenuOpened(id, menu); + } - public boolean execute(XmppActivity activity) { - XmppConnectionService service = activity.xmppConnectionService; - Conversation conversation = service.findConversationByUuid(this.uuid); - if (conversation == null) { - return false; - } - if (conversation.getMode() == Conversation.MODE_MULTI) { - for (Jid jid : jids) { - service.invite(conversation, jid); - } - return false; - } else { - jids.add(conversation.getJid().asBareJid()); - return service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback); - } - } - } + protected void showQrCode() { + showQrCode(getShareableUri()); + } - static class BitmapWorkerTask extends AsyncTask { - private final WeakReference imageViewReference; - private Message message = null; + protected void showQrCode(final String uri) { + if (uri == null || uri.isEmpty()) { + return; + } + Point size = new Point(); + getWindowManager().getDefaultDisplay().getSize(size); + final int width = (size.x < size.y ? size.x : size.y); + Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width); + ImageView view = new ImageView(this); + view.setBackgroundColor(Color.WHITE); + view.setImageBitmap(bitmap); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setView(view); + builder.create().show(); + } - private BitmapWorkerTask(ImageView imageView) { - this.imageViewReference = new WeakReference<>(imageView); - } + protected Account extractAccount(Intent intent) { + final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null; + try { + return jid != null ? xmppConnectionService.findAccountByJid(Jid.ofEscaped(jid)) : null; + } catch (IllegalArgumentException e) { + return null; + } + } - @Override - protected Bitmap doInBackground(Message... params) { - if (isCancelled()) { - return null; - } - message = params[0]; - try { - final XmppActivity activity = find(imageViewReference); - if (activity != null && activity.xmppConnectionService != null) { - return activity.xmppConnectionService.getFileBackend().getThumbnail(message, (int) (activity.metrics.density * 288), false); - } else { - return null; - } - } catch (IOException e) { - return null; - } - } + public AvatarService avatarService() { + return xmppConnectionService.getAvatarService(); + } - @Override - protected void onPostExecute(final Bitmap bitmap) { - if (!isCancelled()) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(bitmap == null ? 0xff333333 : 0x00000000); - } - } - } - } + public void loadBitmap(Message message, ImageView imageView) { + Bitmap bm; + try { + bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true); + } catch (IOException e) { + bm = null; + } + if (bm != null) { + cancelPotentialWork(message, imageView); + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + if (cancelPotentialWork(message, imageView)) { + imageView.setBackgroundColor(0xff333333); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable( + getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(message); + } catch (final RejectedExecutionException ignored) { + ignored.printStackTrace(); + } + } + } + } - private static class AsyncDrawable extends BitmapDrawable { - private final WeakReference bitmapWorkerTaskReference; + protected interface OnValueEdited { + String onValueEdited(String value); + } - private AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { - super(res, bitmap); - bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); - } + public static class ConferenceInvite { + private String uuid; + private List jids = new ArrayList<>(); - private BitmapWorkerTask getBitmapWorkerTask() { - return bitmapWorkerTaskReference.get(); - } - } + public static ConferenceInvite parse(Intent data) { + ConferenceInvite invite = new ConferenceInvite(); + invite.uuid = data.getStringExtra(ChooseContactActivity.EXTRA_CONVERSATION); + if (invite.uuid == null) { + return null; + } + invite.jids.addAll(ChooseContactActivity.extractJabberIds(data)); + return invite; + } - public static XmppActivity find(@NonNull WeakReference viewWeakReference) { - final View view = viewWeakReference.get(); - return view == null ? null : find(view); - } + public boolean execute(XmppActivity activity) { + XmppConnectionService service = activity.xmppConnectionService; + Conversation conversation = service.findConversationByUuid(this.uuid); + if (conversation == null) { + return false; + } + if (conversation.getMode() == Conversation.MODE_MULTI) { + for (Jid jid : jids) { + service.invite(conversation, jid); + } + return false; + } else { + jids.add(conversation.getJid().asBareJid()); + return service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback); + } + } + } - public static XmppActivity find(@NonNull final View view) { - Context context = view.getContext(); - while (context instanceof ContextWrapper) { - if (context instanceof XmppActivity) { - return (XmppActivity) context; - } - context = ((ContextWrapper)context).getBaseContext(); - } - return null; - } + static class BitmapWorkerTask extends AsyncTask { + private final WeakReference imageViewReference; + private Message message = null; + + private BitmapWorkerTask(ImageView imageView) { + this.imageViewReference = new WeakReference<>(imageView); + } + + @Override + protected Bitmap doInBackground(Message... params) { + if (isCancelled()) { + return null; + } + message = params[0]; + try { + final XmppActivity activity = find(imageViewReference); + if (activity != null && activity.xmppConnectionService != null) { + return activity.xmppConnectionService.getFileBackend().getThumbnail(message, (int) (activity.metrics.density * 288), false); + } else { + return null; + } + } catch (IOException e) { + return null; + } + } + + @Override + protected void onPostExecute(final Bitmap bitmap) { + if (!isCancelled()) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(bitmap == null ? 0xff333333 : 0x00000000); + } + } + } + } + + private static class AsyncDrawable extends BitmapDrawable { + private final WeakReference bitmapWorkerTaskReference; + + private AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); + } + + private BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + + public static XmppActivity find(@NonNull WeakReference viewWeakReference) { + final View view = viewWeakReference.get(); + return view == null ? null : find(view); + } + + public static XmppActivity find(@NonNull final View view) { + Context context = view.getContext(); + while (context instanceof ContextWrapper) { + if (context instanceof XmppActivity) { + return (XmppActivity) context; + } + context = ((ContextWrapper) context).getBaseContext(); + } + return null; + } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java index 61177d0dc..f6017cf4d 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java @@ -27,7 +27,7 @@ public class KnownHostsAdapter extends ArrayAdapter { if (split.length == 1) { final String local = split[0].toLowerCase(Locale.ENGLISH); if (Config.QUICKSY_DOMAIN != null && E164_PATTERN.matcher(local).matches()) { - suggestions.add(local + '@' + Config.QUICKSY_DOMAIN); + suggestions.add(local + '@' + Config.QUICKSY_DOMAIN.toEscapedString()); } else { for (String domain : domains) { suggestions.add(local + '@' + domain); diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index d4bd33bb2..14d5749a1 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -35,7 +35,6 @@ import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; -import android.util.Log; import java.io.File; import java.util.ArrayList; @@ -43,7 +42,6 @@ import java.util.Collections; import java.util.List; import java.util.UUID; -import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.MimeUtils; @@ -113,7 +111,7 @@ public class Attachment implements Parcelable { } public static boolean canBeSendInband(final List attachments) { - for(Attachment attachment : attachments) { + for (Attachment attachment : attachments) { if (attachment.type != Type.LOCATION) { return false; } @@ -122,21 +120,21 @@ public class Attachment implements Parcelable { } public static List of(final Context context, Uri uri, Type type) { - final String mime = type == Type.LOCATION ?null :MimeUtils.guessMimeTypeFromUri(context, uri); + final String mime = type == Type.LOCATION ? null : MimeUtils.guessMimeTypeFromUri(context, uri); return Collections.singletonList(new Attachment(uri, type, mime)); } public static List of(final Context context, List uris) { List attachments = new ArrayList<>(); - for(Uri uri : uris) { + for (Uri uri : uris) { final String mime = MimeUtils.guessMimeTypeFromUri(context, uri); - attachments.add(new Attachment(uri, mime != null && mime.startsWith("image/") ? Type.IMAGE : Type.FILE,mime)); + attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime)); } return attachments; } public static Attachment of(UUID uuid, final File file, String mime) { - return new Attachment(uuid, Uri.fromFile(file),mime != null && (mime.startsWith("image/") || mime.startsWith("video/")) ? Type.IMAGE : Type.FILE, mime); + return new Attachment(uuid, Uri.fromFile(file), mime != null && (isImage(mime) || mime.startsWith("video/")) ? Type.IMAGE : Type.FILE, mime); } public static List extractAttachments(final Context context, final Intent intent, Type type) { @@ -151,9 +149,7 @@ public class Attachment implements Parcelable { if (clipData != null) { for (int i = 0; i < clipData.getItemCount(); ++i) { final Uri uri = clipData.getItemAt(i).getUri(); - Log.d(Config.LOGTAG,"uri="+uri+" contentType="+contentType); final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, contentType); - Log.d(Config.LOGTAG,"mime="+mime); uris.add(new Attachment(uri, type, mime)); } } @@ -165,12 +161,12 @@ public class Attachment implements Parcelable { } public boolean renderThumbnail() { - return type == Type.IMAGE || (type == Type.FILE && mime != null && renderFileThumbnail(mime)); + return type == Type.IMAGE || (type == Type.FILE && mime != null && renderFileThumbnail(mime)); } private static boolean renderFileThumbnail(final String mime) { return mime.startsWith("video/") - || mime.startsWith("image/") + || isImage(mime) || (Compatibility.runsTwentyOne() && "application/pdf".equals(mime)); } @@ -181,4 +177,8 @@ public class Attachment implements Parcelable { public UUID getUuid() { return uuid; } + + private static boolean isImage(final String mime) { + return mime.startsWith("image/") && !mime.equals("image/svg+xml"); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java b/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java index 14e82b406..2f29e5b76 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java +++ b/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java @@ -106,12 +106,24 @@ public class PresenceSelector { builder.setPositiveButton( R.string.ok, (dialog, which) -> onFullJidSelected.onFullJidSelected( - Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), resourceArray[selectedResource.get()]) + getNextCounterpart(contact, resourceArray[selectedResource.get()]) ) ); builder.create().show(); } + public static Jid getNextCounterpart(final Contact contact, final String resource) { + return getNextCounterpart(contact.getJid(), resource); + } + + public static Jid getNextCounterpart(final Jid jid, final String resource) { + if (resource.isEmpty()) { + return jid.asBareJid(); + } else { + return jid.withResource(resource); + } + } + public static void warnMutualPresenceSubscription(Activity activity, final Conversation conversation, final OnPresenceSelected listener) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(conversation.getContact().getJid().toString()); diff --git a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java index 09b65f694..a324b242e 100644 --- a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java @@ -39,7 +39,7 @@ public class AccountUtils { for (Account account : service.getAccounts()) { if (account.getStatus() != Account.State.DISABLED) { if (Config.DOMAIN_LOCK != null) { - accounts.add(account.getJid().toEscapedString()); + accounts.add(account.getJid().getEscapedLocal()); } else { accounts.add(account.getJid().asBareJid().toEscapedString()); } diff --git a/src/main/java/eu/siacs/conversations/utils/JidHelper.java b/src/main/java/eu/siacs/conversations/utils/JidHelper.java index 603899fca..c53af65ed 100644 --- a/src/main/java/eu/siacs/conversations/utils/JidHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/JidHelper.java @@ -34,6 +34,7 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; @@ -59,4 +60,8 @@ public class JidHelper { } } + public static boolean isQuicksyDomain(final Jid jid) { + return Config.QUICKSY_DOMAIN != null && Config.QUICKSY_DOMAIN.equals(jid.getDomain()); + } + } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 679a30abe..bd50a22a2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -16,10 +16,10 @@ public abstract class AbstractJingleConnection { public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-"; public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-"; - protected final JingleConnectionManager jingleConnectionManager; + final JingleConnectionManager jingleConnectionManager; protected final XmppConnectionService xmppConnectionService; protected final Id id; - protected final Jid initiator; + private final Jid initiator; AbstractJingleConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) { this.jingleConnectionManager = jingleConnectionManager; @@ -47,8 +47,9 @@ public abstract class AbstractJingleConnection { public final String sessionId; private Id(final Account account, final Jid with, final String sessionId) { + Preconditions.checkNotNull(account); Preconditions.checkNotNull(with); - Preconditions.checkArgument(with.isFullJid()); + Preconditions.checkNotNull(sessionId); this.account = account; this.with = with; this.sessionId = sessionId; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java index d1bfc987a..0fd0eabaf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java @@ -147,7 +147,6 @@ public class JingleCandidate { } public String toString() { - return this.getHost() + ":" + this.getPort() + " (prio=" - + this.getPriority() + ")"; + return String.format("%s:%s (priority=%s,ours=%s)", getHost(), getPort(), getPriority(), isOurs()); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 4a1f7148f..03bafb275 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -11,7 +11,6 @@ import com.google.common.cache.CacheBuilder; import com.google.common.collect.Collections2; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableSet; -import com.google.j2objc.annotations.Weak; import java.lang.ref.WeakReference; import java.security.SecureRandom; @@ -57,8 +56,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { private final HashMap rtpSessionProposals = new HashMap<>(); private final ConcurrentHashMap connections = new ConcurrentHashMap<>(); - private final Cache endedSessions = CacheBuilder.newBuilder() - .expireAfterWrite(30, TimeUnit.MINUTES) + private final Cache terminatedSessions = CacheBuilder.newBuilder() + .expireAfterWrite(24, TimeUnit.HOURS) .build(); private HashMap primaryCandidates = new HashMap<>(); @@ -92,7 +91,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { connection = new JingleFileTransferConnection(this, id, from); } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) && !usesTor(account)) { - final boolean sessionEnded = this.endedSessions.asMap().containsKey(PersistableSessionId.of(id)); + final boolean sessionEnded = this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id)); final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with); if (isBusy() || sessionEnded || stranger) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejected session with " + id.with + " because busy. sessionEnded=" + sessionEnded + ", stranger=" + stranger); @@ -447,6 +446,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { } void finishConnection(final AbstractJingleConnection connection) { + this.connections.remove(connection.getId()); + } + + void finishConnectionOrThrow(final AbstractJingleConnection connection) { final AbstractJingleConnection.Id id = connection.getId(); if (this.connections.remove(id) == null) { throw new IllegalStateException(String.format("Unable to finish connection with id=%s", id.toString())); @@ -680,8 +683,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { throw e; } - void endSession(AbstractJingleConnection.Id id, final AbstractJingleConnection.State state) { - this.endedSessions.put(PersistableSessionId.of(id), state); + void setTerminalSessionState(AbstractJingleConnection.Id id, final RtpEndUserState state, final Set media) { + this.terminatedSessions.put(PersistableSessionId.of(id), new TerminatedRtpSession(state, media)); + } + + public TerminatedRtpSession getTerminalSessionState(final Jid with, final String sessionId) { + return this.terminatedSessions.getIfPresent(new PersistableSessionId(with, sessionId)); } private static class PersistableSessionId { @@ -712,6 +719,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + public static class TerminatedRtpSession { + public final RtpEndUserState state; + public final Set media; + + TerminatedRtpSession(RtpEndUserState state, Set media) { + this.state = state; + this.media = media; + } + } + public enum DeviceDiscoveryState { SEARCHING, DISCOVERED, FAILED; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 2f84f7c45..408635f93 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -4,7 +4,10 @@ import android.util.Base64; import android.util.Log; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.Collections2; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.Iterables; import java.io.File; import java.io.FileInputStream; @@ -197,7 +200,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } }; - public JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { + JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); } @@ -414,15 +417,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } private List getRemoteFeatures() { - final Jid jid = this.id.with; - String resource = jid != null ? jid.getResource() : null; - if (resource != null) { - Presence presence = this.id.account.getRoster().getContact(jid).getPresences().get(resource); - ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null; - return result == null ? Collections.emptyList() : result.getFeatures(); - } else { - return Collections.emptyList(); - } + final String resource = Strings.nullToEmpty(this.id.with.getResource()); + final Presence presence = this.id.account.getRoster().getContact(id.with).getPresences().get(resource); + final ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null; + return result == null ? Collections.emptyList() : result.getFeatures(); } private void init(JinglePacket packet) { //should move to deliverPacket @@ -442,7 +440,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple try { senders = content.getSenders(); } catch (final Exception e) { - senders = Content.Senders.INITIATOR; + senders = Content.Senders.INITIATOR; } this.contentSenders = senders; this.contentName = content.getAttribute("name"); @@ -632,10 +630,14 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INFO); packet.addJingleChild(checksum); - this.sendJinglePacket(packet); + xmppConnectionService.sendIqPacket(id.account, packet, (account, response) -> { + if (response.getType() == IqPacket.TYPE.ERROR) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring error response to our session-info (hash transmission)"); + } + }); } - public Collection getOurCandidates() { + private Collection getOurCandidates() { return Collections2.filter(this.candidates, c -> c != null && c.isOurs()); } @@ -825,8 +827,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.sendFallbackToIbb(); } } else { + //TODO at this point we can already close other connections to free some resources final JingleCandidate candidate = connection.getCandidate(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.getHost() + ":" + candidate.getPort()); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.toString()); this.mJingleStatus = JINGLE_STATUS_TRANSMITTING; if (connection.needsActivation()) { if (connection.getCandidate().isOurs()) { @@ -875,38 +878,23 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } private JingleSocks5Transport chooseConnection() { - JingleSocks5Transport connection = null; - for (Entry cursor : connections - .entrySet()) { - JingleSocks5Transport currentConnection = cursor.getValue(); - // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString()); - if (currentConnection.isEstablished() - && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection - .getCandidate().isOurs()))) { - // Log.d(Config.LOGTAG,"is usable"); - if (connection == null) { - connection = currentConnection; - } else { - if (connection.getCandidate().getPriority() < currentConnection - .getCandidate().getPriority()) { - connection = currentConnection; - } else if (connection.getCandidate().getPriority() == currentConnection - .getCandidate().getPriority()) { - // Log.d(Config.LOGTAG,"found two candidates with same priority"); + final List establishedConnections = FluentIterable.from(connections.entrySet()) + .transform(Entry::getValue) + .filter(c -> (c != null && c.isEstablished() && (c.getCandidate().isUsedByCounterpart() || !c.getCandidate().isOurs()))) + .toSortedList((a, b) -> { + final int compare = Integer.compare(b.getCandidate().getPriority(), a.getCandidate().getPriority()); + if (compare == 0) { if (isInitiator()) { - if (currentConnection.getCandidate().isOurs()) { - connection = currentConnection; - } + //pick the one we sent a candidate-used for (meaning not ours) + return a.getCandidate().isOurs() ? 1 : -1; } else { - if (!currentConnection.getCandidate().isOurs()) { - connection = currentConnection; - } + //pick the one they sent a candidate-used for (meaning ours) + return a.getCandidate().isOurs() ? -1 : 1; } } - } - } - } - return connection; + return compare; + }); + return Iterables.getFirst(establishedConnections, null); } private void sendSuccess() { @@ -1035,7 +1023,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple abort(Reason.CANCEL); } - void abort(final Reason reason) { + private void abort(final Reason reason) { this.disconnectSocks5Connections(); if (this.transport instanceof JingleInBandTransport) { this.transport.disconnect(); @@ -1179,7 +1167,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } } - public int getJingleStatus() { + private int getJingleStatus() { return this.mJingleStatus; } @@ -1222,11 +1210,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple jingleConnectionManager.updateConversationUi(false); } - public String getTransportId() { + String getTransportId() { return this.transportId; } - public FileTransferDescription.Version getFtVersion() { + FileTransferDescription.Version getFtVersion() { return this.description.getVersion(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 8ff496835..e45b79997 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -814,7 +814,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } else if (state == PeerConnection.PeerConnectionState.CLOSED) { return RtpEndUserState.ENDING_CALL; } else { - return RtpEndUserState.CONNECTIVITY_ERROR; + return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; } case REJECTED: case TERMINATED_DECLINED_OR_BUSY: @@ -831,7 +831,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case RETRACTED_RACED: return RtpEndUserState.RETRACTED; case TERMINATED_CONNECTIVITY_ERROR: - return RtpEndUserState.CONNECTIVITY_ERROR; + return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; case TERMINATED_APPLICATION_FAILURE: return RtpEndUserState.APPLICATION_ERROR; } @@ -912,7 +912,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } if (isInState(State.PROCEED)) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ending call while in state PROCEED just means ending the connection"); - this.jingleConnectionManager.endSession(id, State.TERMINATED_SUCCESS); this.webRTCWrapper.close(); transitionOrThrow(State.TERMINATED_SUCCESS); //arguably this wasn't success; but not a real failure either this.finish(); @@ -1189,7 +1188,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (isTerminated()) { this.cancelRingingTimeout(); this.webRTCWrapper.verifyClosed(); - this.jingleConnectionManager.finishConnection(this); + this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia()); + this.jingleConnectionManager.finishConnectionOrThrow(this); } else { throw new IllegalStateException(String.format("Unable to call finish from %s", this.state)); } @@ -1219,7 +1219,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final Conversational conversational = message.getConversation(); if (conversational instanceof Conversation) { ((Conversation) conversational).add(this.message); - xmppConnectionService.databaseBackend.createMessage(message); + xmppConnectionService.createMessageAsync(message); xmppConnectionService.updateConversationUi(); } else { throw new IllegalStateException("Somehow the conversation in a message was a stub"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index 398777cfe..3b97fcbc7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -11,6 +11,7 @@ public enum RtpEndUserState { ENDED, //close UI DECLINED_OR_BUSY, //other party declined; no retry button CONNECTIVITY_ERROR, //network error; retry button + CONNECTIVITY_LOST_ERROR, //network error but for call duration > 0 RETRACTED, //user pressed home or power button during 'ringing' - shows retry button APPLICATION_ERROR //something rather bad happened; libwebrtc failed or we got in IQ-error } diff --git a/src/main/res/drawable-hdpi/ic_help_white_24dp.png b/src/main/res/drawable-hdpi/ic_help_white_24dp.png new file mode 100644 index 000000000..5664f9532 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_help_white_24dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_help_white_24dp.png b/src/main/res/drawable-mdpi/ic_help_white_24dp.png new file mode 100644 index 000000000..db699622b Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_help_white_24dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_help_white_24dp.png b/src/main/res/drawable-xhdpi/ic_help_white_24dp.png new file mode 100644 index 000000000..2d11cf47a Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_help_white_24dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_help_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_help_white_24dp.png new file mode 100644 index 000000000..d49181785 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_help_white_24dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_help_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_help_white_24dp.png new file mode 100644 index 000000000..8eb7241da Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_help_white_24dp.png differ diff --git a/src/main/res/menu/activity_rtp_session.xml b/src/main/res/menu/activity_rtp_session.xml new file mode 100644 index 000000000..540a9def9 --- /dev/null +++ b/src/main/res/menu/activity_rtp_session.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index b6b6f9058..e68ea721a 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -41,7 +41,7 @@ Moderator Teilnehmer Besucher - Möchtest du %svon deiner Kontaktliste entfernen? Unterhaltungen mit diesem Kontakt werden dabei nicht entfernt. + Möchtest du %s von deiner Kontaktliste entfernen? Unterhaltungen mit diesem Kontakt werden dabei nicht entfernt. Möchtest du %s sperren und keine Nachrichten mehr erhalten? Möchtest du %s entsperren und wieder Nachrichten empfangen? Alle Kontakte von %s sperren? @@ -901,7 +901,8 @@ Klingelt Besetzt Verbindungsaufbau fehlgeschlagen - Rückrufruf + Verbindung unterbrochen + Anruf zurückgenommen App-Fehler Auflegen Laufender Anruf @@ -914,6 +915,7 @@ Entgangener Anruf Audioanruf Videoanruf + Hilfe Dein Mikrofon ist nicht verfügbar Du kannst immer nur einen Anruf zur gleichen Zeit machen. Zurück zum laufenden Aufruf diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index adff37f82..7bada1588 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -901,6 +901,7 @@ Llamando Ocupado No se ha podido realizar la llamada + Conexión perdida Llamada rechazada Fallo en la aplicación Colgar @@ -914,6 +915,7 @@ Llamada perdida Audio llamada Video llamada + Ayuda Tu micrófono no está disponible Solo puedes hacer una llamada a la vez Volver a la llamada en curso diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index f937fce9c..9a758c655 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -901,6 +901,7 @@ Sonando Ocupado Non se pode establecer a chamada + Perdeuse a conexión Chamada cortada Fallo na aplicación Colgar @@ -914,6 +915,7 @@ Chamada perdida Chamada de audio Chamada de vídeo + Axuda O micrófono non está dispoñible Só podes manter unha chamada en cada momento. Voltar á chamada activa diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 526e662d2..c2d1d7d88 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -901,6 +901,7 @@ Sta squillando Occupato Impossibile connettere la chiamata + Connessione persa Chiamata ritirata Errore dell\'app Riaggancia @@ -914,6 +915,7 @@ Chiamata persa Chiamata vocale Chiamata video + Aiuto Il tuo microfono non è disponibile Puoi fare solo una chiamata alla volta. Torna alla chiamata in corso diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index ab712e29f..94ebf2f5f 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -919,6 +919,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Dzwonienie Zajęty Nie można wykonać połączenia + Utracono połączenie Anulowane połączenie Błąd aplikacji Rozłącz @@ -932,6 +933,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Nieodebrane połączenie Połączenie audio Połączenie wideo + Pomoc Twój mikrofon jest niedostępny Możesz mieć tylko jedno połączenie na raz. Powróć do trwającego połączenia diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 7ad247d6e..be6cd1d42 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -909,6 +909,7 @@ Sună Ocupat Nu s-a putut conecta apelul + Conexiune pierdută Apel anulat Eroare de aplicație Închide @@ -922,6 +923,7 @@ Apel pierdut Apel audio Apel video + Ajutor Microfonul nu este disponibil Puteți avea un singur apel simultan. Reveniți la apelul în curs diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 111a65c43..a5e07707e 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -30,6 +30,7 @@ только что 1 минуту назад %d мин. назад + %d непрочитанных бесед отправка… Расшифровка сообщения. Подождите… OpenPGP зашифр. сообщение @@ -40,12 +41,14 @@ Модератор Участник Посетитель + Вы хотите удалить %s из своего списка контактов? Беседы, связанные с этим контактом, будут сохранены. Вы хотите заблокировать дальнейшие сообщения от %s? Вы хотите разблокировать пользователя %s? Заблокировать всех пользователей домена %s? Разблокировать всех пользователей домена %s? Контакт заблокирован Заблокирован + Вы хотите удалить %s из избранного? Беседы, связанные с данной закладкой, будут сохранены. Создать новый аккаунт на сервере Изменить пароль на сервере Поделиться с @@ -64,14 +67,22 @@ Сохранить ОК Conversations был неожиданно остановлен + Отправляя отчёты об ошибках, вы помогаете совершенствованию Conversations. Отправить сейчас Больше не спрашивать + Не удалось подключиться к учетной записи + Не удалось подключиться к учетным записям + Нажмите, чтобы настроить учетные записи Прикрепить файл + Контакт не находится в вашем списке контактов. Хотите добавить его? Добавить контакт доставка не удалась + Подготовка к передаче изображения + Подготовка к передаче изображений Обмен файлами. Пожалуйста, подождите… Очистить историю Очистить историю + Вы хотите удалить все сообщения в этой беседе?\n\nВнимание: Данная операция не повлияет на сообщения, хранящиеся на других устройствах или серверах. Удалить файл Вы уверены, что хотите удалить этот файл?\n\nПредупреждение: Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах. Закрыть эту беседу @@ -82,16 +93,20 @@ OMEMO зашифр. сообщение v\\OMEMO зашифр. сообщение OpenPGP зашифр. сообщение + Имя уже используется Отправить в незашифрованном виде Расшифровка не удалась. Вероятно, что у вас нет надлежащего ключа. Установите OpenKeychain + Conversations использует OpenKeychain для шифрования и дешифрования сообщений и управления открытыми ключами.\n\nOpenKeychain распространяется под лицензией GPLv3 и доступна для загрузки через F-Droid или Google Play.\n\n(Потребуется перезапуск Conversations после установки.) Перезапуск Установка Пожалуйста, установите OpenKeychain предложение… ожидание… Нет OpenPGP ключа + Conversations не может зашифровать сообщение, потому что ваш собеседник не анонсирует свой открытый ключ.\n\nПожалуйста, попросите вашего собеседника настроить OpenPGP. Нет OpenPGP ключей + Conversations не может зашифровать сообщение, потому что ваши собеседники не анонсируют свои открытые ключи.\n\nПожалуйста, попросите ваших собеседников настроить OpenPGP. Общие Принимать файлы Автоматический приём файлов… @@ -101,6 +116,10 @@ Вибрировать, когда приходят новые сообщения Светодиодное уведомление Мерцание индикатора при получении нового сообщения + Мелодия звонка + Звук уведомления + Звук уведомления о новых сообщениях + Мелодия входящего звонка Грейс-период Дополнительно Не отправлять отчёты об ошибках @@ -258,8 +277,6 @@ размещено на %s Проверка %s на сервере HTTP Вы неподключены. Попробуйте позже - Проверить размер %s - Проверить размер %1$s на %2$s Опции сообщения Цитировать Вставить как цитату @@ -676,7 +693,7 @@ OMEMO нужно будет явно включать для новых бесед. Создать ярлык Размер шрифта - Относительный размер шрифта используемый в приложении. + Относительный размер шрифта, используемый в приложении. Включено по умолчанию Выключено по умолчанию Маленький @@ -737,7 +754,7 @@ Просмотр медиафайлов Файл не прикреплен из соображений безопасности. Качество видео - Низкое качество означает меньшие файлы + Чем ниже качество, тем меньше объем файлов Среднее (360p) Высокое (720р) отменено diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index cf936ef56..affc4780d 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -3,6 +3,7 @@ Inställningar Ny konversation Kontoinställningar + Hantera konto Stäng denna konversation Kontaktdetaljer Gruppchattdetaljer @@ -34,18 +35,20 @@ Avkrypterar meddelande. Vänta… OpenPGP-krypterat meddelande Nick används redan - Ogiltigt nick + Ogiltigt smeknamn Admin Ägare Moderator Deltagare Besökare + Vill du ta bort %s från din kontaktlista? Konversationer med denna kontakt kommer inte tas bort. Vill du blockera %s från att skicka dig meddelanden? Vill du avblockera %s och tillåta denne att skicka dig meddelanden? Blockera alla kontakter från %s? Avblockera alla kontakter från %s? Kontakt blockerad Blockerad + Vill du ta bort %s som ett bokmärke? Konversationer med detta bokmärke kommer inte tas bort. Registrera nytt konto på servern Byt lösenord på server Dela med… @@ -66,9 +69,13 @@ Conversations har kraschat Skicka nu Fråga aldrig igen + Kunde inte ansluta till konto + Kunde inte ansluta till flera konton Bifoga fil Lägg till kontakt sändning misslyckades + Förbereder att skicka bild + Förbereder att skicka bilder Delar filer. Vänta... Rensa historik Rensa konversationshistorik @@ -81,6 +88,7 @@ Skicka OMEMO-krypterat meddelande Skicka v\\OMEMO-krypterat meddelande Skicka OpenPGP-krypterat meddelande + Nytt smeknamn används Skicka okrypterat Avkryptering misslyckades. Du har kanske kanske inte rätt privat nyckel. OpenKeychain @@ -304,6 +312,7 @@ Bannlys nu Kunde inte ändra rollen för %s Privat, medlemsskap krävs + Gör XMPP-adresser synliga för alla Du deltar ej Aldrig Tills vidare @@ -389,6 +398,8 @@ %d meddelanden Ladda fler meddelanden + Ge Conversations tillgång till extern lagring + Ge Conversations tillgång till kameran Synkronisera med kontakter Notifiera för alla meddelanden Notifieringar deaktiverade @@ -442,6 +453,7 @@ Tillåt Saknar rättigheter för access till %s Fjärrserver hittas inte + Kunde inte uppdatera konto Ta bort OMEMO identiteter Ta bort valda nycklar Du måste vara ansluten för att publicera din avatarbild @@ -451,6 +463,7 @@ Din enhet stödjer inte att deaktivera databesparing för Conversations. Denna enhet har verifierats Kopiera fingeravtryck + Streckkoden innehåller inte fingeravtryck för denna konversation. Verifierade fingeravtryck Använd kameran för att scanna en kontakts streckkod Vänta medans nycklar hämtas @@ -554,6 +567,7 @@ e-bok Öppna med... Välj konto + Ange ditt lösenord till kontot %s för att återställa säkerhetskopian. Skapa gruppchatt Skapa sluten gruppchatt Kanalnamn @@ -576,5 +590,7 @@ Detta verkar vara ett domännamn Lägg till ändå Detta ser ut som en kanaladress + Filen du valde är inte en säkerhetskopia till Conversations Om + Aktivera ett konto diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index 48a4733b9..1f78fb09b 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -96,6 +96,7 @@ + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index b718fb116..30cb1ad91 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -903,6 +903,7 @@ Ringing Busy Could not connect call + Connection lost Retracted call App failure Hang up @@ -916,6 +917,7 @@ Missed call Audio call Video call + Help Your microphone is unavailable You can only have one call at a time. Return to ongoing call diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index c31a9d71f..e66214615 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -114,6 +114,7 @@ @drawable/ic_delete_black_24dp @drawable/ic_search_white_24dp + @drawable/ic_help_white_24dp @drawable/ic_lock_open_white_24dp @drawable/ic_settings_black_24dp @drawable/ic_share_white_24dp @@ -267,6 +268,7 @@ @drawable/ic_delete_white_24dp @drawable/ic_search_white_24dp + @drawable/ic_help_white_24dp @drawable/ic_lock_open_white_24dp @drawable/ic_settings_white_24dp @drawable/ic_share_white_24dp