Detailed changes
@@ -1,5 +1,14 @@
# Changelog
+### Version 2.10.9
+
+* Ask for Bluetooth permissions when making A/V calls (You can reject this if you don’t use Bluetooth headsets)
+* Fix bug when calling Movim
+
+### Version 2.10.8
+
+* Fix wrong avatar being shown for group chats
+
### Version 2.10.7
* always ask for battery optimizations opt-out
@@ -103,7 +103,7 @@ dependencies {
implementation 'io.michaelrocks:libphonenumber-android:8.12.49'
implementation 'io.github.nishkarsh:android-permissions:2.1.6'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
- implementation urlFile('https://cloudflare-ipfs.com/ipfs/QmeqMiLxHi8AAjXobxr3QTfa1bSSLyAu86YviAqQnjxCjM/libwebrtc.aar', 'libwebrtc.aar')
+ implementation 'ch.threema:webrtc-android:100.0.0'
// INSERT
}
@@ -0,0 +1,19 @@
+# Making a backup of Conversations
+
+This tutorial explains how you can backup your Conversations data.
+
+**WARNING**: Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device.
+
+1. Make sure that you know the password to your account(s)! You will need it later to decrypt your backup.
+2. Deactivate all your account(s): on the chat screen, tap on the three buttons in the upper right, and go to "manage accounts".
+3. Go back to Settings, scroll down until you find the option to create a new backup. Tap on that option.
+4. Wait, until the notification tells you that the backup is finished.
+5. Move the backup to whatever location you feel save with.
+
+Done!
+
+## Further information / troubleshooting
+### Unable to decrypt
+This backup method will include your OMEMO keys. Due to forward secrecy you will not be able to recover messages sent and received between creating the backup and restoring it. If you have a server side archive (MAM) those messages will be retrieved but displayed as *unable to decrypt*. For technical reasons you might also lose the first message you either sent or receive after the restore; for each conversation you have. This message will then also show up as *unable to decrypt*, but this will automatically recover itself as long as both participants are on Conversations 2.3.11+. Note that this doesn’t happen if you just transfer to a new phone and no messages have been exchanged between backup and restore.
+
+In the vast, vast majority of cases you won’t have to manually delete OMEMO keys or do anything like that. Conversations only introduced the official backup feature in 2.4.0 after making sure the *OMEMO self healing* mechanism introduced in 2.3.11 works fine.
@@ -0,0 +1,42 @@
+# Migrating to a new device
+
+This tutorial explains how you can transfer your Conversations data from an old to a new device. It assumes that you do not have Conversations installed on your new device, yet. It basically consists of three steps:
+
+1. Make a backup (old device)
+2. Move that backup to your new device
+3. Import the backup (new device)
+
+**WARNING**: Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device.
+
+## 1. Make a backup (old device)
+1. Make sure that you know the password to your account(s)! You will need it later to decrypt your backup.
+2. Deactivate all your account(s): on the chat screen, tap on the three buttons in the upper right, and go to "manage accounts".
+3. Go back to Settings, scroll down until you find the option to create a new backup. Tap on that option.
+4. Wait, until the notification tells you that the backup is finished.
+
+## 2. Move that backup to your new device
+1. Locate the backup. You should find it in your Files, either in *Conversations/Backup* or in *Download/Conversations/Backup*. The file is named after your account (*e.g. kim@example.org*). If you have multiple accounts, you find one file for each.
+2. Use your USB cable or bluetooth, your Nextcloud or other cloud storage or pretty much anything you want to copy the backup from the old device to the new device.
+3. Remember the location you saved your backup to. For instance, you might want to save them to the *Download* folder.
+
+## 3. Import the backup (new device)
+1. Install Conversations on your new device.
+2. Open Conversations for the first time.
+3. Tap on "Use other server"
+4. Tap on the three dot menu in the upper right corner and tap on "Import backup"
+5. If your backup files are not listed, tap on the cloud symbol in the upper right corner to choose the files from the where you saved them.
+6. Enter your account password to decrypt the backup.
+7. Remember to activate your account (head back to "manage accounts", see step 1.2).
+8. Check if chats work.
+
+Once confirmed that the new device is running fine you can just uninstall the app from the old device.
+
+Note: The backup only contains your text chats and required encryption keys, all the files need to be transferred separately and put on the new device in the same locations.
+
+Done!
+
+## Further information / troubleshooting
+### Unable to decrypt
+This backup method will include your OMEMO keys. Due to forward secrecy you will not be able to recover messages sent and received between creating the backup and restoring it. If you have a server side archive (MAM) those messages will be retrieved but displayed as *unable to decrypt*. For technical reasons you might also lose the first message you either sent or receive after the restore; for each conversation you have. This message will then also show up as *unable to decrypt*, but this will automatically recover itself as long as both participants are on Conversations 2.3.11+. Note that this doesn’t happen if you just transfer to a new phone and no messages have been exchanged between backup and restore.
+
+In the vast, vast majority of cases you won’t have to manually delete OMEMO keys or do anything like that. Conversations only introduced the official backup feature in 2.4.0 after making sure the *OMEMO self healing* mechanism introduced in 2.3.11 works fine.
@@ -1,5 +1,7 @@
package eu.siacs.conversations.services;
+import static eu.siacs.conversations.utils.Compatibility.s;
+
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -304,7 +306,9 @@ public class ImportBackupService extends Service {
mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title))
.setContentText(getString(R.string.notification_restored_backup_subtitle))
.setAutoCancel(true)
- .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT))
+ .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), s()
+ ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT))
.setSmallIcon(R.drawable.ic_unarchive_white_24dp);
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
}
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="pick_a_server">挑選您的 XMPP 提供者</string>
+ <string name="use_conversations.im">使用 conversations.im</string>
+ <string name="create_new_account">建立新帳戶</string>
+ <string name="your_server_invitation">您的伺服器邀請</string>
+ <string name="share_invite_with">分享邀請至…</string>
+</resources>
@@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
@@ -59,6 +60,10 @@
<intent>
<action android:name="eu.siacs.conversations.location.show" />
</intent>
+ <intent>
+ <action android:name="android.intent.action.VIEW" />
+ <data android:mimeType="resource/folder" />
+ </intent>
</queries>
@@ -207,8 +207,8 @@ public class PgpDecryptionService {
}
}
final String url = message.getFileParams().url;
- mXmppConnectionService.getFileBackend().updateFileParams(message, url);
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ mXmppConnectionService.getFileBackend().updateFileParams(message, url);
mXmppConnectionService.updateMessage(message);
if (!inputFile.delete()) {
Log.w(Config.LOGTAG,"unable to delete pgp encrypted source file "+inputFile.getAbsolutePath());
@@ -144,6 +144,7 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
@Override
public void onFailure(@NotNull final Throwable throwable) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable);
+ // TODO consider fall back to jingle in 1-on-1 chats with exactly one online presence
fail(throwable.getMessage());
}
}, MoreExecutors.directExecutor());
@@ -126,7 +126,7 @@ public abstract class AbstractParser {
return user;
}
- public static String extractErrorMessage(Element packet) {
+ public static String extractErrorMessage(final Element packet) {
final Element error = packet.findChild("error");
if (error != null && error.getChildren().size() > 0) {
final List<String> errorNames = orderedElementNames(error.getChildren());
@@ -196,8 +196,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
private void parseEvent(final Element event, final Jid from, final Account account) {
- Element items = event.findChild("items");
- String node = items == null ? null : items.getAttribute("node");
+ final Element items = event.findChild("items");
+ final String node = items == null ? null : items.getAttribute("node");
if ("urn:xmpp:avatar:metadata".equals(node)) {
Avatar avatar = Avatar.parseMetadata(items);
if (avatar != null) {
@@ -327,7 +327,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
- mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId);
+ final String message = extractErrorMessage(packet);
+ mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, message);
return true;
}
mXmppConnectionService.markMessage(account,
@@ -1001,7 +1002,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
- if (event != null && InvalidJid.hasValidFrom(original)) {
+ if (event != null && InvalidJid.hasValidFrom(original) && original.getFrom().isBareJid()) {
if (event.hasChild("items")) {
parseEvent(event, original.getFrom(), account);
} else if (event.hasChild("delete")) {
@@ -1013,6 +1014,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
final String nick = packet.findChildContent("nick", Namespace.NICK);
if (nick != null && InvalidJid.hasValidFrom(original)) {
+ if (mXmppConnectionService.isMuc(account, from)) {
+ return;
+ }
final Contact contact = account.getRoster().getContact(from);
if (contact.setPresenceName(nick)) {
mXmppConnectionService.syncRoster(account);
@@ -70,7 +70,6 @@ import eu.siacs.conversations.services.AttachFileToConversationRunnable;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.adapter.MediaAdapter;
import eu.siacs.conversations.ui.util.Attachment;
-import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.utils.FileWriterException;
@@ -404,25 +403,23 @@ public class FileBackend {
public static Uri getMediaUri(Context context, File file) {
final String filePath = file.getAbsolutePath();
- final Cursor cursor;
- try {
- cursor =
- context.getContentResolver()
- .query(
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- new String[] {MediaStore.Images.Media._ID},
- MediaStore.Images.Media.DATA + "=? ",
- new String[] {filePath},
- null);
- } catch (SecurityException e) {
- return null;
- }
- if (cursor != null && cursor.moveToFirst()) {
- final int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
- cursor.close();
- return Uri.withAppendedPath(
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
- } else {
+ try (final Cursor cursor =
+ context.getContentResolver()
+ .query(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ new String[] {MediaStore.Images.Media._ID},
+ MediaStore.Images.Media.DATA + "=? ",
+ new String[] {filePath},
+ null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ final int id =
+ cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
+ return Uri.withAppendedPath(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
+ } else {
+ return null;
+ }
+ } catch (final Exception e) {
return null;
}
}
@@ -1533,43 +1530,69 @@ public class FileBackend {
updateFileParams(message, null);
}
- public void updateFileParams(Message message, String url) {
- DownloadableFile file = getFile(message);
+ public void updateFileParams(final Message message, final String url) {
+ final boolean encrypted =
+ message.getEncryption() == Message.ENCRYPTION_PGP
+ || message.getEncryption() == Message.ENCRYPTION_DECRYPTED;
+ final DownloadableFile file = getFile(message);
final String mime = file.getMimeType();
final boolean privateMessage = message.isPrivateMessage();
final boolean image =
message.getType() == Message.TYPE_IMAGE
|| (mime != null && mime.startsWith("image/"));
- final boolean video = mime != null && mime.startsWith("video/");
- final boolean audio = mime != null && mime.startsWith("audio/");
- final boolean pdf = "application/pdf".equals(mime);
Message.FileParams fileParams = new Message.FileParams();
if (url != null) {
fileParams.url = url;
}
- fileParams.size = file.getSize();
- if (image || video || pdf) {
- try {
- final Dimensions dimensions;
- if (video) {
- dimensions = getVideoDimensions(file);
- } else if (pdf) {
- dimensions = getPdfDocumentDimensions(file);
- } else {
- dimensions = getImageDimensions(file);
+ if (encrypted && !file.exists()) {
+ Log.d(Config.LOGTAG, "skipping updateFileParams because file is encrypted");
+ final DownloadableFile encryptedFile = getFile(message, false);
+ fileParams.size = encryptedFile.getSize();
+ } else {
+ Log.d(Config.LOGTAG, "running updateFileParams");
+ final boolean ambiguous = MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime);
+ final boolean video = mime != null && mime.startsWith("video/");
+ final boolean audio = mime != null && mime.startsWith("audio/");
+ final boolean pdf = "application/pdf".equals(mime);
+ fileParams.size = file.getSize();
+ if (ambiguous) {
+ try {
+ final Dimensions dimensions = getVideoDimensions(file);
+ if (dimensions.valid()) {
+ Log.d(Config.LOGTAG, "ambiguous file " + mime + " is video");
+ fileParams.width = dimensions.width;
+ fileParams.height = dimensions.height;
+ } else {
+ Log.d(Config.LOGTAG, "ambiguous file " + mime + " is audio");
+ fileParams.runtime = getMediaRuntime(file);
+ }
+ } catch (final NotAVideoFile e) {
+ Log.d(Config.LOGTAG, "ambiguous file " + mime + " is audio");
+ fileParams.runtime = getMediaRuntime(file);
}
- if (dimensions.valid()) {
- fileParams.width = dimensions.width;
- fileParams.height = dimensions.height;
+ } else if (image || video || pdf) {
+ try {
+ final Dimensions dimensions;
+ if (video) {
+ dimensions = getVideoDimensions(file);
+ } else if (pdf) {
+ dimensions = getPdfDocumentDimensions(file);
+ } else {
+ dimensions = getImageDimensions(file);
+ }
+ if (dimensions.valid()) {
+ fileParams.width = dimensions.width;
+ fileParams.height = dimensions.height;
+ }
+ } catch (NotAVideoFile notAVideoFile) {
+ Log.d(
+ Config.LOGTAG,
+ "file with mime type " + file.getMimeType() + " was not a video file");
+ // fall threw
}
- } catch (NotAVideoFile notAVideoFile) {
- Log.d(
- Config.LOGTAG,
- "file with mime type " + file.getMimeType() + " was not a video file");
- // fall threw
+ } else if (audio) {
+ fileParams.runtime = getMediaRuntime(file);
}
- } else if (audio) {
- fileParams.runtime = getMediaRuntime(file);
}
message.setFileParams(fileParams);
message.setDeleted(false);
@@ -1579,14 +1602,18 @@ public class FileBackend {
: (image ? Message.TYPE_IMAGE : Message.TYPE_FILE));
}
- private int getMediaRuntime(File file) {
+ private int getMediaRuntime(final File file) {
try {
- MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
+ final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
mediaMetadataRetriever.setDataSource(file.toString());
- return Integer.parseInt(
+ final String value =
mediaMetadataRetriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_DURATION));
- } catch (RuntimeException e) {
+ MediaMetadataRetriever.METADATA_KEY_DURATION);
+ if (Strings.isNullOrEmpty(value)) {
+ return 0;
+ }
+ return Integer.parseInt(value);
+ } catch (final IllegalArgumentException e) {
return 0;
}
}
@@ -9,6 +9,7 @@
*/
package eu.siacs.conversations.services;
+import android.Manifest;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -20,25 +21,25 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.media.AudioManager;
+import android.os.Build;
import android.os.Handler;
import android.os.Looper;
-import android.os.Process;
import android.util.Log;
import androidx.annotation.Nullable;
+import androidx.core.app.ActivityCompat;
+
+import com.google.common.collect.ImmutableList;
import org.webrtc.ThreadUtils;
+import java.util.Collections;
import java.util.List;
-import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.AppRTCUtils;
-/**
- * AppRTCProximitySensor manages functions related to Bluetoth devices in the
- * AppRTC demo.
- */
+/** AppRTCProximitySensor manages functions related to Bluetoth devices in the AppRTC demo. */
public class AppRTCBluetoothManager {
// Timeout interval for starting or stopping audio to a Bluetooth SCO device.
private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000;
@@ -46,28 +47,26 @@ public class AppRTCBluetoothManager {
private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2;
private final Context apprtcContext;
private final AppRTCAudioManager apprtcAudioManager;
- @Nullable
- private final AudioManager audioManager;
+ @Nullable private final AudioManager audioManager;
private final Handler handler;
private final BluetoothProfile.ServiceListener bluetoothServiceListener;
private final BroadcastReceiver bluetoothHeadsetReceiver;
int scoConnectionAttempts;
private State bluetoothState;
- @Nullable
- private BluetoothAdapter bluetoothAdapter;
- @Nullable
- private BluetoothHeadset bluetoothHeadset;
- @Nullable
- private BluetoothDevice bluetoothDevice;
+ @Nullable private BluetoothAdapter bluetoothAdapter;
+ @Nullable private BluetoothHeadset bluetoothHeadset;
+ @Nullable private BluetoothDevice bluetoothDevice;
// Runs when the Bluetooth timeout expires. We use that timeout after calling
// startScoAudio() or stopScoAudio() because we're not guaranteed to get a
// callback after those calls.
- private final Runnable bluetoothTimeoutRunnable = new Runnable() {
- @Override
- public void run() {
- bluetoothTimeout();
- }
- };
+ private final Runnable bluetoothTimeoutRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ bluetoothTimeout();
+ }
+ };
+
protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
Log.d(Config.LOGTAG, "ctor");
ThreadUtils.checkIsOnMainThread();
@@ -80,42 +79,29 @@ public class AppRTCBluetoothManager {
handler = new Handler(Looper.getMainLooper());
}
- /**
- * Construction.
- */
+ /** Construction. */
static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) {
Log.d(Config.LOGTAG, "create" + AppRTCUtils.getThreadInfo());
return new AppRTCBluetoothManager(context, audioManager);
}
- /**
- * Returns the internal state.
- */
+ /** Returns the internal state. */
public State getState() {
ThreadUtils.checkIsOnMainThread();
return bluetoothState;
}
/**
- * Activates components required to detect Bluetooth devices and to enable
- * BT SCO (audio is routed via BT SCO) for the headset profile. The end
- * state will be HEADSET_UNAVAILABLE but a state machine has started which
- * will start a state change sequence where the final outcome depends on
- * if/when the BT headset is enabled.
- * Example of state change sequence when start() is called while BT device
- * is connected and enabled:
- * UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE -->
- * SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO.
- * Note that the AppRTCAudioManager is also involved in driving this state
- * change.
+ * Activates components required to detect Bluetooth devices and to enable BT SCO (audio is
+ * routed via BT SCO) for the headset profile. The end state will be HEADSET_UNAVAILABLE but a
+ * state machine has started which will start a state change sequence where the final outcome
+ * depends on if/when the BT headset is enabled. Example of state change sequence when start()
+ * is called while BT device is connected and enabled: UNINITIALIZED --> HEADSET_UNAVAILABLE -->
+ * HEADSET_AVAILABLE --> SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO.
+ * Note that the AppRTCAudioManager is also involved in driving this state change.
*/
public void start() {
ThreadUtils.checkIsOnMainThread();
- Log.d(Config.LOGTAG, "start");
- if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
- Log.w(Config.LOGTAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
- return;
- }
if (bluetoothState != State.UNINITIALIZED) {
Log.w(Config.LOGTAG, "Invalid BT state");
return;
@@ -130,11 +116,10 @@ public class AppRTCBluetoothManager {
return;
}
// Ensure that the device supports use of BT SCO audio for off call use cases.
- if (!audioManager.isBluetoothScoAvailableOffCall()) {
+ if (this.audioManager == null || !audioManager.isBluetoothScoAvailableOffCall()) {
Log.e(Config.LOGTAG, "Bluetooth SCO audio is not available off call");
return;
}
- logBluetoothAdapterInfo(bluetoothAdapter);
// Establish a connection to the HEADSET profile (includes both Bluetooth Headset and
// Hands-Free) proxy object and install a listener.
if (!getBluetoothProfileProxy(
@@ -149,16 +134,20 @@ public class AppRTCBluetoothManager {
// Register receiver for change in audio connection state of the Headset profile.
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
- Log.d(Config.LOGTAG, "HEADSET profile state: "
- + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
+ if (hasBluetoothConnectPermission()) {
+ Log.d(
+ Config.LOGTAG,
+ "HEADSET profile state: "
+ + stateToString(
+ bluetoothAdapter.getProfileConnectionState(
+ BluetoothProfile.HEADSET)));
+ }
Log.d(Config.LOGTAG, "Bluetooth proxy for headset profile has started");
bluetoothState = State.HEADSET_UNAVAILABLE;
Log.d(Config.LOGTAG, "start done: BT state=" + bluetoothState);
}
- /**
- * Stops and closes all components related to Bluetooth audio.
- */
+ /** Stops and closes all components related to Bluetooth audio. */
public void stop() {
ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "stop: BT state=" + bluetoothState);
@@ -184,23 +173,29 @@ public class AppRTCBluetoothManager {
}
/**
- * Starts Bluetooth SCO connection with remote device.
- * Note that the phone application always has the priority on the usage of the SCO connection
- * for telephony. If this method is called while the phone is in call it will be ignored.
- * Similarly, if a call is received or sent while an application is using the SCO connection,
- * the connection will be lost for the application and NOT returned automatically when the call
- * ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a
- * virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO
- * audio connection is established.
+ * Starts Bluetooth SCO connection with remote device. Note that the phone application always
+ * has the priority on the usage of the SCO connection for telephony. If this method is called
+ * while the phone is in call it will be ignored. Similarly, if a call is received or sent while
+ * an application is using the SCO connection, the connection will be lost for the application
+ * and NOT returned automatically when the call ends. Also note that: up to and including API
+ * version JELLY_BEAN_MR1, this method initiates a virtual voice call to the Bluetooth headset.
+ * After API version JELLY_BEAN_MR2 only a raw SCO audio connection is established.
* TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and
* higher. It might be required to initiates a virtual voice call since many devices do not
* accept SCO audio without a "call".
*/
public boolean startScoAudio() {
ThreadUtils.checkIsOnMainThread();
- Log.d(Config.LOGTAG, "startSco: BT state=" + bluetoothState + ", "
- + "attempts: " + scoConnectionAttempts + ", "
- + "SCO is on: " + isScoOn());
+ Log.d(
+ Config.LOGTAG,
+ "startSco: BT state="
+ + bluetoothState
+ + ", "
+ + "attempts: "
+ + scoConnectionAttempts
+ + ", "
+ + "SCO is on: "
+ + isScoOn());
if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
Log.e(Config.LOGTAG, "BT SCO connection fails - no more attempts");
return false;
@@ -213,24 +208,29 @@ public class AppRTCBluetoothManager {
Log.d(Config.LOGTAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED...");
// The SCO connection establishment can take several seconds, hence we cannot rely on the
// connection to be available when the method returns but instead register to receive the
- // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED.
+ // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be
+ // SCO_AUDIO_STATE_CONNECTED.
bluetoothState = State.SCO_CONNECTING;
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
scoConnectionAttempts++;
startTimer();
- Log.d(Config.LOGTAG, "startScoAudio done: BT state=" + bluetoothState + ", "
- + "SCO is on: " + isScoOn());
+ Log.d(
+ Config.LOGTAG,
+ "startScoAudio done: BT state="
+ + bluetoothState
+ + ", "
+ + "SCO is on: "
+ + isScoOn());
return true;
}
- /**
- * Stops Bluetooth SCO connection with remote device.
- */
+ /** Stops Bluetooth SCO connection with remote device. */
public void stopScoAudio() {
ThreadUtils.checkIsOnMainThread();
- Log.d(Config.LOGTAG, "stopScoAudio: BT state=" + bluetoothState + ", "
- + "SCO is on: " + isScoOn());
+ Log.d(
+ Config.LOGTAG,
+ "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn());
if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
return;
}
@@ -238,17 +238,18 @@ public class AppRTCBluetoothManager {
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
bluetoothState = State.SCO_DISCONNECTING;
- Log.d(Config.LOGTAG, "stopScoAudio done: BT state=" + bluetoothState + ", "
- + "SCO is on: " + isScoOn());
+ Log.d(
+ Config.LOGTAG,
+ "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn());
}
/**
- * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset
- * Service via IPC) to update the list of connected devices for the HEADSET
- * profile. The internal state will change to HEADSET_UNAVAILABLE or to
- * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected
- * device if available.
+ * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset Service via IPC) to
+ * update the list of connected devices for the HEADSET profile. The internal state will change
+ * to HEADSET_UNAVAILABLE or to HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the
+ * connected device if available.
*/
+ @SuppressLint("MissingPermission")
public void updateDevice() {
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
return;
@@ -257,7 +258,12 @@ public class AppRTCBluetoothManager {
// Get connected devices for the headset profile. Returns the set of
// devices which are in state STATE_CONNECTED. The BluetoothDevice class
// is just a thin wrapper for a Bluetooth hardware address.
- List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
+ final List<BluetoothDevice> devices;
+ if (hasBluetoothConnectPermission()) {
+ devices = bluetoothHeadset.getConnectedDevices();
+ } else {
+ devices = ImmutableList.of();
+ }
if (devices.isEmpty()) {
bluetoothDevice = null;
bluetoothState = State.HEADSET_UNAVAILABLE;
@@ -266,17 +272,21 @@ public class AppRTCBluetoothManager {
// Always use first device in list. Android only supports one device.
bluetoothDevice = devices.get(0);
bluetoothState = State.HEADSET_AVAILABLE;
- Log.d(Config.LOGTAG, "Connected bluetooth headset: "
- + "name=" + bluetoothDevice.getName() + ", "
- + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
- + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
+ Log.d(
+ Config.LOGTAG,
+ "Connected bluetooth headset: "
+ + "name="
+ + bluetoothDevice.getName()
+ + ", "
+ + "state="
+ + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
+ + ", SCO audio="
+ + bluetoothHeadset.isAudioConnected(bluetoothDevice));
}
Log.d(Config.LOGTAG, "updateDevice done: BT state=" + bluetoothState);
}
- /**
- * Stubs for test mocks.
- */
+ /** Stubs for test mocks. */
@Nullable
protected AudioManager getAudioManager(Context context) {
return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -295,52 +305,31 @@ public class AppRTCBluetoothManager {
return bluetoothAdapter.getProfileProxy(context, listener, profile);
}
- protected boolean hasPermission(Context context, String permission) {
- return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid())
- == PackageManager.PERMISSION_GRANTED;
- }
-
- /**
- * Logs the state of the local Bluetooth adapter.
- */
- @SuppressLint("HardwareIds")
- protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
- Log.d(Config.LOGTAG, "BluetoothAdapter: "
- + "enabled=" + localAdapter.isEnabled() + ", "
- + "state=" + stateToString(localAdapter.getState()) + ", "
- + "name=" + localAdapter.getName() + ", "
- + "address=" + localAdapter.getAddress());
- // Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter.
- Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices();
- if (!pairedDevices.isEmpty()) {
- Log.d(Config.LOGTAG, "paired devices:");
- for (BluetoothDevice device : pairedDevices) {
- Log.d(Config.LOGTAG, " name=" + device.getName() + ", address=" + device.getAddress());
- }
+ protected boolean hasBluetoothConnectPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ return ActivityCompat.checkSelfPermission(
+ apprtcContext, Manifest.permission.BLUETOOTH_CONNECT)
+ == PackageManager.PERMISSION_GRANTED;
+ } else {
+ return true;
}
}
- /**
- * Ensures that the audio manager updates its list of available audio devices.
- */
+ /** Ensures that the audio manager updates its list of available audio devices. */
private void updateAudioDeviceState() {
ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "updateAudioDeviceState");
apprtcAudioManager.updateAudioDeviceState();
}
- /**
- * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
- */
+ /** Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. */
private void startTimer() {
ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "startTimer");
handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
}
- /**
- * Cancels any outstanding timer tasks.
- */
+ /** Cancels any outstanding timer tasks. */
private void cancelTimer() {
ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "cancelTimer");
@@ -348,23 +337,36 @@ public class AppRTCBluetoothManager {
}
/**
- * Called when start of the BT SCO channel takes too long time. Usually
- * happens when the BT device has been turned on during an ongoing call.
+ * Called when start of the BT SCO channel takes too long time. Usually happens when the BT
+ * device has been turned on during an ongoing call.
*/
+ @SuppressLint("MissingPermission")
private void bluetoothTimeout() {
ThreadUtils.checkIsOnMainThread();
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
return;
}
- Log.d(Config.LOGTAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
- + "attempts: " + scoConnectionAttempts + ", "
- + "SCO is on: " + isScoOn());
+ Log.d(
+ Config.LOGTAG,
+ "bluetoothTimeout: BT state="
+ + bluetoothState
+ + ", "
+ + "attempts: "
+ + scoConnectionAttempts
+ + ", "
+ + "SCO is on: "
+ + isScoOn());
if (bluetoothState != State.SCO_CONNECTING) {
return;
}
// Bluetooth SCO should be connecting; check the latest result.
boolean scoConnected = false;
- List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
+ final List<BluetoothDevice> devices;
+ if (hasBluetoothConnectPermission()) {
+ devices = bluetoothHeadset.getConnectedDevices();
+ } else {
+ devices = Collections.emptyList();
+ }
if (devices.size() > 0) {
bluetoothDevice = devices.get(0);
if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
@@ -387,16 +389,12 @@ public class AppRTCBluetoothManager {
Log.d(Config.LOGTAG, "bluetoothTimeout done: BT state=" + bluetoothState);
}
- /**
- * Checks whether audio uses Bluetooth SCO.
- */
+ /** Checks whether audio uses Bluetooth SCO. */
private boolean isScoOn() {
return audioManager.isBluetoothScoOn();
}
- /**
- * Converts BluetoothAdapter states into local string representations.
- */
+ /** Converts BluetoothAdapter states into local string representations. */
private String stateToString(int state) {
switch (state) {
case BluetoothAdapter.STATE_DISCONNECTED:
@@ -412,11 +410,13 @@ public class AppRTCBluetoothManager {
case BluetoothAdapter.STATE_ON:
return "ON";
case BluetoothAdapter.STATE_TURNING_OFF:
- // Indicates the local Bluetooth adapter is turning off. Local clients should immediately
+ // Indicates the local Bluetooth adapter is turning off. Local clients should
+ // immediately
// attempt graceful disconnection of any remote links.
return "TURNING_OFF";
case BluetoothAdapter.STATE_TURNING_ON:
- // Indicates the local Bluetooth adapter is turning on. However local clients should wait
+ // Indicates the local Bluetooth adapter is turning on. However local clients should
+ // wait
// for STATE_ON before attempting to use the adapter.
return "TURNING_ON";
default:
@@ -457,7 +457,9 @@ public class AppRTCBluetoothManager {
if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
return;
}
- Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
+ Log.d(
+ Config.LOGTAG,
+ "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
// Android only supports one connected Bluetooth Headset at a time.
bluetoothHeadset = (BluetoothHeadset) proxy;
updateAudioDeviceState();
@@ -470,7 +472,9 @@ public class AppRTCBluetoothManager {
if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
return;
}
- Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
+ Log.d(
+ Config.LOGTAG,
+ "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
stopScoAudio();
bluetoothHeadset = null;
bluetoothDevice = null;
@@ -495,12 +499,20 @@ public class AppRTCBluetoothManager {
// headset while audio is active using another audio device.
if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
final int state =
- intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
- Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: "
- + "a=ACTION_CONNECTION_STATE_CHANGED, "
- + "s=" + stateToString(state) + ", "
- + "sb=" + isInitialStickyBroadcast() + ", "
- + "BT state: " + bluetoothState);
+ intent.getIntExtra(
+ BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
+ Log.d(
+ Config.LOGTAG,
+ "BluetoothHeadsetBroadcastReceiver.onReceive: "
+ + "a=ACTION_CONNECTION_STATE_CHANGED, "
+ + "s="
+ + stateToString(state)
+ + ", "
+ + "sb="
+ + isInitialStickyBroadcast()
+ + ", "
+ + "BT state: "
+ + bluetoothState);
if (state == BluetoothHeadset.STATE_CONNECTED) {
scoConnectionAttempts = 0;
updateAudioDeviceState();
@@ -516,13 +528,22 @@ public class AppRTCBluetoothManager {
// Change in the audio (SCO) connection state of the Headset profile.
// Typically received after call to startScoAudio() has finalized.
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
- final int state = intent.getIntExtra(
- BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
- Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: "
- + "a=ACTION_AUDIO_STATE_CHANGED, "
- + "s=" + stateToString(state) + ", "
- + "sb=" + isInitialStickyBroadcast() + ", "
- + "BT state: " + bluetoothState);
+ final int state =
+ intent.getIntExtra(
+ BluetoothHeadset.EXTRA_STATE,
+ BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+ Log.d(
+ Config.LOGTAG,
+ "BluetoothHeadsetBroadcastReceiver.onReceive: "
+ + "a=ACTION_AUDIO_STATE_CHANGED, "
+ + "s="
+ + stateToString(state)
+ + ", "
+ + "sb="
+ + isInitialStickyBroadcast()
+ + ", "
+ + "BT state: "
+ + bluetoothState);
if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
cancelTimer();
if (bluetoothState == State.SCO_CONNECTING) {
@@ -531,14 +552,18 @@ public class AppRTCBluetoothManager {
scoConnectionAttempts = 0;
updateAudioDeviceState();
} else {
- Log.w(Config.LOGTAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
+ Log.w(
+ Config.LOGTAG,
+ "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
}
} else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connecting...");
} else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now disconnected");
if (isInitialStickyBroadcast()) {
- Log.d(Config.LOGTAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
+ Log.d(
+ Config.LOGTAG,
+ "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
return;
}
updateAudioDeviceState();
@@ -547,4 +572,4 @@ public class AppRTCBluetoothManager {
Log.d(Config.LOGTAG, "onReceive done: BT state=" + bluetoothState);
}
}
-}
+}
@@ -1,5 +1,7 @@
package eu.siacs.conversations.services;
+import static eu.siacs.conversations.utils.Compatibility.s;
+
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -15,6 +17,7 @@ import android.util.Log;
import androidx.core.app.NotificationCompat;
+import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import java.io.DataOutputStream;
@@ -114,7 +117,7 @@ public class ExportBackupService extends Service {
}
builder.append(intValue);
} else {
- DatabaseUtils.appendEscapedSQLString(builder, value);
+ appendEscapedSQLString(builder, value);
}
}
builder.append(")");
@@ -127,6 +130,10 @@ public class ExportBackupService extends Service {
writer.append(builder.toString());
}
+ private static void appendEscapedSQLString(final StringBuilder sb, final String sqlString) {
+ DatabaseUtils.appendEscapedSQLString(sb, CharMatcher.is('\u0000').removeFrom(sqlString));
+ }
+
private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) {
final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
@@ -201,7 +208,7 @@ public class ExportBackupService extends Service {
} else if (value.matches("[0-9]+")) {
builder.append(value);
} else {
- DatabaseUtils.appendEscapedSQLString(builder, value);
+ appendEscapedSQLString(builder, value);
}
}
builder.append(")");
@@ -364,9 +371,11 @@ public class ExportBackupService extends Service {
PendingIntent openFolderIntent = null;
- for (Intent intent : getPossibleFileOpenIntents(this, path)) {
+ for (final Intent intent : getPossibleFileOpenIntents(this, path)) {
if (intent.resolveActivityInfo(getPackageManager(), 0) != null) {
- openFolderIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ openFolderIntent = PendingIntent.getActivity(this, 189, intent, s()
+ ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
break;
}
}
@@ -382,7 +391,9 @@ public class ExportBackupService extends Service {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setType(MIME_TYPE);
final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files));
- shareFilesIntent = PendingIntent.getActivity(this, 190, chooser, PendingIntent.FLAG_UPDATE_CURRENT);
+ shareFilesIntent = PendingIntent.getActivity(this, 190, chooser, s()
+ ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
}
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
@@ -1784,7 +1784,9 @@ public class NotificationService {
mXmppConnectionService,
0,
new Intent(mXmppConnectionService, ConversationsActivity.class),
- 0);
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
} catch (RuntimeException e) {
return null;
}
@@ -1833,13 +1835,25 @@ public class NotificationService {
R.drawable.ic_play_circle_filled_white_48dp,
mXmppConnectionService.getString(R.string.start_orbot),
PendingIntent.getActivity(
- mXmppConnectionService, 147, TorServiceUtils.LAUNCH_INTENT, 0));
+ mXmppConnectionService,
+ 147,
+ TorServiceUtils.LAUNCH_INTENT,
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE
+ | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT));
} else {
mBuilder.addAction(
R.drawable.ic_file_download_white_24dp,
mXmppConnectionService.getString(R.string.install_orbot),
PendingIntent.getActivity(
- mXmppConnectionService, 146, TorServiceUtils.INSTALL_INTENT, 0));
+ mXmppConnectionService,
+ 146,
+ TorServiceUtils.INSTALL_INTENT,
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE
+ | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT));
}
}
mBuilder.setDeleteIntent(createDismissErrorIntent());
@@ -1,5 +1,7 @@
package eu.siacs.conversations.services;
+import static eu.siacs.conversations.utils.Compatibility.s;
+
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
@@ -1423,7 +1425,9 @@ public class XmppConnectionService extends Service {
final Intent intent = new Intent(this, EventReceiver.class);
intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE);
try {
- final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0);
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, s()
+ ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
} else {
@@ -1435,7 +1439,7 @@ public class XmppConnectionService extends Service {
}
public void scheduleWakeUpCall(int seconds, int requestCode) {
- final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
+ final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000L;
final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null) {
return;
@@ -1469,7 +1473,9 @@ public class XmppConnectionService extends Service {
final Intent intent = new Intent(this, EventReceiver.class);
intent.setAction(ACTION_IDLE_PING);
try {
- PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, s()
+ ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
} catch (RuntimeException e) {
Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e);
@@ -1482,7 +1488,7 @@ public class XmppConnectionService extends Service {
connection.setOnStatusChangedListener(this.statusListener);
connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
- connection.setOnJinglePacketReceivedListener(((a, jp) -> mJingleConnectionManager.deliverPacket(a, jp)));
+ connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket));
connection.setOnBindListener(this.mOnBindListener);
connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
@@ -1,5 +1,12 @@
package eu.siacs.conversations.ui;
+import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
+import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
+import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
+import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
+import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
+
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -55,6 +62,9 @@ import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.databinding.DataBindingUtil;
import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+
+import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
@@ -114,6 +124,7 @@ import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.NickValidityChecker;
import eu.siacs.conversations.utils.Patterns;
+import eu.siacs.conversations.utils.PermissionUtils;
import eu.siacs.conversations.utils.QuickLoader;
import eu.siacs.conversations.utils.StylingHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;
@@ -129,18 +140,10 @@ import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
-import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
-import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
-import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
-import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
-import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
-import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
-
-import org.jetbrains.annotations.NotNull;
-
-
-public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked {
-
+public class ConversationFragment extends XmppFragment
+ implements EditMessage.KeyboardListener,
+ MessageAdapter.OnContactPictureLongClicked,
+ MessageAdapter.OnContactPictureClicked {
public static final int REQUEST_SEND_MESSAGE = 0x0201;
public static final int REQUEST_DECRYPT_PGP = 0x0202;
@@ -161,10 +164,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307;
public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action";
- public static final String STATE_CONVERSATION_UUID = ConversationFragment.class.getName() + ".uuid";
- public static final String STATE_SCROLL_POSITION = ConversationFragment.class.getName() + ".scroll_position";
- public static final String STATE_PHOTO_URI = ConversationFragment.class.getName() + ".media_previews";
- public static final String STATE_MEDIA_PREVIEWS = ConversationFragment.class.getName() + ".take_photo_uri";
+ public static final String STATE_CONVERSATION_UUID =
+ ConversationFragment.class.getName() + ".uuid";
+ public static final String STATE_SCROLL_POSITION =
+ ConversationFragment.class.getName() + ".scroll_position";
+ public static final String STATE_PHOTO_URI =
+ ConversationFragment.class.getName() + ".media_previews";
+ public static final String STATE_MEDIA_PREVIEWS =
+ ConversationFragment.class.getName() + ".take_photo_uri";
private static final String STATE_LAST_MESSAGE_UUID = "state_last_message_uuid";
private final List<Message> messageList = new ArrayList<>();
@@ -185,282 +192,376 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private Toast messageLoaderToast;
private ConversationsActivity activity;
private boolean reInitRequiredOnStart = true;
- private final OnClickListener clickToMuc = new OnClickListener() {
+ private final OnClickListener clickToMuc =
+ new OnClickListener() {
- @Override
- public void onClick(View v) {
- ConferenceDetailsActivity.open(getActivity(), conversation);
- }
- };
- private final OnClickListener leaveMuc = new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- activity.xmppConnectionService.archiveConversation(conversation);
- }
- };
- private final OnClickListener joinMuc = new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- activity.xmppConnectionService.joinMuc(conversation);
- }
- };
-
- private final OnClickListener acceptJoin = new OnClickListener() {
- @Override
- public void onClick(View v) {
- conversation.setAttribute("accept_non_anonymous", true);
- activity.xmppConnectionService.updateConversation(conversation);
- activity.xmppConnectionService.joinMuc(conversation);
- }
- };
+ @Override
+ public void onClick(View v) {
+ ConferenceDetailsActivity.open(getActivity(), conversation);
+ }
+ };
+ private final OnClickListener leaveMuc =
+ new OnClickListener() {
- private final OnClickListener enterPassword = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ activity.xmppConnectionService.archiveConversation(conversation);
+ }
+ };
+ private final OnClickListener joinMuc =
+ new OnClickListener() {
- @Override
- public void onClick(View v) {
- MucOptions muc = conversation.getMucOptions();
- String password = muc.getPassword();
- if (password == null) {
- password = "";
- }
- activity.quickPasswordEdit(password, value -> {
- activity.xmppConnectionService.providePasswordForMuc(conversation, value);
- return null;
- });
- }
- };
- private final OnScrollListener mOnScrollListener = new OnScrollListener() {
+ @Override
+ public void onClick(View v) {
+ activity.xmppConnectionService.joinMuc(conversation);
+ }
+ };
+
+ private final OnClickListener acceptJoin =
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ conversation.setAttribute("accept_non_anonymous", true);
+ activity.xmppConnectionService.updateConversation(conversation);
+ activity.xmppConnectionService.joinMuc(conversation);
+ }
+ };
- @Override
- public void onScrollStateChanged(AbsListView view, int scrollState) {
- if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) {
- fireReadEvent();
- }
- }
+ private final OnClickListener enterPassword =
+ new OnClickListener() {
- @Override
- public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
- toggleScrollDownButton(view);
- synchronized (ConversationFragment.this.messageList) {
- if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true, false) && messageList.size() > 0) {
- long timestamp;
- if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) {
- timestamp = messageList.get(1).getTimeSent();
- } else {
- timestamp = messageList.get(0).getTimeSent();
+ @Override
+ public void onClick(View v) {
+ MucOptions muc = conversation.getMucOptions();
+ String password = muc.getPassword();
+ if (password == null) {
+ password = "";
}
- activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
- @Override
- public void onMoreMessagesLoaded(final int c, final Conversation conversation) {
- if (ConversationFragment.this.conversation != conversation) {
- conversation.messagesLoaded.set(true);
- return;
- }
- runOnUiThread(() -> {
- synchronized (messageList) {
- final int oldPosition = binding.messagesView.getFirstVisiblePosition();
- Message message = null;
- int childPos;
- for (childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) {
- message = messageList.get(oldPosition + childPos);
- if (message.getType() != Message.TYPE_STATUS) {
- break;
- }
- }
- final String uuid = message != null ? message.getUuid() : null;
- View v = binding.messagesView.getChildAt(childPos);
- final int pxOffset = (v == null) ? 0 : v.getTop();
- ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
- try {
- updateStatusMessages();
- } catch (IllegalStateException e) {
- Log.d(Config.LOGTAG, "caught illegal state exception while updating status messages");
- }
- messageListAdapter.notifyDataSetChanged();
- int pos = Math.max(getIndexOf(uuid, messageList), 0);
- binding.messagesView.setSelectionFromTop(pos, pxOffset);
- if (messageLoaderToast != null) {
- messageLoaderToast.cancel();
- }
- conversation.messagesLoaded.set(true);
- }
+ activity.quickPasswordEdit(
+ password,
+ value -> {
+ activity.xmppConnectionService.providePasswordForMuc(
+ conversation, value);
+ return null;
});
- }
-
- @Override
- public void informUser(final int resId) {
+ }
+ };
+ private final OnScrollListener mOnScrollListener =
+ new OnScrollListener() {
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) {
+ fireReadEvent();
+ }
+ }
- runOnUiThread(() -> {
- if (messageLoaderToast != null) {
- messageLoaderToast.cancel();
- }
- if (ConversationFragment.this.conversation != conversation) {
- return;
- }
- messageLoaderToast = Toast.makeText(view.getContext(), resId, Toast.LENGTH_LONG);
- messageLoaderToast.show();
- });
+ @Override
+ public void onScroll(
+ final AbsListView view,
+ int firstVisibleItem,
+ int visibleItemCount,
+ int totalItemCount) {
+ toggleScrollDownButton(view);
+ synchronized (ConversationFragment.this.messageList) {
+ if (firstVisibleItem < 5
+ && conversation != null
+ && conversation.messagesLoaded.compareAndSet(true, false)
+ && messageList.size() > 0) {
+ long timestamp;
+ if (messageList.get(0).getType() == Message.TYPE_STATUS
+ && messageList.size() >= 2) {
+ timestamp = messageList.get(1).getTimeSent();
+ } else {
+ timestamp = messageList.get(0).getTimeSent();
+ }
+ activity.xmppConnectionService.loadMoreMessages(
+ conversation,
+ timestamp,
+ new XmppConnectionService.OnMoreMessagesLoaded() {
+ @Override
+ public void onMoreMessagesLoaded(
+ final int c, final Conversation conversation) {
+ if (ConversationFragment.this.conversation
+ != conversation) {
+ conversation.messagesLoaded.set(true);
+ return;
+ }
+ runOnUiThread(
+ () -> {
+ synchronized (messageList) {
+ final int oldPosition =
+ binding.messagesView
+ .getFirstVisiblePosition();
+ Message message = null;
+ int childPos;
+ for (childPos = 0;
+ childPos + oldPosition
+ < messageList.size();
+ ++childPos) {
+ message =
+ messageList.get(
+ oldPosition
+ + childPos);
+ if (message.getType()
+ != Message.TYPE_STATUS) {
+ break;
+ }
+ }
+ final String uuid =
+ message != null
+ ? message.getUuid()
+ : null;
+ View v =
+ binding.messagesView.getChildAt(
+ childPos);
+ final int pxOffset =
+ (v == null) ? 0 : v.getTop();
+ ConversationFragment.this.conversation
+ .populateWithMessages(
+ ConversationFragment
+ .this
+ .messageList);
+ try {
+ updateStatusMessages();
+ } catch (IllegalStateException e) {
+ Log.d(
+ Config.LOGTAG,
+ "caught illegal state exception while updating status messages");
+ }
+ messageListAdapter
+ .notifyDataSetChanged();
+ int pos =
+ Math.max(
+ getIndexOf(
+ uuid,
+ messageList),
+ 0);
+ binding.messagesView
+ .setSelectionFromTop(
+ pos, pxOffset);
+ if (messageLoaderToast != null) {
+ messageLoaderToast.cancel();
+ }
+ conversation.messagesLoaded.set(true);
+ }
+ });
+ }
+ @Override
+ public void informUser(final int resId) {
+
+ runOnUiThread(
+ () -> {
+ if (messageLoaderToast != null) {
+ messageLoaderToast.cancel();
+ }
+ if (ConversationFragment.this.conversation
+ != conversation) {
+ return;
+ }
+ messageLoaderToast =
+ Toast.makeText(
+ view.getContext(),
+ resId,
+ Toast.LENGTH_LONG);
+ messageLoaderToast.show();
+ });
+ }
+ });
}
- });
-
+ }
}
- }
- }
- };
- private final EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
- @Override
- public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
- // try to get permission to read the image, if applicable
- if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
- try {
- inputContentInfo.requestPermission();
- } catch (Exception e) {
- Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
- Toast.makeText(getActivity(), activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()), Toast.LENGTH_LONG
- ).show();
- return false;
+ };
+ private final EditMessage.OnCommitContentListener mEditorContentListener =
+ new EditMessage.OnCommitContentListener() {
+ @Override
+ public boolean onCommitContent(
+ InputContentInfoCompat inputContentInfo,
+ int flags,
+ Bundle opts,
+ String[] contentMimeTypes) {
+ // try to get permission to read the image, if applicable
+ if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION)
+ != 0) {
+ try {
+ inputContentInfo.requestPermission();
+ } catch (Exception e) {
+ Log.e(
+ Config.LOGTAG,
+ "InputContentInfoCompat#requestPermission() failed.",
+ e);
+ Toast.makeText(
+ getActivity(),
+ activity.getString(
+ R.string.no_permission_to_access_x,
+ inputContentInfo.getDescription()),
+ Toast.LENGTH_LONG)
+ .show();
+ return false;
+ }
+ }
+ if (hasPermissions(
+ REQUEST_ADD_EDITOR_CONTENT,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ attachEditorContentToConversation(inputContentInfo.getContentUri());
+ } else {
+ mPendingEditorContent = inputContentInfo.getContentUri();
+ }
+ return true;
}
- }
- if (hasPermissions(REQUEST_ADD_EDITOR_CONTENT, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
- attachEditorContentToConversation(inputContentInfo.getContentUri());
- } else {
- mPendingEditorContent = inputContentInfo.getContentUri();
- }
- return true;
- }
- };
+ };
private Message selectedMessage;
- private final OnClickListener mEnableAccountListener = new OnClickListener() {
- @Override
- public void onClick(View v) {
- final Account account = conversation == null ? null : conversation.getAccount();
- if (account != null) {
- account.setOption(Account.OPTION_DISABLED, false);
- activity.xmppConnectionService.updateAccount(account);
- }
- }
- };
- private final OnClickListener mUnblockClickListener = new OnClickListener() {
- @Override
- public void onClick(final View v) {
- v.post(() -> v.setVisibility(View.INVISIBLE));
- if (conversation.isDomainBlocked()) {
- BlockContactDialog.show(activity, conversation);
- } else {
- unblockConversation(conversation);
- }
- }
- };
+ private final OnClickListener mEnableAccountListener =
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Account account = conversation == null ? null : conversation.getAccount();
+ if (account != null) {
+ account.setOption(Account.OPTION_DISABLED, false);
+ activity.xmppConnectionService.updateAccount(account);
+ }
+ }
+ };
+ private final OnClickListener mUnblockClickListener =
+ new OnClickListener() {
+ @Override
+ public void onClick(final View v) {
+ v.post(() -> v.setVisibility(View.INVISIBLE));
+ if (conversation.isDomainBlocked()) {
+ BlockContactDialog.show(activity, conversation);
+ } else {
+ unblockConversation(conversation);
+ }
+ }
+ };
private final OnClickListener mBlockClickListener = this::showBlockSubmenu;
- private final OnClickListener mAddBackClickListener = new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- final Contact contact = conversation == null ? null : conversation.getContact();
- if (contact != null) {
- activity.xmppConnectionService.createContact(contact, true);
- activity.switchToContactDetails(contact);
- }
- }
- };
+ private final OnClickListener mAddBackClickListener =
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ final Contact contact = conversation == null ? null : conversation.getContact();
+ if (contact != null) {
+ activity.xmppConnectionService.createContact(contact, true);
+ activity.switchToContactDetails(contact);
+ }
+ }
+ };
private final View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu;
- private final OnClickListener mAllowPresenceSubscription = new OnClickListener() {
- @Override
- public void onClick(View v) {
- final Contact contact = conversation == null ? null : conversation.getContact();
- if (contact != null) {
- activity.xmppConnectionService.sendPresencePacket(contact.getAccount(),
- activity.xmppConnectionService.getPresenceGenerator()
- .sendPresenceUpdatesTo(contact));
- hideSnackbar();
- }
- }
- };
- protected OnClickListener clickToDecryptListener = new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent();
- if (pendingIntent != null) {
- try {
- getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(),
- REQUEST_DECRYPT_PGP,
- null,
- 0,
- 0,
- 0);
- } catch (SendIntentException e) {
- Toast.makeText(getActivity(), R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show();
- conversation.getAccount().getPgpDecryptionService().continueDecryption(true);
+ private final OnClickListener mAllowPresenceSubscription =
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Contact contact = conversation == null ? null : conversation.getContact();
+ if (contact != null) {
+ activity.xmppConnectionService.sendPresencePacket(
+ contact.getAccount(),
+ activity.xmppConnectionService
+ .getPresenceGenerator()
+ .sendPresenceUpdatesTo(contact));
+ hideSnackbar();
+ }
}
- }
- updateSnackBar(conversation);
- }
- };
+ };
+ protected OnClickListener clickToDecryptListener =
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ PendingIntent pendingIntent =
+ conversation.getAccount().getPgpDecryptionService().getPendingIntent();
+ if (pendingIntent != null) {
+ try {
+ getActivity()
+ .startIntentSenderForResult(
+ pendingIntent.getIntentSender(),
+ REQUEST_DECRYPT_PGP,
+ null,
+ 0,
+ 0,
+ 0);
+ } catch (SendIntentException e) {
+ Toast.makeText(
+ getActivity(),
+ R.string.unable_to_connect_to_keychain,
+ Toast.LENGTH_SHORT)
+ .show();
+ conversation
+ .getAccount()
+ .getPgpDecryptionService()
+ .continueDecryption(true);
+ }
+ }
+ updateSnackBar(conversation);
+ }
+ };
private final AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
- private final OnEditorActionListener mEditorActionListener = (v, actionId, event) -> {
- if (actionId == EditorInfo.IME_ACTION_SEND) {
- InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
- if (imm != null && imm.isFullscreenMode()) {
- imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
- }
- sendMessage();
- return true;
- } else {
- return false;
- }
- };
- private final OnClickListener mScrollButtonListener = new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- stopScrolling();
- setSelection(binding.messagesView.getCount() - 1, true);
- }
- };
- private final OnClickListener mSendButtonListener = new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- Object tag = v.getTag();
- if (tag instanceof SendButtonAction) {
- SendButtonAction action = (SendButtonAction) tag;
- switch (action) {
- case TAKE_PHOTO:
- case RECORD_VIDEO:
- case SEND_LOCATION:
- case RECORD_VOICE:
- case CHOOSE_PICTURE:
- attachFile(action.toChoice());
- break;
- case CANCEL:
- if (conversation != null) {
- if (conversation.setCorrectingMessage(null)) {
- binding.textinput.setText("");
- binding.textinput.append(conversation.getDraftMessage());
- conversation.setDraftMessage(null);
- } else if (conversation.getMode() == Conversation.MODE_MULTI) {
- conversation.setNextCounterpart(null);
- binding.textinput.setText("");
- } else {
- binding.textinput.setText("");
- }
- updateChatMsgHint();
- updateSendButton();
- updateEditablity();
+ private final OnEditorActionListener mEditorActionListener =
+ (v, actionId, event) -> {
+ if (actionId == EditorInfo.IME_ACTION_SEND) {
+ InputMethodManager imm =
+ (InputMethodManager)
+ activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null && imm.isFullscreenMode()) {
+ imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ }
+ sendMessage();
+ return true;
+ } else {
+ return false;
+ }
+ };
+ private final OnClickListener mScrollButtonListener =
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ stopScrolling();
+ setSelection(binding.messagesView.getCount() - 1, true);
+ }
+ };
+ private final OnClickListener mSendButtonListener =
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Object tag = v.getTag();
+ if (tag instanceof SendButtonAction) {
+ SendButtonAction action = (SendButtonAction) tag;
+ switch (action) {
+ case TAKE_PHOTO:
+ case RECORD_VIDEO:
+ case SEND_LOCATION:
+ case RECORD_VOICE:
+ case CHOOSE_PICTURE:
+ attachFile(action.toChoice());
+ break;
+ case CANCEL:
+ if (conversation != null) {
+ if (conversation.setCorrectingMessage(null)) {
+ binding.textinput.setText("");
+ binding.textinput.append(conversation.getDraftMessage());
+ conversation.setDraftMessage(null);
+ } else if (conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.setNextCounterpart(null);
+ binding.textinput.setText("");
+ } else {
+ binding.textinput.setText("");
+ }
+ updateChatMsgHint();
+ updateSendButton();
+ updateEditablity();
+ }
+ break;
+ default:
+ sendMessage();
}
- break;
- default:
+ } else {
sendMessage();
+ }
}
- } else {
- sendMessage();
- }
- }
- };
+ };
private int completionIndex = 0;
private int lastCompletionLength = 0;
private String incomplete;
@@ -531,7 +632,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return (ConversationFragment) fragment;
} else {
fragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
- return fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null;
+ return fragment instanceof ConversationFragment
+ ? (ConversationFragment) fragment
+ : null;
}
}
@@ -593,7 +696,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
next = next.next();
}
-
}
}
return -1;
@@ -601,7 +703,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private ScrollState getScrollPosition() {
final ListView listView = this.binding == null ? null : this.binding.messagesView;
- if (listView == null || listView.getCount() == 0 || listView.getLastVisiblePosition() == listView.getCount() - 1) {
+ if (listView == null
+ || listView.getCount() == 0
+ || listView.getLastVisiblePosition() == listView.getCount() - 1) {
return null;
} else {
final int pos = listView.getFirstVisiblePosition();
@@ -619,10 +723,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
this.lastMessageUuid = lastMessageUuid;
if (lastMessageUuid != null) {
- binding.unreadCountCustomView.setUnreadCount(conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid));
+ binding.unreadCountCustomView.setUnreadCount(
+ conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid));
}
- //TODO maybe this needs a 'post'
- this.binding.messagesView.setSelectionFromTop(scrollPosition.position, scrollPosition.offset);
+ // TODO maybe this needs a 'post'
+ this.binding.messagesView.setSelectionFromTop(
+ scrollPosition.position, scrollPosition.offset);
toggleScrollDownButton();
}
}
@@ -631,61 +737,65 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
if (conversation == null) {
return;
}
- activity.xmppConnectionService.attachLocationToConversation(conversation, uri, new UiCallback<Message>() {
-
- @Override
- public void success(Message message) {
-
- }
+ activity.xmppConnectionService.attachLocationToConversation(
+ conversation,
+ uri,
+ new UiCallback<Message>() {
- @Override
- public void error(int errorCode, Message object) {
- //TODO show possible pgp error
- }
+ @Override
+ public void success(Message message) {}
- @Override
- public void userInputRequired(PendingIntent pi, Message object) {
+ @Override
+ public void error(int errorCode, Message object) {
+ // TODO show possible pgp error
+ }
- }
- });
+ @Override
+ public void userInputRequired(PendingIntent pi, Message object) {}
+ });
}
private void attachFileToConversation(Conversation conversation, Uri uri, String type) {
if (conversation == null) {
return;
}
- final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG);
+ final Toast prepareFileToast =
+ Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG);
prepareFileToast.show();
activity.delegateUriPermissionsToService(uri);
- activity.xmppConnectionService.attachFileToConversation(conversation, uri, type, new UiInformableCallback<Message>() {
- @Override
- public void inform(final String text) {
- hidePrepareFileToast(prepareFileToast);
- runOnUiThread(() -> activity.replaceToast(text));
- }
-
- @Override
- public void success(Message message) {
- runOnUiThread(() -> activity.hideToast());
- hidePrepareFileToast(prepareFileToast);
- }
+ activity.xmppConnectionService.attachFileToConversation(
+ conversation,
+ uri,
+ type,
+ new UiInformableCallback<Message>() {
+ @Override
+ public void inform(final String text) {
+ hidePrepareFileToast(prepareFileToast);
+ runOnUiThread(() -> activity.replaceToast(text));
+ }
- @Override
- public void error(final int errorCode, Message message) {
- hidePrepareFileToast(prepareFileToast);
- runOnUiThread(() -> activity.replaceToast(getString(errorCode)));
+ @Override
+ public void success(Message message) {
+ runOnUiThread(() -> activity.hideToast());
+ hidePrepareFileToast(prepareFileToast);
+ }
- }
+ @Override
+ public void error(final int errorCode, Message message) {
+ hidePrepareFileToast(prepareFileToast);
+ runOnUiThread(() -> activity.replaceToast(getString(errorCode)));
+ }
- @Override
- public void userInputRequired(PendingIntent pi, Message message) {
- hidePrepareFileToast(prepareFileToast);
- }
- });
+ @Override
+ public void userInputRequired(PendingIntent pi, Message message) {
+ hidePrepareFileToast(prepareFileToast);
+ }
+ });
}
public void attachEditorContentToConversation(Uri uri) {
- mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uri, Attachment.Type.FILE));
+ mediaPreviewAdapter.addMediaPreviews(
+ Attachment.of(getActivity(), uri, Attachment.Type.FILE));
toggleInputMethod();
}
@@ -693,10 +803,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
if (conversation == null) {
return;
}
- final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
+ final Toast prepareFileToast =
+ Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
prepareFileToast.show();
activity.delegateUriPermissionsToService(uri);
- activity.xmppConnectionService.attachImageToConversation(conversation, uri, type,
+ activity.xmppConnectionService.attachImageToConversation(
+ conversation,
+ uri,
+ type,
new UiCallback<Message>() {
@Override
@@ -762,19 +876,31 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) {
- return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(requestCode);
+ return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL
+ && trustKeysIfNeeded(requestCode);
}
protected boolean trustKeysIfNeeded(int requestCode) {
AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
final List<Jid> targets = axolotlService.getCryptoTargets(conversation);
boolean hasUnaccepted = !conversation.getAcceptedCryptoTargets().containsAll(targets);
- boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
- boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
+ boolean hasUndecidedOwn =
+ !axolotlService
+ .getKeysWithTrust(FingerprintStatus.createActiveUndecided())
+ .isEmpty();
+ boolean hasUndecidedContacts =
+ !axolotlService
+ .getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets)
+ .isEmpty();
boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(conversation).isEmpty();
boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
boolean downloadInProgress = axolotlService.hasPendingKeyFetches(targets);
- if (hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted || downloadInProgress) {
+ if (hasUndecidedOwn
+ || hasUndecidedContacts
+ || hasPendingKeys
+ || hasNoTrustedKeys
+ || hasUnaccepted
+ || downloadInProgress) {
axolotlService.createSessionsIfNeeded(conversation);
Intent intent = new Intent(getActivity(), TrustKeysActivity.class);
String[] contacts = new String[targets.size()];
@@ -782,7 +908,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
contacts[i] = targets.get(i).toString();
}
intent.putExtra("contacts", contacts);
- intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toEscapedString());
+ intent.putExtra(
+ EXTRA_ACCOUNT,
+ conversation.getAccount().getJid().asBareJid().toEscapedString());
intent.putExtra("conversation", conversation.getUuid());
startActivityForResult(intent, requestCode);
return true;
@@ -799,9 +927,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} else if (multi && conversation.getNextCounterpart() != null) {
this.binding.textinput.setHint(R.string.send_message);
this.binding.textInputHint.setVisibility(View.VISIBLE);
- this.binding.textInputHint.setText(getString(
- R.string.send_private_message_to,
- conversation.getNextCounterpart().getResource()));
+ this.binding.textInputHint.setText(
+ getString(
+ R.string.send_private_message_to,
+ conversation.getNextCounterpart().getResource()));
} else if (multi && !conversation.getMucOptions().participating()) {
this.binding.textInputHint.setVisibility(View.GONE);
this.binding.textinput.setHint(R.string.you_are_not_participating);
@@ -839,14 +968,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
break;
case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
- final List<Attachment> imageUris = Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE);
+ final List<Attachment> imageUris =
+ Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE);
mediaPreviewAdapter.addMediaPreviews(imageUris);
toggleInputMethod();
break;
case ATTACHMENT_CHOICE_TAKE_PHOTO:
final Uri takePhotoUri = pendingTakePhotoUri.pop();
if (takePhotoUri != null) {
- mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE));
+ mediaPreviewAdapter.addMediaPreviews(
+ Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE));
toggleInputMethod();
} else {
Log.d(Config.LOGTAG, "lost take photo uri. unable to to attach");
@@ -855,8 +986,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
case ATTACHMENT_CHOICE_CHOOSE_FILE:
case ATTACHMENT_CHOICE_RECORD_VIDEO:
case ATTACHMENT_CHOICE_RECORD_VOICE:
- final Attachment.Type type = requestCode == ATTACHMENT_CHOICE_RECORD_VOICE ? Attachment.Type.RECORDING : Attachment.Type.FILE;
- final List<Attachment> fileUris = Attachment.extractAttachments(getActivity(), data, type);
+ final Attachment.Type type =
+ requestCode == ATTACHMENT_CHOICE_RECORD_VOICE
+ ? Attachment.Type.RECORDING
+ : Attachment.Type.FILE;
+ final List<Attachment> fileUris =
+ Attachment.extractAttachments(getActivity(), data, type);
mediaPreviewAdapter.addMediaPreviews(fileUris);
toggleInputMethod();
break;
@@ -870,14 +1005,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} else {
geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude));
}
- mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION));
+ mediaPreviewAdapter.addMediaPreviews(
+ Attachment.of(getActivity(), geo, Attachment.Type.LOCATION));
toggleInputMethod();
break;
case REQUEST_INVITE_TO_CONVERSATION:
XmppActivity.ConferenceInvite invite = XmppActivity.ConferenceInvite.parse(data);
if (invite != null) {
if (invite.execute(activity)) {
- activity.mToast = Toast.makeText(activity, R.string.creating_conference, Toast.LENGTH_LONG);
+ activity.mToast =
+ Toast.makeText(
+ activity, R.string.creating_conference, Toast.LENGTH_LONG);
activity.mToast.show();
}
}
@@ -887,40 +1025,51 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private void commitAttachments() {
final List<Attachment> attachments = mediaPreviewAdapter.getAttachments();
- if (anyNeedsExternalStoragePermission(attachments) && !hasPermissions(REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ if (anyNeedsExternalStoragePermission(attachments)
+ && !hasPermissions(
+ REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return;
}
if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_ATTACHMENTS)) {
return;
}
- final PresenceSelector.OnPresenceSelected callback = () -> {
- for (Iterator<Attachment> i = attachments.iterator(); i.hasNext(); i.remove()) {
- final Attachment attachment = i.next();
- if (attachment.getType() == Attachment.Type.LOCATION) {
- attachLocationToConversation(conversation, attachment.getUri());
- } else if (attachment.getType() == Attachment.Type.IMAGE) {
- Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
- attachImageToConversation(conversation, attachment.getUri(), attachment.getMime());
- } else {
- Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
- attachFileToConversation(conversation, attachment.getUri(), attachment.getMime());
- }
- }
- mediaPreviewAdapter.notifyDataSetChanged();
- toggleInputMethod();
- };
+ final PresenceSelector.OnPresenceSelected callback =
+ () -> {
+ for (Iterator<Attachment> i = attachments.iterator(); i.hasNext(); i.remove()) {
+ final Attachment attachment = i.next();
+ if (attachment.getType() == Attachment.Type.LOCATION) {
+ attachLocationToConversation(conversation, attachment.getUri());
+ } else if (attachment.getType() == Attachment.Type.IMAGE) {
+ Log.d(
+ Config.LOGTAG,
+ "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
+ attachImageToConversation(
+ conversation, attachment.getUri(), attachment.getMime());
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
+ attachFileToConversation(
+ conversation, attachment.getUri(), attachment.getMime());
+ }
+ }
+ mediaPreviewAdapter.notifyDataSetChanged();
+ toggleInputMethod();
+ };
if (conversation == null
|| conversation.getMode() == Conversation.MODE_MULTI
|| Attachment.canBeSendInband(attachments)
- || (conversation.getAccount().httpUploadAvailable() && FileBackend.allFilesUnderSize(getActivity(), attachments, getMaxHttpUploadSize(conversation)))) {
+ || (conversation.getAccount().httpUploadAvailable()
+ && FileBackend.allFilesUnderSize(
+ getActivity(), attachments, getMaxHttpUploadSize(conversation)))) {
callback.onPresenceSelected();
} else {
activity.selectPresence(conversation, callback);
}
}
-
- private static boolean anyNeedsExternalStoragePermission(final Collection<Attachment> attachments) {
+ private static boolean anyNeedsExternalStoragePermission(
+ final Collection<Attachment> attachments) {
for (final Attachment attachment : attachments) {
if (attachment.getType() != Attachment.Type.LOCATION) {
return true;
@@ -304,14 +304,16 @@ public class RtpSessionActivity extends XmppActivity
}
private void requestPermissionsAndAcceptCall() {
- final List<String> permissions;
+ final ImmutableList.Builder<String> permissions = ImmutableList.builder();
if (getMedia().contains(Media.VIDEO)) {
- permissions =
- ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO);
+ permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO);
} else {
- permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO);
+ permissions.add(Manifest.permission.RECORD_AUDIO);
}
- if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
+ }
+ if (PermissionUtils.hasPermission(this, permissions.build(), REQUEST_ACCEPT_CALL)) {
putScreenInCallMode();
checkRecorderAndAcceptCall();
}
@@ -523,13 +525,16 @@ public class RtpSessionActivity extends XmppActivity
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (PermissionUtils.allGranted(grantResults)) {
+ final PermissionUtils.PermissionResult permissionResult =
+ PermissionUtils.removeBluetoothConnect(permissions, grantResults);
+ if (PermissionUtils.allGranted(permissionResult.grantResults)) {
if (requestCode == REQUEST_ACCEPT_CALL) {
checkRecorderAndAcceptCall();
}
} else {
@StringRes int res;
- final String firstDenied = getFirstDenied(grantResults, permissions);
+ final String firstDenied =
+ getFirstDenied(permissionResult.grantResults, permissionResult.permissions);
if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
res = R.string.no_microphone_permission;
} else if (Manifest.permission.CAMERA.equals(firstDenied)) {
@@ -12,6 +12,7 @@ import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.base.Optional;
+import com.google.common.base.Strings;
import java.util.List;
@@ -25,11 +26,13 @@ import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.utils.IrregularUnicodeDetector;
+import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
-public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapter.ConversationViewHolder> {
+public class ConversationAdapter
+ extends RecyclerView.Adapter<ConversationAdapter.ConversationViewHolder> {
private final XmppActivity activity;
private final List<Conversation> conversations;
@@ -40,11 +43,15 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
this.conversations = conversations;
}
-
@NonNull
@Override
public ConversationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- return new ConversationViewHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.conversation_list_row, parent, false));
+ return new ConversationViewHolder(
+ DataBindingUtil.inflate(
+ LayoutInflater.from(parent.getContext()),
+ R.layout.conversation_list_row,
+ parent,
+ false));
}
@Override
@@ -55,15 +62,18 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
}
CharSequence name = conversation.getName();
if (name instanceof Jid) {
- viewHolder.binding.conversationName.setText(IrregularUnicodeDetector.style(activity, (Jid) name));
+ viewHolder.binding.conversationName.setText(
+ IrregularUnicodeDetector.style(activity, (Jid) name));
} else {
viewHolder.binding.conversationName.setText(name);
}
if (conversation == ConversationFragment.getConversation(activity)) {
- viewHolder.binding.frame.setBackgroundColor(StyledAttributes.getColor(activity, R.attr.color_background_tertiary));
+ viewHolder.binding.frame.setBackgroundColor(
+ StyledAttributes.getColor(activity, R.attr.color_background_tertiary));
} else {
- viewHolder.binding.frame.setBackgroundColor(StyledAttributes.getColor(activity, R.attr.color_background_primary));
+ viewHolder.binding.frame.setBackgroundColor(
+ StyledAttributes.getColor(activity, R.attr.color_background_primary));
}
Message message = conversation.getLatestMessage();
@@ -93,31 +103,70 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
} else {
final boolean fileAvailable = !message.isDeleted();
final boolean showPreviewText;
- if (fileAvailable && (message.isFileOrImage() || message.treatAsDownloadable() || message.isGeoUri())) {
+ if (fileAvailable
+ && (message.isFileOrImage()
+ || message.treatAsDownloadable()
+ || message.isGeoUri())) {
final int imageResource;
if (message.isGeoUri()) {
- imageResource = activity.getThemeResource(R.attr.ic_attach_location, R.drawable.ic_attach_location);
+ imageResource =
+ activity.getThemeResource(
+ R.attr.ic_attach_location, R.drawable.ic_attach_location);
showPreviewText = false;
} else {
- //TODO move this into static MediaPreview method and use same icons as in MediaAdapter
+ // TODO move this into static MediaPreview method and use same icons as in
+ // MediaAdapter
final String mime = message.getMimeType();
- switch (mime == null ? "" : mime.split("/")[0]) {
- case "image":
- imageResource = activity.getThemeResource(R.attr.ic_attach_photo, R.drawable.ic_attach_photo);
- showPreviewText = false;
- break;
- case "video":
- imageResource = activity.getThemeResource(R.attr.ic_attach_videocam, R.drawable.ic_attach_videocam);
+ if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) {
+ final Message.FileParams fileParams = message.getFileParams();
+ if (fileParams.width > 0 && fileParams.height > 0) {
+ imageResource =
+ activity.getThemeResource(
+ R.attr.ic_attach_videocam,
+ R.drawable.ic_attach_videocam);
showPreviewText = false;
- break;
- case "audio":
- imageResource = activity.getThemeResource(R.attr.ic_attach_record, R.drawable.ic_attach_record);
+ } else if (fileParams.runtime > 0) {
+ imageResource =
+ activity.getThemeResource(
+ R.attr.ic_attach_record, R.drawable.ic_attach_record);
showPreviewText = false;
- break;
- default:
- imageResource = activity.getThemeResource(R.attr.ic_attach_document, R.drawable.ic_attach_document);
+ } else {
+ imageResource =
+ activity.getThemeResource(
+ R.attr.ic_attach_document,
+ R.drawable.ic_attach_document);
showPreviewText = true;
- break;
+ }
+ } else {
+ switch (Strings.nullToEmpty(mime).split("/")[0]) {
+ case "image":
+ imageResource =
+ activity.getThemeResource(
+ R.attr.ic_attach_photo, R.drawable.ic_attach_photo);
+ showPreviewText = false;
+ break;
+ case "video":
+ imageResource =
+ activity.getThemeResource(
+ R.attr.ic_attach_videocam,
+ R.drawable.ic_attach_videocam);
+ showPreviewText = false;
+ break;
+ case "audio":
+ imageResource =
+ activity.getThemeResource(
+ R.attr.ic_attach_record,
+ R.drawable.ic_attach_record);
+ showPreviewText = false;
+ break;
+ default:
+ imageResource =
+ activity.getThemeResource(
+ R.attr.ic_attach_document,
+ R.drawable.ic_attach_document);
+ showPreviewText = true;
+ break;
+ }
}
}
viewHolder.binding.conversationLastmsgImg.setImageResource(imageResource);
@@ -126,13 +175,18 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
viewHolder.binding.conversationLastmsgImg.setVisibility(View.GONE);
showPreviewText = true;
}
- final Pair<CharSequence, Boolean> preview = UIHelper.getMessagePreview(activity, message, viewHolder.binding.conversationLastmsg.getCurrentTextColor());
+ final Pair<CharSequence, Boolean> preview =
+ UIHelper.getMessagePreview(
+ activity,
+ message,
+ viewHolder.binding.conversationLastmsg.getCurrentTextColor());
if (showPreviewText) {
viewHolder.binding.conversationLastmsg.setText(UIHelper.shorten(preview.first));
} else {
viewHolder.binding.conversationLastmsgImg.setContentDescription(preview.first);
}
- viewHolder.binding.conversationLastmsg.setVisibility(showPreviewText ? View.VISIBLE : View.GONE);
+ viewHolder.binding.conversationLastmsg.setVisibility(
+ showPreviewText ? View.VISIBLE : View.GONE);
if (preview.second) {
if (isRead) {
viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.ITALIC);
@@ -153,7 +207,8 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
if (message.getStatus() == Message.STATUS_RECEIVED) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
viewHolder.binding.senderName.setVisibility(View.VISIBLE);
- viewHolder.binding.senderName.setText(UIHelper.getMessageDisplayName(message).split("\\s+")[0] + ':');
+ viewHolder.binding.senderName.setText(
+ UIHelper.getMessageDisplayName(message).split("\\s+")[0] + ':');
} else {
viewHolder.binding.senderName.setVisibility(View.GONE);
}
@@ -165,33 +220,47 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
}
}
-
final Optional<OngoingRtpSession> ongoingCall;
if (conversation.getMode() == Conversational.MODE_MULTI) {
ongoingCall = Optional.absent();
} else {
- ongoingCall = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact());
+ ongoingCall =
+ activity.xmppConnectionService
+ .getJingleConnectionManager()
+ .getOngoingRtpConnection(conversation.getContact());
}
if (ongoingCall.isPresent()) {
viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE);
- final int ic_ongoing_call = activity.getThemeResource(R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp);
- viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call);
+ final int ic_ongoing_call =
+ activity.getThemeResource(
+ R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp);
+ viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call);
} else {
- final long muted_till = conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
+ final long muted_till =
+ conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
if (muted_till == Long.MAX_VALUE) {
viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE);
- int ic_notifications_off = activity.getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black_24dp);
+ int ic_notifications_off =
+ activity.getThemeResource(
+ R.attr.icon_notifications_off,
+ R.drawable.ic_notifications_off_black_24dp);
viewHolder.binding.notificationStatus.setImageResource(ic_notifications_off);
} else if (muted_till >= System.currentTimeMillis()) {
viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE);
- int ic_notifications_paused = activity.getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black_24dp);
+ int ic_notifications_paused =
+ activity.getThemeResource(
+ R.attr.icon_notifications_paused,
+ R.drawable.ic_notifications_paused_black_24dp);
viewHolder.binding.notificationStatus.setImageResource(ic_notifications_paused);
} else if (conversation.alwaysNotify()) {
viewHolder.binding.notificationStatus.setVisibility(View.GONE);
} else {
viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE);
- int ic_notifications_none = activity.getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black_24dp);
+ int ic_notifications_none =
+ activity.getThemeResource(
+ R.attr.icon_notifications_none,
+ R.drawable.ic_notifications_none_black_24dp);
viewHolder.binding.notificationStatus.setImageResource(ic_notifications_none);
}
}
@@ -202,9 +271,16 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
} else {
timestamp = conversation.getLatestMessage().getTimeSent();
}
- viewHolder.binding.pinnedOnTop.setVisibility(conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP,false) ? View.VISIBLE : View.GONE);
- viewHolder.binding.conversationLastupdate.setText(UIHelper.readableTimeDifference(activity, timestamp));
- AvatarWorkerTask.loadAvatar(conversation, viewHolder.binding.conversationImage, R.dimen.avatar_on_conversation_overview);
+ viewHolder.binding.pinnedOnTop.setVisibility(
+ conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false)
+ ? View.VISIBLE
+ : View.GONE);
+ viewHolder.binding.conversationLastupdate.setText(
+ UIHelper.readableTimeDifference(activity, timestamp));
+ AvatarWorkerTask.loadAvatar(
+ conversation,
+ viewHolder.binding.conversationImage,
+ R.dimen.avatar_on_conversation_overview);
viewHolder.itemView.setOnClickListener(v -> listener.onConversationClick(v, conversation));
}
@@ -217,7 +293,6 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
this.listener = listener;
}
-
public void insert(Conversation c, int position) {
conversations.add(position, c);
notifyDataSetChanged();
@@ -241,5 +316,4 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
binding.getRoot().setLongClickable(true);
}
}
-
}
@@ -38,6 +38,8 @@ import android.os.Parcelable;
import com.google.common.base.MoreObjects;
+import org.jetbrains.annotations.NotNull;
+
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
@@ -89,6 +91,7 @@ public class Attachment implements Parcelable {
return type;
}
+ @NotNull
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
@@ -139,6 +142,9 @@ public class Attachment implements Parcelable {
public static List<Attachment> of(final Context context, List<Uri> uris, final String type) {
final List<Attachment> attachments = new ArrayList<>();
for (final Uri uri : uris) {
+ if (uri == null) {
+ continue;
+ }
final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type);
attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime));
}
@@ -1,5 +1,7 @@
package eu.siacs.conversations.utils;
+import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE;
+
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
@@ -24,23 +26,22 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.SettingsFragment;
-import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE;
-
public class Compatibility {
- private static final List<String> UNUSED_SETTINGS_POST_TWENTYSIX = Arrays.asList(
- "led",
- "notification_ringtone",
- "notification_headsup",
- "vibrate_on_notification"
- );
- private static final List<String> UNUESD_SETTINGS_PRE_TWENTYSIX = Collections.singletonList(
- "message_notification_settings"
- );
-
+ private static final List<String> UNUSED_SETTINGS_POST_TWENTYSIX =
+ Arrays.asList(
+ "led",
+ "notification_ringtone",
+ "notification_headsup",
+ "vibrate_on_notification");
+ private static final List<String> UNUSED_SETTINGS_PRE_TWENTYSIX =
+ Collections.singletonList("message_notification_settings");
public static boolean hasStoragePermission(Context context) {
- return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+ || ContextCompat.checkSelfPermission(
+ context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED;
}
public static boolean s() {
@@ -70,20 +71,22 @@ public class Compatibility {
private static boolean targetsTwentySix(Context context) {
try {
final PackageManager packageManager = context.getPackageManager();
- final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0);
+ final ApplicationInfo applicationInfo =
+ packageManager.getApplicationInfo(context.getPackageName(), 0);
return applicationInfo == null || applicationInfo.targetSdkVersion >= 26;
} catch (PackageManager.NameNotFoundException | RuntimeException e) {
- return true; //when in doubt…
+ return true; // when in doubt…
}
}
private static boolean targetsTwentyFour(Context context) {
try {
final PackageManager packageManager = context.getPackageManager();
- final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0);
+ final ApplicationInfo applicationInfo =
+ packageManager.getApplicationInfo(context.getPackageName(), 0);
return applicationInfo == null || applicationInfo.targetSdkVersion >= 24;
} catch (PackageManager.NameNotFoundException | RuntimeException e) {
- return true; //when in doubt…
+ return true; // when in doubt…
}
}
@@ -96,14 +99,23 @@ public class Compatibility {
}
public static boolean keepForegroundService(Context context) {
- return runsAndTargetsTwentySix(context) || getBooleanPreference(context, SettingsActivity.KEEP_FOREGROUND_SERVICE, R.bool.enable_foreground_service);
+ return runsAndTargetsTwentySix(context)
+ || getBooleanPreference(
+ context,
+ SettingsActivity.KEEP_FOREGROUND_SERVICE,
+ R.bool.enable_foreground_service);
}
public static void removeUnusedPreferences(SettingsFragment settingsFragment) {
- List<PreferenceCategory> categories = Arrays.asList(
- (PreferenceCategory) settingsFragment.findPreference("notification_category"),
- (PreferenceCategory) settingsFragment.findPreference("advanced"));
- for (String key : (runsTwentySix() ? UNUSED_SETTINGS_POST_TWENTYSIX : UNUESD_SETTINGS_PRE_TWENTYSIX)) {
+ List<PreferenceCategory> categories =
+ Arrays.asList(
+ (PreferenceCategory)
+ settingsFragment.findPreference("notification_category"),
+ (PreferenceCategory) settingsFragment.findPreference("advanced"));
+ for (String key :
+ (runsTwentySix()
+ ? UNUSED_SETTINGS_POST_TWENTYSIX
+ : UNUSED_SETTINGS_PRE_TWENTYSIX)) {
Preference preference = settingsFragment.findPreference(key);
if (preference != null) {
for (PreferenceCategory category : categories) {
@@ -115,7 +127,8 @@ public class Compatibility {
}
if (Compatibility.runsTwentySix()) {
if (targetsTwentySix(settingsFragment.getContext())) {
- Preference preference = settingsFragment.findPreference(SettingsActivity.KEEP_FOREGROUND_SERVICE);
+ Preference preference =
+ settingsFragment.findPreference(SettingsActivity.KEEP_FOREGROUND_SERVICE);
if (preference != null) {
for (PreferenceCategory category : categories) {
if (category != null) {
@@ -136,11 +149,12 @@ public class Compatibility {
context.startService(intent);
}
} catch (RuntimeException e) {
- Log.d(Config.LOGTAG, context.getClass().getSimpleName() + " was unable to start service");
+ Log.d(
+ Config.LOGTAG,
+ context.getClass().getSimpleName() + " was unable to start service");
}
}
-
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
public static boolean hasFeatureCamera(final Context context) {
final PackageManager packageManager = context.getPackageManager();
@@ -22,12 +22,14 @@ import android.provider.OpenableColumns;
import android.util.Log;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -40,6 +42,13 @@ import eu.siacs.conversations.services.ExportBackupService;
* Used to implement java.net.URLConnection and android.webkit.MimeTypeMap.
*/
public final class MimeUtils {
+
+ public static final List<String> AMBIGUOUS_CONTAINER_FORMATS = ImmutableList.of(
+ "application/ogg",
+ "video/3gpp", // .3gp files can contain audio, video or both
+ "video/3gpp2"
+ );
+
private static final Map<String, String> mimeTypeToExtensionMap = new HashMap<>();
private static final Map<String, String> extensionToMimeTypeMap = new HashMap<>();
@@ -229,36 +238,8 @@ public final class MimeUtils {
add("video/3gpp", "3gp");
add("video/3gpp2", "3gpp2");
add("video/3gpp2", "3g2");
- add("video/avi", "avi");
- add("video/dl", "dl");
- add("video/dv", "dif");
- add("video/dv", "dv");
- add("video/fli", "fli");
- add("video/m4v", "m4v");
- add("video/mp2ts", "ts");
- add("video/ogg", "ogv");
- add("video/mpeg", "mpeg");
- add("video/mpeg", "mpg");
- add("video/mpeg", "mpe");
- add("video/mp4", "mp4");
- add("video/mpeg", "VOB");
- add("video/quicktime", "qt");
- add("video/quicktime", "mov");
- add("video/vnd.mpegurl", "mxu");
- add("video/webm", "webm");
- add("video/x-la-asf", "lsf");
- add("video/x-la-asf", "lsx");
- add("video/x-matroska", "mkv");
- add("video/x-mng", "mng");
- add("video/x-ms-asf", "asf");
- add("video/x-ms-asf", "asx");
- add("video/x-ms-wm", "wm");
- add("video/x-ms-wmv", "wmv");
- add("video/x-ms-wmx", "wmx");
- add("video/x-ms-wvx", "wvx");
- add("video/x-sgi-movie", "movie");
- add("video/x-webex", "wrf");
add("audio/3gpp", "3gpp");
+ add("audio/3gpp", "3gp");
add("audio/aac", "aac");
add("audio/aac-adts", "aac");
add("audio/amr", "amr");
@@ -398,6 +379,35 @@ public final class MimeUtils {
add("text/x-tex", "cls");
add("text/x-vcalendar", "vcs");
add("text/x-vcard", "vcf");
+ add("video/avi", "avi");
+ add("video/dl", "dl");
+ add("video/dv", "dif");
+ add("video/dv", "dv");
+ add("video/fli", "fli");
+ add("video/m4v", "m4v");
+ add("video/mp2ts", "ts");
+ add("video/ogg", "ogv");
+ add("video/mpeg", "mpeg");
+ add("video/mpeg", "mpg");
+ add("video/mpeg", "mpe");
+ add("video/mp4", "mp4");
+ add("video/mpeg", "VOB");
+ add("video/quicktime", "qt");
+ add("video/quicktime", "mov");
+ add("video/vnd.mpegurl", "mxu");
+ add("video/webm", "webm");
+ add("video/x-la-asf", "lsf");
+ add("video/x-la-asf", "lsx");
+ add("video/x-matroska", "mkv");
+ add("video/x-mng", "mng");
+ add("video/x-ms-asf", "asf");
+ add("video/x-ms-asf", "asx");
+ add("video/x-ms-wm", "wm");
+ add("video/x-ms-wmv", "wmv");
+ add("video/x-ms-wmx", "wmx");
+ add("video/x-ms-wvx", "wvx");
+ add("video/x-sgi-movie", "movie");
+ add("video/x-webex", "wrf");
add("x-conference/x-cooltalk", "ice");
add("x-epoc/x-sisx-app", "sisx");
applyOverrides();
@@ -8,7 +8,9 @@ import android.os.Build;
import androidx.core.app.ActivityCompat;
import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
+import java.util.ArrayList;
import java.util.List;
public class PermissionUtils {
@@ -40,11 +42,41 @@ public class PermissionUtils {
return null;
}
- public static boolean hasPermission(final Activity activity, final List<String> permissions, final int requestCode) {
+ public static class PermissionResult {
+ public final String[] permissions;
+ public final int[] grantResults;
+
+ public PermissionResult(String[] permissions, int[] grantResults) {
+ this.permissions = permissions;
+ this.grantResults = grantResults;
+ }
+ }
+
+ public static PermissionResult removeBluetoothConnect(
+ final String[] inPermissions, final int[] inGrantResults) {
+ final List<String> outPermissions = new ArrayList<>();
+ final List<Integer> outGrantResults = new ArrayList<>();
+ for (int i = 0; i < Math.min(inPermissions.length, inGrantResults.length); ++i) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (inPermissions[i].equals(Manifest.permission.BLUETOOTH_CONNECT)) {
+ continue;
+ }
+ }
+ outPermissions.add(inPermissions[i]);
+ outGrantResults.add(inGrantResults[i]);
+ }
+
+ return new PermissionResult(
+ outPermissions.toArray(new String[0]), Ints.toArray(outGrantResults));
+ }
+
+ public static boolean hasPermission(
+ final Activity activity, final List<String> permissions, final int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final ImmutableList.Builder<String> missingPermissions = new ImmutableList.Builder<>();
for (final String permission : permissions) {
- if (ActivityCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
+ if (ActivityCompat.checkSelfPermission(activity, permission)
+ != PackageManager.PERMISSION_GRANTED) {
missingPermissions.add(permission);
}
}
@@ -52,7 +84,8 @@ public class PermissionUtils {
if (missing.size() == 0) {
return true;
}
- ActivityCompat.requestPermissions(activity, missing.toArray(new String[0]), requestCode);
+ ActivityCompat.requestPermissions(
+ activity, missing.toArray(new String[0]), requestCode);
return false;
} else {
return true;
@@ -477,8 +477,10 @@ public class UIHelper {
public static String getFileDescriptionString(final Context context, final Message message) {
final String mime = message.getMimeType();
- if (mime == null) {
+ if (Strings.isNullOrEmpty(mime)) {
return context.getString(R.string.file);
+ } else if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) {
+ return context.getString(R.string.multimedia_file);
} else if (mime.startsWith("audio/")) {
return context.getString(R.string.audio);
} else if (mime.startsWith("video/")) {
@@ -892,7 +892,15 @@ public class JingleConnectionManager extends AbstractConnectionManager {
for (final AbstractJingleConnection connection : this.connections.values()) {
if (connection.getId().sessionId.equals(sessionId)) {
if (connection instanceof JingleRtpConnection) {
- ((JingleRtpConnection) connection).rejectCall();
+ try {
+ ((JingleRtpConnection) connection).rejectCall();
+ return;
+ } catch (final IllegalStateException e) {
+ Log.w(
+ Config.LOGTAG,
+ "race condition on rejecting call from notification",
+ e);
+ }
}
}
}
@@ -908,12 +916,12 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
}
- public void failProceed(Account account, final Jid with, String sessionId) {
+ public void failProceed(Account account, final Jid with, final String sessionId, final String message) {
final AbstractJingleConnection.Id id =
AbstractJingleConnection.Id.of(account, with, sessionId);
final AbstractJingleConnection existingJingleConnection = connections.get(id);
if (existingJingleConnection instanceof JingleRtpConnection) {
- ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed();
+ ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(message);
}
}
@@ -802,7 +802,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
} catch (final WebRTCWrapper.InitializationException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC");
webRTCWrapper.close();
- sendSessionTerminate(Reason.FAILED_APPLICATION);
+ sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
return;
}
final org.webrtc.SessionDescription sdp =
@@ -933,10 +933,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
- void deliverFailedProceed() {
+ void deliverFailedProceed(final String message) {
Log.d(
Config.LOGTAG,
- id.account.getJid().asBareJid() + ": receive message error for proceed message");
+ id.account.getJid().asBareJid() + ": receive message error for proceed message ("+Strings.nullToEmpty(message)+")");
if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) {
webRTCWrapper.close();
Log.d(
@@ -1277,7 +1277,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
webRTCWrapper.close();
final Reason reason = Reason.ofThrowable(throwable);
if (isInState(targetState)) {
- sendSessionTerminate(reason);
+ sendSessionTerminate(reason, throwable.getMessage());
} else {
sendRetract(reason);
}
@@ -182,6 +182,9 @@ public class RtpContentMap {
final IceUdpTransportInfo.Credentials credentials =
Iterables.getFirst(allCredentials, null);
if (allCredentials.size() == 1 && credentials != null) {
+ if (Strings.isNullOrEmpty(credentials.password) || Strings.isNullOrEmpty(credentials.ufrag)) {
+ throw new IllegalStateException("Credentials are missing password or ufrag");
+ }
return credentials;
}
throw new IllegalStateException("Content map does not have distinct credentials");
@@ -417,6 +417,7 @@
<string name="video">Video</string>
<string name="image">Bild</string>
<string name="vector_graphic">Vektorgrafik</string>
+ <string name="multimedia_file">Multimediadatei</string>
<string name="pdf_document">PDF-Dokument</string>
<string name="apk">Android App</string>
<string name="vcard">Kontakt</string>
@@ -417,6 +417,7 @@
<string name="video">video</string>
<string name="image">imaxe</string>
<string name="vector_graphic">gráfico de vector</string>
+ <string name="multimedia_file">ficheiro multimedia</string>
<string name="pdf_document">documento PDF</string>
<string name="apk">App Android</string>
<string name="vcard">Contacto</string>
@@ -957,4 +957,6 @@
<string name="plain_text_document">プレーンテキスト文書</string>
<string name="account_registrations_are_not_supported">アカウント登録はサポートされていません</string>
<string name="no_xmpp_adddress_found">XMPPアドレスがみつかりません</string>
- </resources>
+ <string name="account_status_temporary_auth_failure">一時的な認証失敗</string>
+
+</resources>
@@ -423,6 +423,7 @@
<string name="video">plik wideo</string>
<string name="image">obraz</string>
<string name="vector_graphic">grafika wektorowa</string>
+ <string name="multimedia_file">plik multimediów</string>
<string name="pdf_document">Dokument PDF</string>
<string name="apk">Aplikacja Androida</string>
<string name="vcard">Kontakt</string>
@@ -1003,4 +1004,6 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż
<string name="plain_text_document">Dokument zwykłego tekstu</string>
<string name="account_registrations_are_not_supported">Rejestracja kont nie jest wspierana</string>
<string name="no_xmpp_adddress_found">Nie znaleziono adresu XMPP</string>
- </resources>
+ <string name="account_status_temporary_auth_failure">Tymczasowy błąd uwierzytelniania</string>
+
+</resources>
@@ -420,6 +420,7 @@
<string name="video">vídeo</string>
<string name="image">imagem</string>
<string name="vector_graphic">gráfico vetorial</string>
+ <string name="multimedia_file">arquivo multimídia</string>
<string name="pdf_document">Documento PDF</string>
<string name="apk">Aplicativo Android</string>
<string name="vcard">Contato</string>
@@ -420,6 +420,7 @@
<string name="video">video</string>
<string name="image">imagine</string>
<string name="vector_graphic">grafic vectorial</string>
+ <string name="multimedia_file">fișier multimedia</string>
<string name="pdf_document">document PDF</string>
<string name="apk">Aplicație Android</string>
<string name="vcard">Contact</string>
@@ -201,6 +201,7 @@
<string name="server_info_blocking">XEP-0191: Blocking Command</string>
<string name="server_info_roster_version">XEP-0237: Roster Versioning</string>
<string name="server_info_stream_management">XEP-0198: Stream Management</string>
+ <string name="server_info_external_service_discovery">XEP-0215: External Service Discovery</string>
<string name="server_info_pep">XEP-0163: PEP (Avatarbilder / OMEMO)</string>
<string name="server_info_http_upload">XEP-0363: Ladda upp via HTTP</string>
<string name="server_info_push">XEP-0357: Push</string>
@@ -464,6 +465,7 @@
<string name="download_failed_file_not_found">Nerladdning gick fel: Filen hittades inte</string>
<string name="download_failed_could_not_connect">Nerladdningen gick fel: Kunder inte ansluta till server</string>
<string name="download_failed_could_not_write_file">Nerladdning gick fel: Kunde inte skriva fil</string>
+ <string name="download_failed_invalid_file">Nedladdning misslyckades: Ogiltig fil</string>
<string name="account_status_tor_unavailable">Tor-nätverk ej tillgängligt</string>
<string name="account_status_bind_failure">Bind-fel</string>
<string name="account_status_host_unknown">Den här servern ansvarar inte för den här domänen</string>
@@ -537,11 +539,15 @@
<string name="security_error_invalid_file_access">Säkerhetsfel: Ogiltig filåtkomst!</string>
<string name="no_application_to_share_uri">Ingen applikation hittades för att dela URI</string>
<string name="share_uri_with">Dela URI med...</string>
+ <string name="welcome_text_quicksy"><![CDATA[Quicksy är en spin-off av den populära XMPP-klienten Conversations med automatisk kontaktupptäckt.<br><br>Du registrerar dig med ditt telefonnummer och Quicksy kommer automatiskt – baserat på telefonnumren i din adressbok – att föreslå möjliga kontakter till dig.<br><br>Genom att registrera dig godkänner du vår <a href=\"https://quicksy.im/#privacy\">integritetspolicy</a>.]]></string>
<string name="agree_and_continue">Acceptera och gå vidare</string>
+ <string name="magic_create_text">En guide har skapats för kontoskapande på conversations.im.¹\nNär du väljer conversations.im som leverantör kommer du att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress.</string>
<string name="your_full_jid_will_be">Din fullständiga XMPP-adress kommer att vara: %s</string>
<string name="create_account">Skapa konto</string>
<string name="use_own_provider">Använd min egen leverantör</string>
<string name="pick_your_username">Välj användarnamn</string>
+ <string name="pref_manually_change_presence">Hantera tillgänglighet manuellt</string>
+ <string name="pref_manually_change_presence_summary">Ställ in din tillgänglighet när du redigerar ditt statusmeddelande.</string>
<string name="status_message">Statusmeddelande</string>
<string name="presence_chat">Tillgänglig</string>
<string name="presence_online">Online</string>
@@ -559,6 +565,8 @@
<string name="gp_short">Kort</string>
<string name="gp_medium">Medium</string>
<string name="gp_long">Lång</string>
+ <string name="pref_broadcast_last_activity">Gör användandet offentligt</string>
+ <string name="pref_broadcast_last_activity_summary">Låter dina kontakter veta när du använder Conversations</string>
<string name="pref_privacy">Privatliv</string>
<string name="pref_theme_options">Tema</string>
<string name="pref_theme_options_summary">Välj färgschema</string>
@@ -591,6 +599,8 @@
<string name="show_error_message">Visa felmeddelande</string>
<string name="error_message">Felmeddelande</string>
<string name="data_saver_enabled">Databesparing</string>
+ <string name="data_saver_enabled_explained">Ditt operativsystem begränsar åtkomsten till Internet i bakgrunden för %1$s. För att få aviseringar om nya meddelanden bör du tillåta obegränsad åtkomst för %1$s, när databesparing är på.\n %1$s kommer fortfarande att anstränga sig för att spara data när det är möjligt.</string>
+ <string name="device_does_not_support_data_saver">Din enhet stöder inte inaktivering av databesparing för %1$s.</string>
<string name="error_unable_to_create_temporary_file">Det gick inte att skapa en tillfällig fil</string>
<string name="this_device_has_been_verified">Denna enhet har verifierats</string>
<string name="copy_fingerprint">Kopiera fingeravtryck</string>
@@ -613,10 +623,13 @@
<string name="pref_clean_private_storage_summary">Rensa privat lagring där filer lagras (De kan om-laddas från servern)</string>
<string name="i_followed_this_link_from_a_trusted_source">Jag följde denna länk från en trovärdig källa</string>
<string name="verifying_omemo_keys_trusted_source">Du håller på att verifiera OMEMO-nyckeln för %1$s efter att du följt en länk. Detta är endast säkert om du följde länken från en trovärdig källa där endast %2$s kan ha publiserat denna länk.</string>
+ <string name="verifying_omemo_keys_trusted_source_account">Du är på väg att verifiera OMEMO-nycklarna för ditt eget konto. Detta är bara säkert om du följde den här länken från en pålitlig källa där bara du kunde ha publicerat den här länken.</string>
+ <string name="continue_btn">Fortsätt</string>
<string name="verify_omemo_keys">Verifiera OMEMO-nycklar</string>
<string name="show_inactive_devices">Visa inaktiva</string>
<string name="hide_inactive_devices">Dölj inaktiva</string>
<string name="distrust_omemo_key">Lita ej på enhet</string>
+ <string name="distrust_omemo_key_text">Är du säker på att du vill ta bort verifieringen av den här enheten?\nDen här enheten och meddelanden från den kommer att markeras som \"Ej betrodd\".</string>
<plurals name="seconds">
<item quantity="one">%d sekund</item>
<item quantity="other">%d sekunder</item>
@@ -649,12 +662,15 @@
<string name="corresponding_conversations_closed">Korresponderande konversationer är stängda.</string>
<string name="contact_blocked_past_tense">Kontakt blockerad.</string>
<string name="pref_notifications_from_strangers">Notifieringar från främlingar</string>
+ <string name="pref_notifications_from_strangers_summary">Meddela för meddelanden och samtal från främlingar.</string>
<string name="received_message_from_stranger">Mottagna meddelanden från främlingar</string>
<string name="block_stranger">Blockera främling</string>
<string name="block_entire_domain">Blockera hel domän</string>
<string name="online_right_now">online just nu</string>
<string name="retry_decryption">Försök dekryptera igen</string>
<string name="session_failure">Sessionsfel</string>
+ <string name="sasl_downgrade">Nedgraderad SASL-mekanism</string>
+ <string name="account_status_regis_web">Servern kräver registrering via webbplatsen</string>
<string name="open_website">Öppna webbsida</string>
<string name="application_found_to_open_website">Ingen applikation hittades för att kunna öppna webbsidan</string>
<string name="pref_headsup_notifications">Se upp-notifikationer</string>
@@ -662,46 +678,110 @@
<string name="today">Idag</string>
<string name="yesterday">Igår</string>
<string name="pref_validate_hostname">Bekräfta värdnamn med DNSSEC</string>
+ <string name="pref_validate_hostname_summary">Servercertifikat som innehåller det validerade värdnamnet anses vara verifierade</string>
<string name="certificate_does_not_contain_jid">Certifikatet innehåller ej en XMPP-adress</string>
<string name="server_info_partial">delvis</string>
<string name="attach_record_video">Spela in video</string>
<string name="copy_to_clipboard">Kopiera till urklipp</string>
<string name="message_copied_to_clipboard">Meddelande kopierat till urklipp</string>
<string name="message">Meddelande</string>
+ <string name="private_messages_are_disabled">Privata meddelanden är inaktiverade</string>
+ <string name="huawei_protected_apps">Skyddade applikationer</string>
+ <string name="huawei_protected_apps_summary">För att fortsätta ta emot aviseringar, även när skärmen är avstängd, måste du lägga till Conversations i listan över skyddade applikationer.</string>
<string name="mtm_accept_cert">Godkänn okänt certifikat?</string>
<string name="mtm_trust_anchor">Servercertifikatet är inte signerat av en känd certifikatutfärdare.</string>
+ <string name="mtm_accept_servername">Acceptera servernamn som inte matchar?</string>
+ <string name="mtm_hostname_mismatch">Servern kunde inte autentisera som \"%s\". Certifikatet är endast giltigt för:</string>
<string name="mtm_connect_anyway">Vill du ansluta ändå?</string>
<string name="mtm_cert_details">Certifikatdetaljer:</string>
+ <string name="once">En gång</string>
+ <string name="qr_code_scanner_needs_access_to_camera">QR-läsaren behöver åtkomst till kameran</string>
+ <string name="pref_scroll_to_bottom">Bläddra till botten</string>
+ <string name="pref_scroll_to_bottom_summary">Bläddra ner efter att du har skickat ett meddelande</string>
+ <string name="edit_status_message_title">Redigera Statusmeddelande</string>
+ <string name="edit_status_message">Redigera statusmeddelande</string>
+ <string name="disable_encryption">Inaktivera kryptering</string>
+ <string name="error_trustkey_general">%1$s kan inte skicka krypterade meddelanden till %2$s. Detta kan bero på att din kontakt använder en föråldrad server eller klient som inte kan hantera OMEMO.</string>
+ <string name="error_trustkey_device_list">Det gick inte att hämta enhetslistan</string>
+ <string name="error_trustkey_bundle">Det gick inte att hämta krypteringsnycklar</string>
+ <string name="error_trustkey_hint_mutual">Tips: I vissa fall kan detta åtgärdas genom att lägga till varandra i era respektive kontaktlistor.</string>
+ <string name="disable_encryption_message">Är du säker på att du vill inaktivera OMEMO-kryptering för den här konversationen?\nDetta gör att din serveradministratör kan läsa dina meddelanden, men det kan också vara det enda sättet att kommunicera med människor som använder äldre klienter.</string>
+ <string name="disable_now">Inaktivera nu</string>
<string name="draft">Utkast:</string>
+ <string name="pref_omemo_setting">OMEMO-kryptering</string>
+ <string name="pref_omemo_setting_summary_always">OMEMO kommer alltid att användas för privata konversationer och privata gruppchattar.</string>
+ <string name="pref_omemo_setting_summary_default_on">OMEMO kommer att användas som standard för nya konversationer.</string>
+ <string name="pref_omemo_setting_summary_default_off">OMEMO måste manuellt aktiveras för varje ny konversation.</string>
<string name="create_shortcut">Skapa genväg</string>
+ <string name="pref_font_size">Textstorlek</string>
+ <string name="pref_font_size_summary">Den relativa teckenstorleken som används i appen.</string>
+ <string name="default_on">På som standard</string>
+ <string name="default_off">Av som standard</string>
<string name="small">Liten</string>
<string name="medium">Mellan</string>
<string name="large">Stor</string>
+ <string name="not_encrypted_for_this_device">Meddelandet är inte krypterat för den här enheten.</string>
+ <string name="omemo_decryption_failed">Misslyckades med att dekryptera OMEMO-meddelandet.</string>
+ <string name="undo">ångra</string>
+ <string name="location_disabled">Platsdelning är inaktiverat</string>
+ <string name="action_fix_to_location">Lås position</string>
+ <string name="action_unfix_from_location">Lås upp position</string>
<string name="action_copy_location">Kopiera plats</string>
<string name="action_share_location">Dela plats</string>
+ <string name="action_directions">Hänvisningar</string>
<string name="title_activity_share_location">Dela plats</string>
<string name="title_activity_show_location">Visa plats</string>
<string name="share">Dela</string>
+ <string name="unable_to_start_recording">Det gick inte att starta inspelningen</string>
<string name="please_wait">Var god dröj...</string>
+ <string name="no_microphone_permission">Ge %1$s tillgång till mikrofonen</string>
<string name="search_messages">Söka i meddelanden</string>
<string name="gif">GIF</string>
+ <string name="view_conversation">Visa konversation</string>
+ <string name="pref_use_share_location_plugin">Dela plats-tillägget</string>
+ <string name="copy_link">Kopiera webbadress</string>
<string name="copy_jabber_id">Kopiera XMPP-adress</string>
+ <string name="p1_s3_filetransfer">HTTP-fildelning för S3</string>
+ <string name="pref_start_search">Direktsök</string>
+ <string name="group_chat_avatar">Gruppkonversationens visningsbild</string>
+ <string name="host_does_not_support_group_chat_avatars">Värden stöder inte visningsbilder för gruppkonversationer</string>
+ <string name="only_the_owner_can_change_group_chat_avatar">Endast ägaren kan ändra visningsbilden för gruppkonversationen</string>
+ <string name="contact_name">Kontaktnamn</string>
<string name="nickname">Smeknamn</string>
<string name="group_chat_name">Namn</string>
<string name="providing_a_name_is_optional">Att ange ett namn är valfritt</string>
<string name="create_dialog_group_chat_name">Gruppchattens namn</string>
+ <string name="unable_to_save_recording">Kunde inte att spara inspelningen</string>
+ <string name="foreground_service_channel_name">Förgrundsservice</string>
+ <string name="notification_group_status_information">Statusinformation</string>
+ <string name="error_channel_name">Anslutningsproblem</string>
+ <string name="notification_group_messages">Meddelanden</string>
+ <string name="notification_group_calls">Samtal</string>
+ <string name="messages_channel_name">Meddelanden</string>
+ <string name="incoming_calls_channel_name">Inkommande samtal</string>
+ <string name="ongoing_calls_channel_name">Pågående samtal</string>
+ <string name="silent_messages_channel_name">Tysta meddelanden</string>
+ <string name="delivery_failed_channel_name">Misslyckade leveranser</string>
+ <string name="video_compression_channel_name">Videokompression</string>
+ <string name="view_media">Visa media</string>
<string name="group_chat_members">Deltagare</string>
+ <string name="media_browser">Mediautforskare</string>
+ <string name="pref_video_compression">Videokvalitet</string>
<string name="video_360p">Mellan (360p)</string>
<string name="video_720p">Hög (720p)</string>
+ <string name="cancelled">avbruten</string>
+ <string name="already_drafting_message">Du håller redan på att skriva ett meddelande.</string>
<string name="choose_a_country">Välj ett land</string>
<string name="phone_number">telefonnummer</string>
<string name="verify_your_phone_number">Bekräfta ditt telefonnummer</string>
+ <string name="back">tillbaka</string>
<string name="yes">Ja</string>
<string name="no">Nej</string>
<string name="verifying">Bekräftar...</string>
<string name="unknown_api_error_network">Okänt nätverksfel.</string>
<string name="too_many_attempts">För många försök</string>
<string name="the_app_is_out_of_date">Du använder en föråldrad version av denna app.</string>
+ <string name="update">Uppdatera</string>
<string name="your_name">Ditt namn</string>
<string name="enter_your_name">Skriv in ditt namn</string>
<string name="reject_request">Avslå begäran</string>
@@ -709,38 +789,130 @@
<string name="start_orbot">Starta Orbot</string>
<string name="ebook">e-bok</string>
<string name="open_with">Öppna med...</string>
+ <string name="set_profile_picture">Konversationens profilbild</string>
<string name="choose_account">Välj konto</string>
<string name="restore_backup">Återställa säkerhetskopiering</string>
<string name="restore">Återställa</string>
<string name="enter_password_to_restore">Ange ditt lösenord till kontot %s för att återställa säkerhetskopian.</string>
<string name="unable_to_restore_backup">Det gick inte att återställa säkerhetskopian.</string>
+ <string name="backup_channel_name">Säkerhetskopia & Återställ</string>
+ <string name="enter_jabber_id">Ange XMPP-adress</string>
<string name="create_group_chat">Skapa gruppchatt</string>
+ <string name="join_public_channel">Anslut till publik gruppkonversation</string>
<string name="create_private_group_chat">Skapa sluten gruppchatt</string>
+ <string name="create_public_channel">Skapa publik gruppkonversation</string>
<string name="create_dialog_channel_name">Kanalnamn</string>
<string name="xmpp_address">XMPP-adress</string>
<string name="please_enter_name">Vänligen ange ett namn på kanalen</string>
<string name="please_enter_xmpp_address">Ange en XMPP-adress</string>
<string name="this_is_an_xmpp_address">Detta är en XMPP-adress. Ange ett namn.</string>
+ <string name="creating_channel">Skapar publik gruppkonversation...</string>
<string name="channel_already_exists">Denna kanal finns redan</string>
<string name="joined_an_existing_channel">Du har gått med i en befintlig kanal</string>
+ <string name="unable_to_set_channel_configuration">Det gick inte att spara kanalkonfigurationen</string>
+ <string name="allow_participants_to_edit_subject">Tillåt vem som helst att ändra ämnet</string>
+ <string name="allow_participants_to_invite_others">Tillåt vem som helst att bjuda in andra</string>
+ <string name="anyone_can_edit_subject">Vem som helst kan ändra ämnet.</string>
+ <string name="owners_can_edit_subject">Ägaren kan ändra ämnet.</string>
+ <string name="admins_can_edit_subject">Administratörer kan ändra ämnet.</string>
+ <string name="owners_can_invite_others">Ägare kan bjuda in andra.</string>
+ <string name="anyone_can_invite_others">Vem som helst kan bjuda in andra.</string>
<string name="jabber_ids_are_visible_to_admins">XMPP-adresser är synliga för administratörer.</string>
<string name="jabber_ids_are_visible_to_anyone">XMPP-adresser är synliga för alla.</string>
+ <string name="no_users_hint_channel">Den här publika gruppkonversationen har inga deltagare. Bjud in dina kontakter eller använd \'dela-knappen\' för att dela XMPP-adressen.</string>
<string name="no_users_hint_group_chat">Denna slutna gruppchatt har inga deltagare.</string>
<string name="manage_permission">Hantera rättigheter</string>
+ <string name="search_participants">Sök efter deltagare</string>
<string name="file_too_large">För stor fil</string>
<string name="attach">Bifoga</string>
<string name="discover_channels">Upptäck kanaler</string>
+ <string name="search_channels">Sök efter gruppkonversationer</string>
+ <string name="channel_discovery_opt_in_title">Möjlig integritetskränkning!</string>
<string name="i_already_have_an_account">Jag har redan ett konto</string>
<string name="add_existing_account">Lägg till befintligt konto</string>
<string name="register_new_account">Skapa nytt konto</string>
<string name="this_looks_like_a_domain">Detta verkar vara ett domännamn</string>
<string name="add_anway">Lägg till ändå</string>
<string name="this_looks_like_channel">Detta ser ut som en kanaladress</string>
+ <string name="share_backup_files">Dela säkerhetskopior</string>
+ <string name="conversations_backup">Säkerhetskopior för Conversations</string>
+ <string name="event">Händelse</string>
+ <string name="open_backup">Öppna säkerhetskopia</string>
<string name="not_a_backup_file">Filen du valde är inte en säkerhetskopia till Conversations</string>
+ <string name="account_already_setup">Det här kontot har redan konfigurerats</string>
+ <string name="please_enter_password">Var god ange lösenordet för det här kontot</string>
+ <string name="unable_to_perform_this_action">Det gick inte att utföra den här åtgärden</string>
+ <string name="open_join_dialog">Anslut till publik gruppkonversation...</string>
+ <string name="sharing_application_not_grant_permission">Delnings-appen gav inte behörighet till att komma åt den här filen.</string>
+ <string name="group_chats_and_channels"><![CDATA[Gruppkonversationer & Kanaler]]></string>
+ <string name="jabber_network">jabber.network</string>
+ <string name="local_server">Lokal server</string>
+ <string name="pref_channel_discovery_summary">De flesta användare bör välja \"jabber.network\" för bättre förslag från hela det offentliga XMPP-ekosystemet.</string>
+ <string name="pref_channel_discovery">Metod för kanalupptäckt</string>
+ <string name="backup">Säkerhetskopiering</string>
<string name="category_about">Om</string>
<string name="please_enable_an_account">Aktivera ett konto</string>
+ <string name="make_call">Ring</string>
+ <string name="rtp_state_incoming_call">Inkommande samtal</string>
+ <string name="rtp_state_incoming_video_call">Inkommande videosamtal</string>
+ <string name="rtp_state_connecting">Ansluter</string>
+ <string name="rtp_state_connected">Ansluten</string>
+ <string name="rtp_state_reconnecting">Återansluter</string>
+ <string name="rtp_state_accepting_call">Accepterar samtal</string>
+ <string name="rtp_state_ending_call">Avslutar samtal</string>
+ <string name="answer_call">Svara</string>
+ <string name="dismiss_call">Avvisa</string>
+ <string name="rtp_state_finding_device">Upptäcker enheter</string>
+ <string name="rtp_state_ringing">Ringer</string>
<string name="rtp_state_declined_or_busy">Upptagen</string>
+ <string name="rtp_state_connectivity_error">Kunde inte koppla samtal</string>
+ <string name="rtp_state_connectivity_lost_error">Anslutning bröts</string>
+ <string name="rtp_state_retracted">Återkallat samtal</string>
+ <string name="rtp_state_application_failure">Appmisslyckande</string>
+ <string name="rtp_state_security_error">Verifikationsproblem</string>
+ <string name="hang_up">Lägg på</string>
+ <string name="ongoing_call">Pågående samtal</string>
+ <string name="ongoing_video_call">Pågående videosamtal</string>
+ <string name="reconnecting_call">Återansluter samtalet</string>
+ <string name="reconnecting_video_call">Återansluter videosamtalet</string>
+ <string name="disable_tor_to_make_call">Inaktivera Tor för att ringa samtal</string>
+ <string name="incoming_call">Inkommande samtal</string>
+ <string name="incoming_call_duration">Inkommande samtal · %s</string>
+ <string name="missed_call_timestamp">Missat samtal · %s</string>
+ <string name="outgoing_call">Utgående samtal</string>
+ <string name="outgoing_call_duration">Pågående samtal · %s</string>
+ <string name="missed_call">Missat samtal</string>
+ <string name="audio_call">Röstsamtal</string>
+ <string name="video_call">Videosamtal</string>
+ <string name="help">Hjälp</string>
+ <string name="switch_to_conversation">Växla till konversation</string>
+ <string name="microphone_unavailable">Din mikrofon är inte tillgänglig</string>
+ <string name="only_one_call_at_a_time">Du kan bara ha ett samtal åt gången.</string>
+ <string name="return_to_ongoing_call">Återgå till pågående samtal</string>
+ <string name="could_not_switch_camera">Kunde inte växla kamera</string>
<string name="add_to_favorites">Fäst flik till toppen</string>
<string name="remove_from_favorites">Ta bort flik från toppen</string>
+ <string name="gpx_track">GPX-spår</string>
+ <string name="could_not_correct_message">Kunde inte korrigera meddelandet</string>
+ <string name="search_all_conversations">Alla konversationer</string>
+ <string name="search_this_conversation">Den här konversationen</string>
+ <string name="your_avatar">Din visningsbild</string>
+ <string name="avatar_for_x">Visningsbild för %s</string>
+ <string name="encrypted_with_omemo">Krypterad med OMEMO</string>
+ <string name="encrypted_with_openpgp">Krypterad med OpenPGP</string>
+ <string name="not_encrypted">Inte krypterad</string>
+ <string name="exit">Avsluta</string>
+ <string name="record_voice_mail">Spela in ett röstmeddelande</string>
+ <string name="play_audio">Spela upp ljud</string>
+ <string name="pause_audio">Pausa ljud</string>
+ <string name="add_contact_or_create_or_join_group_chat">Lägg till kontakt, skapa eller gå med i gruppchatt eller upptäck kanaler</string>
+ <plurals name="view_users">
+ <item quantity="one">Visa %1$d deltagare</item>
+ <item quantity="other">Visa %1$d deltagare</item>
+ </plurals>
+ <string name="failed_deliveries">Misslyckade leveranser</string>
<string name="more_options">Fler alternativ</string>
+ <string name="no_application_found">Ingen applikation hittades</string>
+ <string name="invite_to_app">Bjud in till Conversations</string>
+ <string name="no_xmpp_adddress_found">Ingen XMPP-adress hittades</string>
</resources>
@@ -414,6 +414,7 @@
<string name="video">视频</string>
<string name="image">图片</string>
<string name="vector_graphic">矢量图</string>
+ <string name="multimedia_file">多媒体文件</string>
<string name="pdf_document">PDF文档</string>
<string name="apk">Android App</string>
<string name="vcard">联系人</string>
@@ -1,97 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="action_settings">設定</string>
- <string name="action_add">新對話</string>
+ <string name="action_add">新會話</string>
<string name="action_accounts">管理帳戶</string>
- <string name="action_contact_details">聯絡人詳情</string>
+ <string name="action_account">管理帳戶</string>
+ <string name="action_end_conversation">關閉會話</string>
+ <string name="action_contact_details">聯絡人詳細資料</string>
+ <string name="action_muc_details">群組聊天詳細資料</string>
+ <string name="channel_details">頻道詳細資料</string>
<string name="action_add_account">新增帳戶</string>
- <string name="action_edit_contact">編輯姓名</string>
- <string name="action_add_phone_book">添加到地址薄</string>
- <string name="action_delete_contact">從列表中刪除</string>
+ <string name="action_edit_contact">編輯名稱</string>
+ <string name="action_add_phone_book">新增至通訊錄</string>
+ <string name="action_delete_contact">從名冊中刪除</string>
<string name="action_block_contact">封鎖連絡人</string>
<string name="action_unblock_contact">解除封鎖連絡人</string>
<string name="action_block_domain">封鎖網域</string>
<string name="action_unblock_domain">解除封鎖網域</string>
+ <string name="action_block_participant">封鎖成員</string>
+ <string name="action_unblock_participant">解除封鎖成員</string>
<string name="title_activity_manage_accounts">管理帳戶</string>
- <string name="title_activity_settings">設置</string>
- <string name="title_activity_sharewith">分享到 Conversation</string>
+ <string name="title_activity_settings">設定</string>
+ <string name="title_activity_sharewith">分享至 Conversation</string>
<string name="title_activity_start_conversation">開始會話</string>
+ <string name="title_activity_choose_contact">選擇聯絡人</string>
+ <string name="title_activity_choose_contacts">選擇聯絡人</string>
+ <string name="title_activity_share_via_account">透過帳戶分享</string>
<string name="title_activity_block_list">封鎖清單</string>
<string name="just_now">剛剛</string>
<string name="minute_ago">1 分鐘前</string>
- <string name="minutes_ago">%d分鐘前</string>
- <string name="sending">正在發送…</string>
- <string name="message_decrypting">訊息解密中,請稍候…</string>
- <string name="pgp_message">OpenPGP 加密的信息</string>
- <string name="nick_in_use">該名稱已存在</string>
+ <string name="minutes_ago">%d 分鐘前</string>
+ <plurals name="x_unread_conversations">
+ <item quantity="other">%d 則未讀會話</item>
+
+ </plurals>
+ <string name="sending">正在傳送…</string>
+ <string name="message_decrypting">正在解密訊息,請稍候…</string>
+ <string name="pgp_message">OpenPGP 已加密的訊息</string>
+ <string name="nick_in_use">暱稱已有人使用</string>
+ <string name="invalid_muc_nick">無效的暱稱</string>
<string name="admin">管理員</string>
- <string name="owner">所有者</string>
+ <string name="owner">擁有者</string>
<string name="moderator">版主</string>
- <string name="participant">參與者</string>
+ <string name="participant">成員</string>
<string name="visitor">訪客</string>
- <string name="block_contact_text">要封鎖 %s 讓它不能送訊息給你嗎?</string>
- <string name="unblock_contact_text">要解除封鎖 %s 讓它可以送訊息給你嗎?</string>
- <string name="block_domain_text">要封鎖來自 %s 的所有連絡人嗎?</string>
- <string name="unblock_domain_text">要解除封鎖來自 %s 的所有連絡人嗎?</string>
+ <string name="remove_contact_text">要將 %s 從你的聯絡人清單中移除嗎?與此聯絡人的會話將不會被移除。</string>
+ <string name="block_contact_text">要封鎖 %s 向您傳送訊息嗎?</string>
+ <string name="unblock_contact_text">要解除封鎖 %s 並允許他們向您傳送訊息嗎?</string>
+ <string name="block_domain_text">要封鎖來自 %s 的所有聯絡人嗎?</string>
+ <string name="unblock_domain_text">要解除封鎖來自 %s 的所有聯絡人嗎?</string>
<string name="contact_blocked">連絡人已封鎖</string>
+ <string name="blocked">已封鎖</string>
+ <string name="remove_bookmark_text">要從書籤中移除 %s 嗎?與此書籤相關的會話將不會被移除。</string>
<string name="register_account">在伺服器上註冊新帳戶</string>
- <string name="change_password_on_server">在伺服器上改變密碼</string>
- <string name="share_with">分享…</string>
- <string name="contacts">連絡人</string>
- <string name="contact">連絡人</string>
+ <string name="change_password_on_server">在伺服器上變更密碼</string>
+ <string name="share_with">分享至…</string>
+ <string name="start_conversation">開始會話</string>
+ <string name="invite_contact">邀請聯絡人</string>
+ <string name="invite">邀請</string>
+ <string name="contacts">聯絡人</string>
+ <string name="contact">聯絡人</string>
<string name="cancel">取消</string>
- <string name="set">設置</string>
- <string name="add">添加</string>
+ <string name="set">設定</string>
+ <string name="add">新增</string>
<string name="edit">編輯</string>
<string name="delete">刪除</string>
<string name="block">封鎖</string>
<string name="unblock">解除封鎖</string>
- <string name="save">保存</string>
+ <string name="save">儲存</string>
<string name="ok">完成</string>
- <string name="send_now">現在發送</string>
+ <string name="crash_report_title">%1$s 已當機</string>
+ <string name="send_now">立即傳送</string>
<string name="send_never">不再詢問</string>
+ <string name="problem_connecting_to_account">無法連線至帳戶</string>
+ <string name="problem_connecting_to_accounts">無法連線至多個帳戶</string>
+ <string name="touch_to_fix">輕觸以管理你的帳戶</string>
<string name="attach_file">附加檔案</string>
- <string name="add_contact">添加連絡人</string>
+ <string name="not_in_roster">要將這位遺失的聯絡人新增至你的聯絡人清單嗎?</string>
+ <string name="add_contact">新增聯絡人</string>
<string name="send_failed">傳遞失敗</string>
- <string name="sharing_files_please_wait">正在分享檔案中,請稍候…</string>
+ <string name="preparing_image">正在準備傳送圖片</string>
+ <string name="preparing_images">正在準備傳送圖片</string>
+ <string name="sharing_files_please_wait">正在分享檔案,請稍候…</string>
<string name="action_clear_history">清除歷史記錄</string>
<string name="clear_conversation_history">清除會話記錄</string>
- <string name="choose_presence">選擇設備</string>
- <string name="send_unencrypted_message">發送未加密的訊息</string>
- <string name="send_message">送訊息</string>
- <string name="send_message_to_x">送訊息給 %s</string>
- <string name="send_omemo_message">送 OMEMO 加密訊息</string>
- <string name="send_omemo_x509_message">送 v\\OMEMO 加密訊息</string>
- <string name="send_pgp_message">送 OpenPGP 加密訊息</string>
- <string name="send_unencrypted">不加密發送</string>
+ <string name="delete_file_dialog">刪除檔案</string>
+ <string name="also_end_conversation">之後關閉此會話</string>
+ <string name="choose_presence">選擇裝置</string>
+ <string name="send_unencrypted_message">傳送未加密的訊息</string>
+ <string name="send_message">傳送訊息</string>
+ <string name="send_message_to_x">傳送訊息至 %s</string>
+ <string name="send_omemo_message">傳送 OMEMO 加密訊息</string>
+ <string name="send_omemo_x509_message">傳送 v\\OMEMO 加密訊息</string>
+ <string name="send_pgp_message">傳送 OpenPGP 加密訊息</string>
+ <string name="your_nick_has_been_changed">新暱稱已被使用</string>
+ <string name="send_unencrypted">不加密傳送</string>
<string name="decryption_failed">解密失敗,可能是私密金鑰不正確。</string>
<string name="openkeychain_required">OpenKeychain</string>
- <string name="restart">重啟</string>
+ <string name="restart">重新啟動</string>
<string name="install">安裝</string>
<string name="openkeychain_not_installed">請安裝 OpenKeychain 以解密</string>
- <string name="offering">輸入…</string>
- <string name="waiting">等待…</string>
- <string name="no_pgp_key">未發現 OpenPGP 金鑰</string>
+ <string name="offering">正在提供…</string>
+ <string name="waiting">正在等候…</string>
+ <string name="no_pgp_key">找不到 OpenPGP 金鑰</string>
<string name="no_pgp_keys">未找到 OpenPGP 金鑰</string>
- <string name="pref_general">常規</string>
- <string name="pref_accept_files">接收檔案</string>
- <string name="pref_accept_files_summary">自動接收小於 … 的檔案</string>
+ <string name="pref_general">一般</string>
+ <string name="pref_accept_files">接受檔案</string>
+ <string name="pref_accept_files_summary">自動接受小於此大小的檔案</string>
<string name="pref_attachments">附件</string>
<string name="pref_notification_settings">通知</string>
<string name="pref_vibrate">震動</string>
<string name="pref_vibrate_summary">收到新訊息時震動</string>
- <string name="pref_led">LED 燈通知</string>
+ <string name="pref_led">LED 通知</string>
<string name="pref_led_summary">收到新訊息時閃爍通知燈</string>
<string name="pref_ringtone">鈴聲</string>
+ <string name="pref_notification_sound">通知音效</string>
+ <string name="pref_notification_sound_summary">收到新訊息時發出通知音效</string>
+ <string name="pref_call_ringtone_summary">來電時響鈴</string>
<string name="pref_notification_grace_period">靜默期限</string>
- <string name="pref_advanced_options">高級</string>
- <string name="pref_never_send_crash">總不發送崩潰報告</string>
+ <string name="pref_advanced_options">進階</string>
+ <string name="pref_never_send_crash">永不傳送當機報告</string>
<string name="pref_confirm_messages">確認訊息</string>
- <string name="pref_confirm_messages_summary">讓聯絡人知道它們的訊息已經收到以及讀取</string>
+ <string name="pref_confirm_messages_summary">讓你的聯絡人知道你已經收到並閱讀了他們的訊息</string>
+ <string name="pref_prevent_screenshots">防止截圖</string>
+ <string name="pref_prevent_screenshots_summary">在多工畫面隱藏應用程式聯絡人並且封鎖螢幕截圖</string>
<string name="pref_ui_options">UI</string>
<string name="accept">接受</string>
<string name="error">產生了一個錯誤</string>
- <string name="your_account">你的帳號</string>
+ <string name="your_account">你的帳戶</string>
<string name="send_presence_updates">發送線上連絡人列表更新</string>
<string name="receive_presence_updates">接收線上連絡人列表更新</string>
<string name="ask_for_presence_updates">請求線上連絡人列表更新</string>
@@ -114,12 +149,12 @@
<string name="account_status_regis_success">註冊完成</string>
<string name="account_status_policy_violation">違反政策</string>
<string name="account_status_incompatible_server">伺服器不相容</string>
- <string name="account_status_stream_error">流錯誤</string>
+ <string name="account_status_stream_error">串流錯誤</string>
<string name="encryption_choice_unencrypted">TLS</string>
<string name="encryption_choice_otr">OTR</string>
<string name="encryption_choice_pgp">OpenPGP</string>
<string name="encryption_choice_omemo">OMEMO</string>
- <string name="mgmt_account_delete">刪除帳號</string>
+ <string name="mgmt_account_delete">刪除帳戶</string>
<string name="mgmt_account_disable">暫時不可用</string>
<string name="mgmt_account_publish_avatar">發佈頭像</string>
<string name="mgmt_account_publish_pgp">發佈 OpenPGP 公開金鑰</string>
@@ -128,16 +163,21 @@
<string name="mgmt_account_enable">啟用帳戶</string>
<string name="mgmt_account_are_you_sure">確定?</string>
<string name="attach_record_voice">錄音</string>
+ <string name="account_settings_jabber_id">XMPP 位址</string>
+ <string name="block_jabber_id">封鎖 XMPP 位址</string>
<string name="account_settings_example_jabber_id">username@example.com</string>
<string name="password">密碼</string>
- <string name="add_phone_book_text">是否添加 %s 到地址薄?</string>
+ <string name="invalid_jid">這不是有效的 XMPP 位址</string>
+ <string name="error_out_of_memory">記憶體不足,圖片過大</string>
+ <string name="add_phone_book_text">要將 %s 新增至通訊錄嗎?</string>
<string name="server_info_show_more">伺服器資訊</string>
<string name="server_info_mam">XEP-0313: MAM</string>
<string name="server_info_carbon_messages">XEP-0280: 訊息複本</string>
<string name="server_info_csi">XEP-0352: 用戶端狀態指示</string>
- <string name="server_info_blocking">XEP-0191: 封鎖指令</string>
- <string name="server_info_roster_version">XEP-0237: 花名冊版本控制</string>
- <string name="server_info_stream_management">XEP-0198: 流管理</string>
+ <string name="server_info_blocking">XEP-0191: 封鎖命令</string>
+ <string name="server_info_roster_version">XEP-0237: 名冊版本設定</string>
+ <string name="server_info_stream_management">XEP-0198: 串流管理</string>
+ <string name="server_info_external_service_discovery">XEP-0215: 外部服務探索</string>
<string name="server_info_pep">XEP-0163: PEP (替身 / OMEMO)</string>
<string name="server_info_http_upload">XEP-0363: HTTP 檔案上傳</string>
<string name="server_info_push">XEP-0357: Push</string>
@@ -151,25 +191,31 @@
<string name="openpgp_key_id">OpenPGP 金鑰 ID</string>
<string name="omemo_fingerprint">OMEMO 指紋</string>
<string name="omemo_fingerprint_x509">v\\OMEMO 指紋</string>
- <string name="other_devices">其他設備</string>
+ <string name="other_devices">其他裝置</string>
<string name="trust_omemo_fingerprints">信任的 OMEMO 指紋</string>
- <string name="fetching_keys">獲取金鑰中</string>
+ <string name="fetching_keys">正在擷取金鑰…</string>
<string name="done">完成</string>
<string name="decrypt">解密</string>
- <string name="search">查找</string>
- <string name="enter_contact">輸入連絡人</string>
- <string name="view_contact_details">查看連絡人詳細資訊</string>
- <string name="block_contact">封鎖連絡人</string>
- <string name="unblock_contact">解除封鎖連絡人</string>
- <string name="create">創建</string>
- <string name="select">選擇</string>
- <string name="contact_already_exists">連絡人已存在</string>
+ <string name="bookmarks">書籤</string>
+ <string name="search">尋找</string>
+ <string name="enter_contact">輸入聯絡人</string>
+ <string name="delete_contact">刪除聯絡人</string>
+ <string name="view_contact_details">檢視聯絡人詳細資料</string>
+ <string name="block_contact">封鎖聯絡人</string>
+ <string name="unblock_contact">解除封鎖聯絡人</string>
+ <string name="create">建立</string>
+ <string name="select">選取</string>
+ <string name="contact_already_exists">聯絡人已存在</string>
<string name="join">加入</string>
- <string name="save_as_bookmark">保存為書簽</string>
- <string name="delete_bookmark">刪除書簽</string>
+ <string name="channel_full_jid_example">channel@conference.example.com/nick</string>
+ <string name="channel_bare_jid_example">channel@conference.example.com</string>
+ <string name="save_as_bookmark">儲存為書籤</string>
+ <string name="delete_bookmark">刪除書籤</string>
+ <string name="topic">主旨</string>
+ <string name="joining_conference">正在加入群組聊天…</string>
<string name="leave">離開</string>
- <string name="contact_added_you">連絡人已添加你到連絡人列表</string>
- <string name="add_back">反向添加</string>
+ <string name="contact_added_you">聯絡人已新增至你的聯絡人清單</string>
+ <string name="add_back">新增回</string>
<string name="contact_has_read_up_to_this_point">%s 已讀此句</string>
<string name="publish">發佈</string>
<string name="publishing">正在發佈…</string>
@@ -180,25 +226,29 @@
<string name="private_message_to">至 %s</string>
<string name="send_private_message_to">送私密訊息給 %s</string>
<string name="connect">連接</string>
- <string name="account_already_exists">該帳號已存在</string>
+ <string name="account_already_exists">此帳戶已存在</string>
<string name="next">下一步</string>
- <string name="skip">忽略</string>
+ <string name="server_info_session_established">工作階段已建立</string>
+ <string name="skip">跳過</string>
<string name="disable_notifications">關閉通知</string>
<string name="enable">打開通知</string>
+ <string name="conference_requires_password">群組聊天需要密碼</string>
<string name="enter_password">輸入密碼</string>
- <string name="request_now">現在發送請求</string>
+ <string name="request_now">立即要求</string>
<string name="ignore">忽略</string>
- <string name="pref_security_settings">安全</string>
+ <string name="pref_security_settings">安全性</string>
<string name="pref_allow_message_correction">允許更正訊息</string>
<string name="pref_allow_message_correction_summary">允許您的連絡人追回編輯他們的訊息</string>
- <string name="pref_expert_options">高級設置</string>
+ <string name="pref_expert_options">專家設定</string>
<string name="pref_expert_options_summary">請謹慎使用</string>
+ <string name="title_activity_about_x">關於 %s</string>
<string name="title_pref_quiet_hours">靜默時間段</string>
<string name="title_pref_quiet_hours_start_time">開始時間</string>
<string name="title_pref_quiet_hours_end_time">結束時間</string>
<string name="title_pref_enable_quiet_hours">啟用靜默時間段</string>
<string name="pref_quiet_hours_summary">在靜默時間段內通知將保持靜音</string>
<string name="pref_expert_options_other">其他</string>
+ <string name="pref_autojoin">同步處理書籤</string>
<string name="using_account">用帳戶 %s</string>
<string name="checking_x">正在 HTTP 伺服器中檢查 %s</string>
<string name="not_connected_try_again">你沒有連接。請稍後重試</string>
@@ -208,7 +258,11 @@
<string name="quote">引用</string>
<string name="copy_original_url">拷貝原始URL</string>
<string name="send_again">再次發送</string>
- <string name="file_url">檔案位址(URL)</string>
+ <string name="file_url">檔案 URL</string>
+ <string name="url_copied_to_clipboard">已複製 URL 到剪貼簿</string>
+ <string name="jabber_id_copied_to_clipboard">已複製 XMPP 位址到剪貼簿</string>
+ <string name="error_message_copied_to_clipboard">已複製錯誤訊息到剪貼簿</string>
+ <string name="web_address">網頁地址</string>
<string name="scan_qr_code">掃描二維條碼</string>
<string name="show_qr_code">顯示二維條碼</string>
<string name="show_block_list">顯示封鎖清單</string>
@@ -216,15 +270,26 @@
<string name="confirm">確認</string>
<string name="try_again">再試一遍</string>
<string name="pref_keep_foreground_service_summary">防止作業系統中斷你的連接</string>
- <string name="choose_file">選檔案</string>
- <string name="receiving_x_file">接收中 %1$s (已完成 %2$d%%)</string>
+ <string name="pref_create_backup">建立備份</string>
+ <string name="pref_create_backup_summary">備份檔案將被儲存至 %s</string>
+ <string name="notification_create_backup_title">正在建立備份檔案</string>
+ <string name="notification_backup_created_title">你的備份已建立</string>
+ <string name="notification_backup_created_subtitle">此備份檔案已被儲存至 %s</string>
+ <string name="restoring_backup">正在還原備份</string>
+ <string name="notification_restored_backup_title">你的備份已還原</string>
+ <string name="notification_restored_backup_subtitle">不要忘記啟用帳戶。</string>
+ <string name="choose_file">選擇檔案</string>
+ <string name="receiving_x_file">正在接收 %1$s (已完成 %2$d%%)</string>
<string name="download_x_file">下載 %s</string>
<string name="delete_x_file">刪除 %s</string>
<string name="file">檔案</string>
- <string name="open_x_file"> 打開 %s</string>
- <string name="sending_file">發送中 (已完成 %1$d%%)</string>
+ <string name="open_x_file">開啟 %s</string>
+ <string name="sending_file">正在傳送 (已完成 %1$d%%)</string>
+ <string name="preparing_file">正在準備分享檔案</string>
<string name="x_file_offered_for_download">可以下載 %s</string>
<string name="cancel_transmission">取消傳送</string>
+ <string name="file_transmission_failed">無法分享檔案</string>
+ <string name="file_deleted">檔案已刪除</string>
<string name="pref_show_dynamic_tags_summary">在連絡人下方顯示唯讀標籤</string>
<string name="enable_notifications">啟用通知</string>
<string name="account_image_description">帳戶頭像</string>
@@ -418,20 +483,122 @@
<string name="retry_decryption">再試解密ㄧ次</string>
<string name="session_failure">通訊對話錯誤</string>
<string name="pref_headsup_notifications">頭條通知</string>
- <string name="message_copied_to_clipboard">消息已經拷貝到剪貼板</string>
+ <string name="today">今天</string>
+ <string name="yesterday">昨天</string>
+ <string name="attach_record_video">錄製影片</string>
+ <string name="copy_to_clipboard">複製到剪貼簿</string>
+ <string name="message_copied_to_clipboard">訊息已複製到剪貼簿</string>
+ <string name="message">訊息</string>
+ <string name="private_messages_are_disabled">私密訊息已停用</string>
+ <string name="huawei_protected_apps">受保護的應用程式</string>
+ <string name="mtm_accept_cert">接受未知憑證?</string>
+ <string name="mtm_cert_details">憑證詳細資料:</string>
+ <string name="once">僅一次</string>
+ <string name="pref_scroll_to_bottom">捲動至底部</string>
+ <string name="pref_scroll_to_bottom_summary">傳送訊息後向下捲動</string>
+ <string name="edit_status_message_title">編輯狀態訊息</string>
+ <string name="edit_status_message">編輯訊息</string>
+ <string name="disable_encryption">停用加密</string>
+ <string name="error_trustkey_device_list">無法擷取裝置清單</string>
+ <string name="error_trustkey_bundle">無法擷取加密金鑰</string>
+ <string name="disable_now">立即停用</string>
<string name="pref_omemo_setting">OMEMO 加密</string>
<string name="pref_omemo_setting_summary_always">一對一以及私人群組的聊天一定會用 OMEMO</string>
<string name="pref_omemo_setting_summary_default_on">新的對話預設會用 OMEMO 加密</string>
- <string name="pref_omemo_setting_summary_default_off">新的對話必須要手動開啟 OMEMO 加密</string>
+ <string name="pref_omemo_setting_summary_default_off">新的會話必須要手動開啟 OMEMO 加密</string>
+ <string name="create_shortcut">建立捷徑</string>
<string name="pref_font_size">字型大小</string>
- <string name="pref_font_size_summary">App 中所使用的相對字型大小</string>
+ <string name="pref_font_size_summary">應用程式中使用的相對字型大小</string>
<string name="default_on">預設開啟</string>
<string name="default_off">預設關閉</string>
<string name="small">小</string>
- <string name="medium">適中</string>
+ <string name="medium">中</string>
<string name="large">大</string>
+ <string name="undo">復原</string>
+ <string name="location_disabled">位置分享已停用</string>
+ <string name="action_fix_to_location">固定位置</string>
+ <string name="action_unfix_from_location">取消固定位置</string>
+ <string name="action_copy_location">複製位置</string>
+ <string name="action_share_location">分享位置</string>
+ <string name="action_directions">方向</string>
+ <string name="title_activity_share_location">分享位置</string>
<string name="title_activity_show_location">顯示位置</string>
+ <string name="share">分享</string>
+ <string name="unable_to_start_recording">無法開始錄製</string>
+ <string name="please_wait">請稍候…</string>
+ <string name="no_microphone_permission">授予 %1$s 以存取麥克風</string>
<string name="search_messages">搜尋訊息</string>
+ <string name="gif">GIF</string>
+ <string name="view_conversation">檢視會話</string>
+ <string name="pref_use_share_location_plugin">分享位置外掛程式</string>
+ <string name="pref_use_share_location_plugin_summary">使用分享位置外掛程式而非內建地圖</string>
+ <string name="copy_link">複製網站位址</string>
+ <string name="copy_jabber_id">複製 XMPP 位址</string>
+ <string name="pref_start_search">直接搜尋</string>
+ <string name="nickname">暱稱</string>
+ <string name="group_chat_name">名稱</string>
<string name="create_dialog_group_chat_name">聊天群組名稱</string>
+ <string name="unable_to_save_recording">無法儲存錄製</string>
+ <string name="notification_group_status_information">狀態資訊</string>
+ <string name="notification_group_messages">訊息 </string>
+ <string name="notification_group_calls">通話</string>
+ <string name="messages_channel_name">訊息</string>
+ <string name="incoming_calls_channel_name">來電</string>
+ <string name="ongoing_calls_channel_name">正在進行的通話</string>
+ <string name="silent_messages_channel_name">無聲訊息</string>
+ <string name="pref_message_notification_settings">訊息通知設定</string>
+ <string name="pref_incoming_call_notification_settings">來電通知設定</string>
+ <string name="video_compression_channel_name">影片壓縮</string>
+ <string name="view_media">檢視媒體</string>
+ <string name="group_chat_members">成員</string>
+ <string name="media_browser">媒體瀏覽器</string>
+ <string name="pref_video_compression">影片質量</string>
+ <string name="pref_video_compression_summary">低質量意味這更小的檔案</string>
+ <string name="video_360p">中 (360P)</string>
+ <string name="video_720p">高 (720P)</string>
+ <string name="cancelled">已取消</string>
+ <string name="invalid_country_code">無效的國家碼</string>
+ <string name="choose_a_country">選擇國家</string>
+ <string name="phone_number">電話號碼</string>
+ <string name="verify_your_phone_number">驗證電話號碼</string>
+ <string name="please_enter_your_phone_number">請輸入您的電話號碼。</string>
+ <string name="search_countries">搜尋國家</string>
+ <string name="verify_x">驗證 %s</string>
+ <string name="resend_sms">重新傳送簡訊</string>
+ <string name="resend_sms_in">重新傳送簡訊 (%s)</string>
+ <string name="wait_x">請等候 (%s)</string>
+ <string name="back">返回</string>
+ <string name="yes">是</string>
+ <string name="no">否</string>
+ <string name="verifying">正在驗證…</string>
+ <string name="requesting_sms">正在要求簡訊…</string>
+ <string name="unknown_api_error_network">未知網路錯誤。</string>
+ <string name="no_network_connection">沒有網路連線。</string>
+ <string name="update">更新</string>
+ <string name="your_name">你的名稱</string>
+ <string name="enter_your_name">輸入你的名稱</string>
+ <string name="reject_request">拒絕要求</string>
+ <string name="ebook">電子書</string>
+ <string name="open_with">開啟為…</string>
+ <string name="choose_account">選擇帳戶</string>
+ <string name="restore_backup">還原備份</string>
+ <string name="restore">還原</string>
+ <string name="backup_channel_name">備份與還原</string>
+ <string name="create_group_chat">建立群組聊天</string>
+ <string name="join_public_channel">加入公用頻道</string>
+ <string name="create_private_group_chat">建立私人群組聊天</string>
+ <string name="create_public_channel">建立公用頻道</string>
+ <string name="create_dialog_channel_name">頻道名稱</string>
+ <string name="xmpp_address">XMPP 位址</string>
+ <string name="event">活動</string>
+ <string name="open_backup">開啟備份</string>
+ <string name="local_server">本機伺服器</string>
+ <string name="category_about">關於</string>
<string name="rtp_state_declined_or_busy">忙碌</string>
+ <string name="help">說明</string>
+ <string name="add_to_favorites">釘選</string>
+ <string name="remove_from_favorites">取消釘選</string>
+ <string name="exit">離開</string>
+ <string name="play_audio">播放音訊</string>
+ <string name="more_options">更多選項</string>
</resources>
@@ -416,6 +416,7 @@
<string name="video">video</string>
<string name="image">image</string>
<string name="vector_graphic">vector graphic</string>
+ <string name="multimedia_file">multimedia file</string>
<string name="pdf_document">PDF document</string>
<string name="apk">Android App</string>
<string name="vcard">Contact</string>
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="set_profile_picture">Quicksy 設定檔圖片</string>
+ <string name="not_available_in_your_country">Quicksy 在您的國家無法使用。</string>
+ <string name="unable_to_verify_server_identity">無法驗證伺服器身分。</string>
+ <string name="unknown_security_error">未知安全性錯誤。</string>
+ <string name="timeout_while_connecting_to_server">連線伺服器逾時。</string>
+</resources>